Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions documentation/components/libs/postgresql.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,10 @@ foreach (sql_query_functions($query)->all() as $func) {
}
```

Extractors report every node of their kind anywhere in the statement — DDL targets (`CREATE TABLE x AS …`,
`CREATE VIEW v …`, `SELECT … INTO t`), `FOR UPDATE OF t`, `excluded.*`, window `ORDER BY` — so filter the result
when you need only some of them.
Extractors report every node of their kind anywhere in the statement - DDL targets (`CREATE TABLE x AS ...`,
`CREATE VIEW v ...`, `SELECT ... INTO t`), `excluded.*`, window `ORDER BY` - so filter the result when you need only
some of them. `sql_query_tables()` returns relations only: it skips CTE references and `FOR UPDATE OF` names, and
includes the targets of `DROP`, `COMMENT ON`, `SECURITY LABEL` and `ALTER EXTENSION`.

### Parsing Utilities

Expand Down Expand Up @@ -574,6 +575,7 @@ Visitors declare which node types they handle via `nodeClasses()` (one or many).
- `ColumnRefCollector` - collects all `ColumnRef` nodes
- `FuncCallCollector` - collects all `FuncCall` nodes
- `RangeVarCollector` - collects all `RangeVar` nodes
- `RelationCollector` - collects a `RangeVar` per relation - skips CTE references and `FOR UPDATE OF` names, builds one for `DROP` / `COMMENT ON` targets

### Custom Modifiers

Expand Down
59 changes: 33 additions & 26 deletions documentation/upgrading.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,24 +280,23 @@ final class MyExtractor implements Extractor

### 27) `flow-php/postgresql` - `Traverser` visits every node

| Before | After |
|-----------------------------------------------------------------------------------------------|-----------------------------------------------------------|
| `sql_query_tables('CREATE TABLE x AS SELECT * FROM t')` - `[]` | `[t, x]` |
| `sql_query_tables('SELECT * FROM t JOIN u ON true FOR UPDATE OF t')` - `[t, u]` | `[t, u, t]` - every reference, filter duplicates yourself |
| `sql_query_tables('CREATE VIEW v AS SELECT a FROM src')` - `[]` | `[v, src]` |
| `sql_query_tables('SELECT a INTO new_t FROM src')` - `[src]` | `[new_t, src]` |
| `sql_query_columns('… ON CONFLICT (name) DO UPDATE SET name = excluded.name')` - `[]` | `[excluded.name]` |
| `OrderBy` of `SELECT a, row_number() OVER (ORDER BY b) FROM t ORDER BY a` - 1 clause | 2 clauses - window `ORDER BY` included |
| `sql_query_tables('SELECT (SELECT x FROM a) FROM b')` - `[b, a]` | `[a, b]` - descriptor (PostgreSQL walker) order |
| `sql_query_depth()`: `EXPLAIN SELECT 1` 0, `CREATE VIEW v AS SELECT 1` 0, window subquery 1 | 1, 1, 2 |
| `sql_to_keyset_query()` cursor on `… WHERE $1 IN (SELECT …)` - `$1`, clashing with the user's | `$2` |
| `TypeCastStripper` left casts under a `SubLink` test, a window and `COLLATE` | stripped |
| Before | After |
|---------------------------------------------------------------------------------------------------|-------------------------------------------------|
| `sql_query_tables('CREATE TABLE x AS SELECT * FROM t')` - `[]` | `[t, x]` |
| `sql_query_tables('CREATE VIEW v AS SELECT a FROM src')` - `[]` | `[v, src]` |
| `sql_query_tables('SELECT a INTO new_t FROM src')` - `[src]` | `[new_t, src]` |
| `sql_query_columns('... ON CONFLICT (name) DO UPDATE SET name = excluded.name')` - `[]` | `[excluded.name]` |
| `OrderBy` of `SELECT a, row_number() OVER (ORDER BY b) FROM t ORDER BY a` - 1 clause | 2 clauses - window `ORDER BY` included |
| `sql_query_tables('SELECT (SELECT x FROM a) FROM b')` - `[b, a]` | `[a, b]` - descriptor (PostgreSQL walker) order |
| `sql_query_depth()`: `EXPLAIN SELECT 1` 0, `CREATE VIEW v AS SELECT 1` 0, window subquery 1 | 1, 1, 2 |
| `sql_to_keyset_query()` cursor on `... WHERE $1 IN (SELECT ...)` - `$1`, clashing with the user's | `$2` |
| `TypeCastStripper` left casts under a `SubLink` test, a window and `COLLATE` | stripped |

### 28) `flow-php/postgresql` - traversal contract: messages, depth, replacement, `REMOVE_NODE`

| Before | After |
|-------------------------------------------------------------------|------------------------------------------------------------------------------------------|
| `ModificationContext::ancestors()` / `parent()` - `Node` wrappers | the real messages (`SelectStmt`, `RangeSubselect`, …), no `Node` wrappers |
| `ModificationContext::ancestors()` / `parent()` - `Node` wrappers | the real messages (`SelectStmt`, `RangeSubselect`, ...), no `Node` wrappers |
| depth of a CTE body - 3 | 4 - `WithClause`, `WindowDef`, `IntoClause`, `OnConflictClause` are levels too |
| a replacement returned below the top-level statement - ignored | written into its slot |
| a replacement of the wrong class - ignored | `ParserException` |
Expand All @@ -310,15 +309,15 @@ final class MyExtractor implements Extractor
| `ExplainModifier::nodeClasses()` - `[SelectStmt::class]` | `[ParseResult::class]` |
| INSERT/UPDATE/DELETE/MERGE/CTAS/EXECUTE/DECLARE - returned unwrapped | wrapped in `EXPLAIN` |
| `sql_to_explain('CREATE TABLE x (a int)')`, `sql_to_explain('EXPLAIN SELECT 1')` - returned unwrapped | `InvalidStatementException` |
| `$client->explain('INSERT …')` - the `INSERT` ran and committed | EXPLAIN; with ANALYZE inside a transaction (savepoint when one is open) that is always rolled back |
| `traverse(new PaginationModifier(…), new ExplainModifier(…))` - both applied | the pagination is dropped (or the traversal throws) - call `traverse()` again with `ExplainModifier` alone, last |
| `$client->explain('INSERT ...')` - the `INSERT` ran and committed | EXPLAIN; with ANALYZE inside a transaction (savepoint when one is open) that is always rolled back |
| `traverse(new PaginationModifier(...), new ExplainModifier(...))` - both applied | the pagination is dropped (or the traversal throws) - call `traverse()` again with `ExplainModifier` alone, last |

### 30) `flow-php/postgresql` - keyset pagination wraps `UNION` / `INTERSECT` / `EXCEPT`

| Before | After |
|-----------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|
| `sql_to_keyset_query('SELECT id FROM t UNION SELECT id FROM u ORDER BY id', …)` - cursor dropped, page 2 fails with `08P01` | `SELECT * FROM (…) _keyset_subq WHERE id > $1 ORDER BY id LIMIT …` |
| qualified keyset column (`t.id`) on a set operation - `42P01` at run time | `PaginationException` |
| Before | After |
|-------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------|
| `sql_to_keyset_query('SELECT id FROM t UNION SELECT id FROM u ORDER BY id', ...)` - cursor dropped, page 2 fails with `08P01` | `SELECT * FROM (...) _keyset_subq WHERE id > $1 ORDER BY id LIMIT ...` |
| qualified keyset column (`t.id`) on a set operation - `42P01` at run time | `PaginationException` |

### 31) `flow-php/postgresql` - schema keeps the declared expression text, compares normalised keys

Expand All @@ -335,20 +334,28 @@ final class MyExtractor implements Extractor

### 32) `flow-php/postgresql` - schema DDL emits index `WHERE` and trigger `WHEN`

| Before | After |
|----------------------------------------------------------------|-------------------------------------------------------------------------------|
| `CREATE UNIQUE INDEX t_email_live ON s.t (email)` | `CREATE UNIQUE INDEX t_email_live ON s.t (email) WHERE deleted_at IS NULL` |
| `CREATE TRIGGER t_trg … FOR EACH ROW EXECUTE FUNCTION f()` | `CREATE TRIGGER t_trg … FOR EACH ROW WHEN (new.i > 0) EXECUTE FUNCTION s.f()` |
| an unqualified trigger function resolved through `search_path` | resolved to the table's schema |
| introspected `Trigger::$functionName` - `name` | `schema.name`; new `Trigger::withFunctionSchema()` |
| a declared `'s.f'` always drifted against the catalog | no drift |
| Before | After |
|----------------------------------------------------------------|---------------------------------------------------------------------------------|
| `CREATE UNIQUE INDEX t_email_live ON s.t (email)` | `CREATE UNIQUE INDEX t_email_live ON s.t (email) WHERE deleted_at IS NULL` |
| `CREATE TRIGGER t_trg ... FOR EACH ROW EXECUTE FUNCTION f()` | `CREATE TRIGGER t_trg ... FOR EACH ROW WHEN (new.i > 0) EXECUTE FUNCTION s.f()` |
| an unqualified trigger function resolved through `search_path` | resolved to the table's schema |
| introspected `Trigger::$functionName` - `name` | `schema.name`; new `Trigger::withFunctionSchema()` |
| a declared `'s.f'` always drifted against the catalog | no drift |

### 33) `flow-php/postgresql` - a failed `SAVEPOINT` leaves the outer transaction open

| Before | After |
|---------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|
| nesting level reset to 0, the caller's `rollBack()` throws, the connection stays in `25P02` | nesting level kept; the caller's `rollBack()` ends the outer transaction |

### 34) `flow-php/postgresql` - `sql_query_tables()` skips CTE references, reports `DROP` / `COMMENT ON` targets

| Before | After |
|--------------------------------------------------------------------------------------|------------|
| `sql_query_tables('WITH c AS (SELECT * FROM users) SELECT * FROM c')` - `[c, users]` | `[users]` |
| `sql_query_tables('DROP TABLE a, s.b')` - `[]` | `[a, s.b]` |
| `sql_query_tables("COMMENT ON COLUMN s.t.c IS 'x'")` - `[]` | `[s.t]` |

---

## Upgrading from 0.43.x to 0.44.x
Expand Down
Loading
Loading