Skip to content

Commit 695719f

Browse files
committed
chore: add docs with analysis on the open issues
1 parent 42c6d7e commit 695719f

File tree

2 files changed

+133
-0
lines changed

2 files changed

+133
-0
lines changed

plans/ISSUE_POSTGRES_SCHEMA.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
Issue summary
2+
3+
cloudsync_init('users') fails in Supabase postgres with:
4+
"column reference \"id\" is ambiguous".
5+
Both public.users and auth.users exist. Several PostgreSQL SQL templates use only table_name (no schema), so information_schema lookups and dynamic SQL see multiple tables and generate ambiguous column references.
6+
7+
Proposed fixes (options)
8+
9+
1) Minimal fix (patch specific templates)
10+
- Add table_schema = current_schema() to information_schema queries.
11+
- Keep relying on search_path.
12+
- Resolves Supabase default postgres collisions without changing the API.
13+
14+
2) Robust fix (explicit schema support)
15+
- Allow schema-qualified inputs, e.g. cloudsync_init('public.users').
16+
- Parse schema/table and propagate through query builders.
17+
- Always generate fully-qualified table names ("schema"."table").
18+
- Apply schema-aware filters in information_schema queries.
19+
- Removes ambiguity regardless of search_path or duplicate table names across schemas.
20+
- Note: payload compatibility requires cloudsync_changes.tbl to remain unqualified; PG apply should resolve schema via cloudsync_table_settings (not search_path) when applying payloads.
21+
22+
Bugged query templates
23+
24+
Already fixed:
25+
- SQL_PRAGMA_TABLEINFO_PK_COLLIST
26+
- SQL_PRAGMA_TABLEINFO_PK_DECODE_SELECTLIST
27+
28+
Still vulnerable (missing schema filter):
29+
- SQL_BUILD_SELECT_NONPK_COLS_BY_ROWID
30+
- SQL_PRAGMA_TABLEINFO_LIST_NONPK_NAME_CID
31+
- SQL_CLOUDSYNC_DELETE_COLS_NOT_IN_SCHEMA_OR_PKCOL
32+
- SQL_PRAGMA_TABLEINFO_PK_QUALIFIED_COLLIST_FMT
33+
34+
Robust fix implementation plan
35+
36+
Goals
37+
- Support cloudsync_init('users') and cloudsync_init('public.users')
38+
- Default schema to current_schema() when not provided
39+
- Persist schema so future connections are independent of search_path
40+
- Generate fully qualified table names in all PostgreSQL SQL builders
41+
42+
1) Parse schema/table at init
43+
- In cloudsync_init_table() (cloudsync.c), parse the input table_name:
44+
- If it contains a dot, split schema/table
45+
- Else schema = current_schema() (query once)
46+
- Normalize case to match existing behavior
47+
48+
2) Persist schema in settings
49+
- Store schema in cloudsync_table_settings using key='schema'
50+
- Keep tbl_name as unqualified table name
51+
- On first run, if schema is not stored, write it
52+
53+
3) Store schema in context
54+
- Add char *schema to cloudsync_table_context
55+
- Populate on table creation and when reloading from settings
56+
- Use schema when building SQL
57+
58+
4) Restore schema on new connections
59+
- During context rebuild, read schema from cloudsync_table_settings
60+
- If missing, fallback to current_schema(), optionally persist it
61+
62+
5) Qualify SQL everywhere (Postgres)
63+
- Use "schema"."table" in generated SQL
64+
- Add table_schema filters to information_schema queries:
65+
- SQL_BUILD_SELECT_NONPK_COLS_BY_ROWID
66+
- SQL_PRAGMA_TABLEINFO_LIST_NONPK_NAME_CID
67+
- SQL_CLOUDSYNC_DELETE_COLS_NOT_IN_SCHEMA_OR_PKCOL
68+
- SQL_PRAGMA_TABLEINFO_PK_QUALIFIED_COLLIST_FMT
69+
- Any other information_schema templates using only table_name
70+
71+
6) Compatibility
72+
- Existing DBs without schema setting continue to work via current_schema()
73+
- No API changes required for unqualified names
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# WARNING: resource was not closed: relation "cloudsync_changes"
2+
3+
## Summary
4+
The warning was emitted by PostgreSQL when a SPI query left a “relation” resource open. In practice, it means a SPI tuptable (or a relation opened internally by SPI when executing a query) wasn’t released before the outer SQL statement completed. PostgreSQL 17 is stricter about reporting this, so the same issue might have been silent in earlier versions.
5+
6+
We isolated the warning to the `cloudsync_payload_apply` path when it inserted into the `cloudsync_changes` view and triggered `cloudsync_changes_insert_trigger`. The warnings did **not** occur for direct, manual `INSERT INTO cloudsync_changes ...` statements issued in psql.
7+
8+
## Why it only happened in the payload-apply path
9+
The key difference was **nested SPI usage** and **statement lifetime**:
10+
11+
1. **`cloudsync_payload_apply` loops many changes and uses SPI internally**
12+
- `cloudsync_payload_apply` is a C function that processes a payload by decoding multiple changes and applying them in a loop.
13+
- For each change, it executed an `INSERT INTO cloudsync_changes (...)` (via `SQL_CHANGES_INSERT_ROW`), which fires the INSTEAD OF trigger (`cloudsync_changes_insert_trigger`).
14+
15+
2. **The trigger itself executed SPI queries**
16+
- The trigger function uses SPI to read and write metadata tables.
17+
- This creates *nested* SPI usage within a call stack that is already inside a SPI-driven C function.
18+
19+
3. **Nested SPI + `INSERT INTO view` has different resource lifetime than a plain insert**
20+
- With a manual psql statement, the SPI usage occurs only once, in a clean top-level context. The statement finishes, SPI cleanup happens, and any tuptable resources are released.
21+
- In the payload apply path, SPI queries happen inside the trigger, inside another SPI-driven C function, inside a loop. If any intermediate SPI tuptable or relation is not freed, it can “leak” out of the trigger scope and be reported when the outer statement completes.
22+
- That’s why the warning appears specifically when the trigger is executed as part of `cloudsync_payload_apply` but not for direct inserts from psql.
23+
24+
4. **PostgreSQL 17 reports this more aggressively**
25+
- Earlier versions often tolerated missing `SPI_freetuptable()` calls without warning. PG17 emits the warning when the statement finishes and resources are still registered as open.
26+
27+
## Why direct INSERTs from psql didn’t warn
28+
The smoke test included a manual `INSERT INTO cloudsync_changes ...`, and it never produced the warning. That statement:
29+
30+
- Runs as a single SQL statement initiated by the client.
31+
- Executes the trigger in a clean SPI call stack with no nested SPI calls.
32+
- Completes quickly, and the SPI context is unwound immediately, which can mask missing frees.
33+
34+
In contrast, the payload-apply path:
35+
36+
- Opens SPI state for the duration of the payload apply loop.
37+
- Executes many trigger invocations before returning.
38+
- Accumulates any unfreed resources over several calls.
39+
40+
So the leak only becomes visible in the payload-apply loop.
41+
42+
## Fix that removed the warning
43+
We introduced a new SQL function that bypasses the trigger and does the work directly:
44+
45+
- Added `cloudsync_changes_apply(...)` and rewired `SQL_CHANGES_INSERT_ROW` to call it via:
46+
```sql
47+
SELECT cloudsync_changes_apply(...)
48+
```
49+
- The apply function executes the same logic but without inserting into the view and firing the INSTEAD OF trigger.
50+
- This removes the nested SPI + trigger path for the payload apply loop.
51+
52+
Additionally, we tightened SPI cleanup in multiple functions by ensuring `SPI_freetuptable(SPI_tuptable)` is called after `SPI_execute`/`SPI_execute_plan` calls where needed.
53+
54+
## Takeaway
55+
The warning was not tied to the `cloudsync_changes` view itself, but to **nested SPI contexts and missing SPI cleanup** during payload apply. It was only visible when:
56+
57+
- the apply loop executed many insert-trigger calls, and
58+
- the server (PG17) reported unclosed relation resources at statement end.
59+
60+
By switching to `cloudsync_changes_apply(...)` and tightening SPI tuptable cleanup, we removed the warning from the payload-apply path while leaving manual insert behavior unchanged.

0 commit comments

Comments
 (0)