diff --git a/ISSUES.md b/ISSUES.md index 5f3039f..81e8808 100644 --- a/ISSUES.md +++ b/ISSUES.md @@ -71,12 +71,12 @@ Legend: `[ ]` open · `[x]` resolved · `[~]` won't fix / by design. ## P4 — Display & docs -- [ ] **E1 · `format_sql` splits `LEFT/RIGHT/FULL OUTER JOIN` across two lines.** +- [x] **E1 · `format_sql` splits `LEFT/RIGHT/FULL OUTER JOIN` across two lines.** The keyword list processes bare `JOIN` before the multi-word forms, so the newline is inserted mid-keyword. Cosmetic but mangles a teaching artifact. *Fix:* order the keyword list longest-first (or special-case `… OUTER JOIN`). -- [ ] **E2 · README overstates SQLite zero-setup for outer joins.** `RIGHT`/`FULL +- [x] **E2 · README overstates SQLite zero-setup for outer joins.** `RIGHT`/`FULL OUTER JOIN` require SQLite ≥ 3.39 (2022); older platform Pythons raise `OperationalError`. *Fix:* add a one-line version caveat near the outer-join docs / backend-coverage note. diff --git a/README.md b/README.md index a6f4a34..294d3e7 100644 --- a/README.md +++ b/README.md @@ -218,6 +218,10 @@ s.outer_join(sp, how="right") # all shipments, even without suppliers s.outer_join(sp, how="full") # both ``` +> **Note:** `how="right"` and `how="full"` require **SQLite 3.39+** (2022), +> when SQLite added `RIGHT`/`FULL OUTER JOIN`. `how="left"` works on all +> supported versions. PostgreSQL and MySQL support all three. + ### Set Operations These require both relations to have *identical schemas* (same attribute @@ -409,6 +413,9 @@ identifier quoting, and introspects table schemas from the database. > schema introspection branches all exist — but are not currently run > against live databases in CI. If you use coddpiece on PG or MySQL and > spot a regression in those paths, please open an issue. +> +> Note that `RIGHT`/`FULL OUTER JOIN` require **SQLite 3.39+** (2022); +> older SQLite builds will reject those queries. ### Complete Operation Reference diff --git a/coddpiece/display.py b/coddpiece/display.py index 36f252a..030be20 100644 --- a/coddpiece/display.py +++ b/coddpiece/display.py @@ -486,9 +486,28 @@ def format_sql(sql: str) -> str: for kw in ["FROM", "WHERE", "JOIN", "LEFT OUTER JOIN", "RIGHT OUTER JOIN", "FULL OUTER JOIN", "GROUP BY", "HAVING", "ORDER BY", "UNION", "INTERSECT", "EXCEPT"]: - # Only break before top-level clauses (not inside subqueries) + # Only break before top-level clauses (not inside subqueries). + # + # The bare "JOIN" keyword is a space-delimited substring of every + # multi-word form ("LEFT OUTER JOIN", etc.). Without the guard below, + # this loop's bare-JOIN pass matches the space *inside* an already + # emitted "LEFT OUTER JOIN" and splits it across two lines + # ("LEFT OUTER\nJOIN ..."). Crucially, reordering the list so the + # multi-word forms run first does NOT help: the later bare-JOIN pass + # re-splits them regardless. The negative lookbehind makes bare JOIN + # decline to match a JOIN that belongs to a compound keyword, leaving + # the dedicated multi-word rules to break those clauses as one unit. + if kw == "JOIN": + pattern = ( + r'\s+' + r'(?