Repository: edgedb/edgedb

Issue:
InternalServerError: missing FROM-clause entry for table "ins_contents~3" when access policies are applied
<!-- Please search existing issues to avoid creating duplicates. -->

The `InternalServerError: missing FROM-clause entry for table "ins_contents~3"` error sometimes occurs in deeply nested queries *only when access policies are applied*

<!--
For the EdgeDB Version: run `edgedb query 'select sys::get_version_as_str()'` from your project directory (or run `select sys::get_version_as_str();` in the EdgeDB interactive shell).
For the EdgeDB CLI Version: Run `edgedb --version` from anywhere
-->

- EdgeDB Version: `3.4+301ba34`
- EdgeDB CLI Version: `3.5.0+907ff37`
- OS Version: `Ubuntu 23.04`

Please note that the query and schema has been modified to simplify the queries and schema

## 1. Setup
Set `global current_accounts_array` to contain `bffeb170-6d5d-11ee-bcd6-ad19361485ad`

```eql
insert Account {
    id := <uuid>"bffeb170-6d5d-11ee-bcd6-ad19361485ad",
    address := "0x9D2B268D6e51074c466b823Fd3A3b794cA750CB0"
}
```

## 2. Observe problem
### 2.1 Enable access policies and run problematic query
Expect `InternalServerError: missing FROM-clause entry for table "ins_contents~3"`
Please note these queries are generated by the js client, but occur in the ui as well
```eql
INSERT default::Policy {
      account := (
        WITH
          __scope_0_defaultAccount := DETACHED default::Account
        SELECT __scope_0_defaultAccount {
          id
        }
        FILTER (__scope_0_defaultAccount.address = "0x9D2B268D6e51074c466b823Fd3A3b794cA750CB0")
      ),
      key := <default::uint16>1,
      stateHistory := (
        INSERT default::PolicyState {
          proposal := (
            INSERT default::TransactionProposal {
              hash := "0x8ab322da38c5c044c903b2de5bba9d8f93fee416a7fd5047bd516ae1a9aa5eee",
              account := (
                WITH
                  __scope_1_defaultAccount := DETACHED default::Account
                SELECT __scope_1_defaultAccount {
                  id
                }
                FILTER (__scope_1_defaultAccount.address = "0x9D2B268D6e51074c466b823Fd3A3b794cA750CB0")
              )
            }
          ),
        }
      )
    }
```

### 2.3 Disable access policies and re-run problematic query
Expect query to succeed

## 3. Workaround using WITH
Moving the `INSERT default::TransactionProposal` into a with block fixes the issue.

### 3.1 Enable access policies and run query
Expect query to succeed
```eql
WITH nestedProposal := (
  INSERT default::TransactionProposal {
              hash := "0x8ab322da38c5c044c903b2de5bba9d8f93fee416a7fd5047bd516ae1a9aa5ee5",
              account := (
                WITH
                  __scope_1_defaultAccount := DETACHED default::Account
                SELECT __scope_1_defaultAccount {
                  id
                }
                FILTER (__scope_1_defaultAccount.address = "0x9D2B268D6e51074c466b823Fd3A3b794cA750CB0")
              ),
              nonce := <default::uint64>5n
            }
)
INSERT default::Policy {
      account := (
        WITH
          __scope_0_defaultAccount := DETACHED default::Account
        SELECT __scope_0_defaultAccount {
          id
        }
        FILTER (__scope_0_defaultAccount.address = "0x9D2B268D6e51074c466b823Fd3A3b794cA750CB0")
      ),
      key := <default::uint16>5,
      stateHistory := (
        INSERT default::PolicyState {
          proposal := nestedProposal,
        }
      )
    }
```

<!-- If the issue is about a query error, please also provide your schema -->

---
## Schema
```
module default {
  global current_accounts_array: array<uuid>;
  global current_accounts_set := array_unpack(global current_accounts_array);
  global current_accounts := <Account>(global current_accounts_set);

  type Account {
    required address: str { constraint exclusive; }

    access policy members_select_insert_update
      allow select, insert, update
      using (.id in global current_accounts_set);
  }

type Policy {
    required account: Account;
    required key: int32;

    required multi stateHistory: PolicyState {
      constraint exclusive;
      on source delete delete target;
      on target delete allow;
    }

    constraint exclusive on ((.account, .key));

    access policy members_select_insert_update
      allow select, insert, update
      using (.account in global current_accounts);

    access policy can_be_deleted_when_inactive
      allow delete
      using (not .isActive);
  }

type PolicyState {
    link policy := .<stateHistory[is Policy];
    proposal: TransactionProposal {
      on source delete delete target; 
      on target delete delete source;
    }
  }

type TransactionProposal {
    required hash: str { constraint exclusive; }
    required account: Account;
    required nonce: int32;

    constraint exclusive on ((.account, .nonce));
    index on (.nonce);

    access policy members_only
      allow all
      using (.account in global current_accounts);
  }
}
```


