How It Works

A deep dive into the 5-phase pipeline that powers 3xcode pyspark convert, from raw SQL to production-quality, dependency-aware PySpark, plus the privacy architecture that keeps your source code off 3XDE's servers.

The 5-Phase Pipeline

Every 3xcode pyspark convert run (single file or bulk directory) moves each SQL file through five distinct phases. Each phase is its own fresh AI engine call, chained together by a runner that maps them onto the CLI's progress UI as four stages: Analysis (Discovery + Planning), Conversion, Validation, and Audit (Auto-Fix). Between Planning and Conversion sits one free, deterministic grouping step (no LLM call) that decides how objects get bundled into output files.

1

Discovery

Inventory every object, detect dialect + platform

2

Planning

Build dependency graph, order the conversion

3

Conversion

Generate PySpark, tier by tier

4

Validation

Three-stage correctness review

5

Auto-Fix

Safely repair HIGH/ERROR issues

Phase 1: Discovery

Discovery turns one raw SQL file into a structured inventory of every convertible object (procedures, functions, views, triggers) with exact line boundaries and metadata. Files over 4,000 lines are read in two chunks so large scripts don't blow the context window, and the agent prescans with a lightweight regex tool before reading the full file.

Dialect Detection

Classifies the file into one of tsql, plsql, plpgsql, mysql, ansi, spark_sql, or unknown. A normalizer maps dozens of LLM spellings onto these: 'Transact-SQL', 'Azure SQL', 'Synapse' → tsql; 'Postgres', 'Redshift' → plpgsql; 'Hive', 'HiveQL', 'Databricks SQL' → spark_sql, so nothing fails validation on a naming quirk.

Platform Auto-Detection

A separate, deterministic, regex-only pass (no LLM call) scores the SQL against 133 syntax markers across 10 platforms to guess the specific vendor behind a dialect family (an ansi file could be Snowflake, BigQuery, or Teradata). Fully explainable: the winning platform, confidence, and exact markers that fired are all recorded.

Platform detection covers 10 vendors: Databricks, Snowflake, PostgreSQL, MySQL, SQL Server, Oracle, Redshift, BigQuery, Teradata, and Hive. It never blocks discovery: if it fails, detection is simply skipped and the pipeline continues.

For each object, discovery records its type, exact start_line/end_line, parameters, cross-references to other objects, temp tables, cursor / dynamic-SQL / transaction flags, window-function count, a complexity score (Simple → Very Complex), known limitations, and the specific SQL constructs it uses, the same tags that later drive selective knowledge injection in Planning.

Discovery is deliberately honest rather than silently degrading: if the model's JSON response partially fails validation, every object that does validate on its own is salvaged rather than the whole result being discarded. A run ends in one of three states: OK (objects found), EMPTY (a clean, genuinely-nothing-to-convert file, e.g. DML-only, or already Spark SQL), or FAILED (unparseable / processing error). A failed discovery is treated as a hard pipeline failure with an honest audit, never a silent green "completed."

Phase 2: Planning

Planning reads every discovered object once and distills targeted, per-object conversion instructions, so the Conversion phase never needs the full knowledge base re-shipped to it. This alone saves 60–80% of input tokens versus re-sending knowledge on every conversion call.

Dependency-Ordered Conversion

Planning builds the full cross-object dependency graph, then topologically sorts it into conversion levels with an explicit precedence order: Views → Functions → Procedures → Triggers. Level 0 is views and standalone functions with no dependencies; every higher level depends only on lower ones.

Per-Object Instructions

For each object, the plan references exact SQL line numbers, names the specific PySpark pattern for each construct, flags relevant gotchas by number, notes limitations that need TODO comments, and specifies which dependency signatures are required before conversion.

The dependency-tier ordering matters in practice: a procedure that selects from a view needs that view's PySpark signature to exist (or at least be known) before it can be correctly converted or validated. If Claude's plan fails to parse, a fallback builds a trivial single-level sequential plan covering every object, so the pipeline degrades gracefully instead of dying.

A deterministic, no-LLM grouping step then runs between Planning and Conversion, bundling objects into output files by production-style rules: utilities (pure functions + simple procs called by 2+ other objects), main_pipeline (the highest-complexity procedure plus its exclusive helpers), and standalone (everything else, independently schedulable jobs).

Phase 3: Conversion

Conversion turns each output group into one production-quality .py file. Groups are processed in strict dependency tiers: utilities (0) → standalone (1) → main_pipeline (2), with groups within the same tier converting concurrently, but tiers always running in order.

A signature registry is threaded across tiers: after each tier finishes, every successfully converted group's public function signatures are extracted and added to the registry, snapshotted before the next tier starts. This means dependent code always sees the real signatures of what it calls, never a guess. It's the same dependency-ordering principle from Planning, applied at the output-file level.

Self-Validating Generation

Each conversion call has access to file read/write/edit and command-execution tools, plus two dedicated PySpark validators. The agent is instructed to check its own output and fix-and-rewrite on failure before ever handing back a result.

Timeout Salvage, Not Blind Retry

Up to 3 retries with exponential backoff, but a timeout doesn't retry (that would just burn another full timeout budget). Instead, if the partial file on disk still parses as valid Python, it's kept and flagged NEEDS_REVIEW rather than thrown away.

Phase 4: Validation

Every generated file goes through a three-stage, context-aware review:

StageWhat it checksCost
1. Local checksast.parse() for syntax validity, bare col() without F. prefix, .collect() OOM risk, TODO-comment countingFree, no API call
2. Dependency contextWalks the conversion plan's dependency edges to find which other generated files this one depends on, and extracts their signatures as ground truthFree, local
3. Claude reviewApplies the full validation checklist and gotcha list; when dependency context exists, verifies every called function actually exists with a matching signature and argument countFresh LLM call

Real gotcha categories the pipeline actively checks for, pulled from the knowledge base:

CategoryGotcha
Date functionsDATEDIFF argument order reversed between T-SQL and Spark
Date functionsmonths_between returns DOUBLE, not INT
Date functionsdate_sub with a negative value goes forward in time, not backward
Window functionsWindow frame RANGE includes duplicate rows unexpectedly
NULL semanticsNULL ordering in ORDER BY differs between source dialect and Spark
NULL semanticsCOUNT(*) vs COUNT(col) behave differently with NULLs
String opsCase-sensitive string comparison: Spark is case-sensitive by default
CursorsPython UDFs are 50–100x slower than native `F.xxx`; cursors should become window functions, not UDF loops
Type castingDecimal arithmetic precision loss
API deprecations.unionAll() removed in PySpark 3.x → .union()
API deprecations.registerTempTable() removed → .createOrReplaceTempView()
No direct equivalentCursors, dynamic SQL / `sp_executesql`, transactions, MERGE/upsert, recursive queries, dynamic pivot: flagged for manual review
InfrastructureDelta Lake provisioning, Change Data Feed, JDBC connections, HDFS/ADLS/S3 paths, Kafka/streaming: needs external setup, not code

Only relevant knowledge sections are pulled into the prompt based on which constructs (date functions, window functions, NULL handling, string ops, cursors, CTEs, JSON, type casts, stored procs, MERGE, temp tables, joins, aggregations) are actually detected in the SQL, cutting a simple SELECT+JOIN prompt by roughly 81%.

Phase 5: Auto-Fix

Every validation issue is first classified into an action bucket: infrastructure (needs external setup, never auto-fixed), manual (cursors, dynamic SQL, transactions, MERGE, recursive queries; no safe automatic transform, HIGH/ERROR only), auto_fix (any other HIGH/ERROR issue, eligible for a fix pass), or review (below HIGH/ERROR severity, surfaced for awareness, never touched).

Only files with at least one auto_fix-eligible issue get a fresh fix-pass call (tools limited to Read and Write). Claude fixes only the listed issues, preserves TODOs/docstrings/blank lines exactly, and explicitly skips anything unsafe rather than guessing.