Discussion:
I have run into this problem in the past as well when doing multiple of the same selects in a deeply nested select query, just as is done with the multiple scope defaultAccount selects here
Could you share the schema?
> Could you share the schema?

Editied the issue to include a slimmed down schema
The minimized schema doesn't quite work: `can_be_deleted_when_inactive` uses a nonexistent property, so I deleted that. The query also required a bit of tweaking, but I have been able to reproduce this.
Further minimization:
```
module default {
  global current_account_id: uuid;
  global current_account := <Account>(global current_account_id);

  type Account {
    required address: str { constraint exclusive; }
  }

  type Policy {
    multi stateHistory: PolicyState;
  }

  type PolicyState {
    proposal: TransactionProposal;
  }

  type TransactionProposal {
    required account: Account;

    access policy members_only
      allow all
      using (.account in global current_account);
  }
}
```

```
INSERT default::Policy {
  stateHistory := (
    INSERT default::PolicyState {
      proposal := (
        INSERT default::TransactionProposal {
          account := (
            SELECT Account
            FILTER .address = "0x9D2B268D6e51074c466b823Fd3A3b794cA750CB0"
          )
        }
      ),
    }
  )
};
```

Replacing the cast in the definition of `current_accounts` with an explicit select and filter seemed to fix things, so that's where I'll start looking.

I also ran into about a million stupid migration bugs when trying to change the definitions of globals that I will look at after
Making `stateHistory` single makes the issue go away. Making `proposal` multi causes no change
Looks like a stray volatility ref :(
The cast desugars internally to something based on a for loop, which links against volatility refs

Resolving patch:
diff --git a/edb/pgsql/compiler/context.py b/edb/pgsql/compiler/context.py
--- a/edb/pgsql/compiler/context.py
+++ b/edb/pgsql/compiler/context.py
@@ -420,6 +420,7 @@ def __init__(
                 self.path_scope = collections.ChainMap()
                 self.rel_hierarchy = {}
                 self.scope_tree = prevlevel.scope_tree.root
+                self.volatility_ref = ()
 
                 self.disable_semi_join = frozenset()
                 self.force_optional = frozenset()


Test patch:
diff --git a/tests/test_edgeql_insert.py b/tests/test_edgeql_insert.py
--- a/tests/test_edgeql_insert.py
+++ b/tests/test_edgeql_insert.py
@@ -1218,6 +1218,57 @@ async def test_edgeql_insert_conflict_policy_02(self):
                 "violates exclusivity constraint"):
             await self.con.execute(Q)
 
+    async def test_edgeql_insert_policy_cast(self):
+        # Test for #6305, where a cast in a global used in an access policy
+        # was causing a stray volatility ref to show up in the wrong CTE
+        await self.con.execute('''
+            create global sub_id -> uuid;
+            create global sub := <Subordinate>(global sub_id);
+            alter type Note {
+                create access policy asdf allow all using (
+                    (.subject in global sub) ?? false
+                )
+            };
+        ''')
+
+        sub = await self.con.query_single('''
+            insert Subordinate { name := "asdf" };
+        ''')
+
+        async with self.assertRaisesRegexTx(
+                edgedb.AccessPolicyError,
+                "violation on insert of default::Note"):
+            await self.con.execute('''
+                insert Person { notes := (insert Note { name := "" }) };
+            ''')
+
+        async with self.assertRaisesRegexTx(
+                edgedb.AccessPolicyError,
+                "violation on insert of default::Note"):
+            await self.con.execute('''
+                insert Person {
+                    notes := (insert Note {
+                        name := "",
+                        subject := assert_single(
+                          (select Subordinate filter .name = 'asdf'))
+                    })
+                };
+            ''')
+
+        await self.con.execute('''
+            set global sub_id := <uuid>$0
+        ''', sub.id)
+
+        await self.con.execute('''
+            insert Person {
+                notes := (insert Note {
+                    name := "",
+                    subject := assert_single(
+                      (select Subordinate filter .name = 'asdf'))
+                })
+            };
+        ''')
+
     async def test_edgeql_insert_for_01(self):
         await self.con.execute(r'''
             FOR x IN {3, 5, 7, 2}