Safety Guardrails: Applied Before Any Fix Is Accepted

  1. No parseable output: the whole fix attempt is marked FAILED; the original file on disk is never touched.
  2. Truncation guard: if the fixed content is more than 50% shorter than the original, the fix is rejected and REVERTED. Original file is never overwritten.
  3. Syntax safety net: `ast.parse()` on the fixed content; a SyntaxError means REVERTED, with the exact line and message recorded.
  4. Write-after-verify: only if both guardrails pass does the fixed file actually get written to disk. The write is the last step, never the first, so a bad fix can never clobber a good file.
  5. Partial tracking: any issue the model explicitly skipped is matched back to the original issue list, and the fix is marked PARTIAL rather than FIXED if anything remains.

Every issue across every file finally lands in one of four developer-facing buckets: auto_fixed, requires_manual (with prescriptive fix guidance, e.g. "rewrite as a window function using F.lag()/F.lead()"), infrastructure_setup, and recommended_review, each with a priority level, drawn from a lookup table of roughly 20 known patterns. Every output file also gets a per-file confidence/completion grade (A+ through F) so you know at a glance which files need a human look before they ship.

Your Code Never Leaves Your Machine

This is a genuine architectural boundary, not a policy promise. 3xcode splits cleanly into two data paths, and your SQL source code is only ever on one of them.

What Goes to the 3XDE Backend

  • Auth credentials and session tokens
  • License validation and conversion-count metadata
  • A hashed machine fingerprint (X-Machine-ID) and CLI version, never raw hostname/arch values
  • BYOK provider metadata: provider name + preferred models only, never the actual key
  • Usage tracking (cost, duration, success rate) for your account

What Never Goes to the 3XDE Backend

  • Your SQL source code, not one line, ever
  • Your generated PySpark output
  • Your BYOK provider API keys/secrets, stored locally via your OS keychain, with a JSON fallback
  • Raw machine identifiers (hostname, architecture, OS name)

Here's the flow that makes this true: the 3XDE backend supplies the conversion prompts and the SQL-to-PySpark knowledge base (gotchas, mapping tables, limitations) at runtime. Nothing ships inside the installed package, which is also how the proprietary conversion logic stays protected. But once those prompts and knowledge are assembled locally, the CLI sends your SQL source directly to the LLM provider (Anthropic, AWS Bedrock, Google Vertex AI, or Azure AI Foundry) from your own machine. The 3XDE backend is never in that request path. It handles auth, licensing, and usage metadata; it does not proxy, log, or ever see your source code.

Local Key Storage

BYOK provider keys are stored via your OS keychain (macOS Keychain, Linux Secret Service, Windows Credential Manager) with a JSON fallback locked to owner-only permissions. The backend only ever registers the provider name and preferred models, never the secret.

Direct-to-Provider Requests

Whether you use a platform-provided key or your own BYOK key, the conversion call itself goes straight from your machine to the LLM provider's API. 3XDE's servers are not an intermediary for that traffic.

You can see this boundary directly in how failures are handled: if the backend is unreachable, the CLI first tries an encrypted local cache of the last-fetched prompts, and only if that fallback also fails does it give up, raising an honest PromptUnavailableError and writing an audit explaining why, rather than silently doing something it can't stand behind.

What actually happens on `3xcode pyspark convert`
# 1. CLI authenticates + fetches license/usage metadata from the 3XDE backend
# 2. CLI resolves conversion prompts + knowledge base from the 3XDE backend
#    (falls back to an encrypted local cache if unreachable; fails honestly if both fail)
# 3. CLI reads your .sql file (this stays on your machine)
# 4. CLI sends the SQL + assembled prompt DIRECTLY to your LLM provider
#    (Anthropic / Bedrock / Vertex / Azure, never routed through 3XDE)
# 5. Generated PySpark is written to your local output directory

3xcode pyspark convert your_file.sql