A role with SELECT on a table is refused COUNT(*) on it: "permission denied for routine count"
- Dominant language
- Go
- Stars
- 2.1k
- Forks
- 73
- Avg merge
- 1d 10h
- Merged PRs (30d)
- 129
Description
On DoltgreSQL 1.3.1, a login role granted `SELECT` on a table can read its rows, but `SELECT COUNT(*)` on
the same table fails with `ERROR: permission denied for routine count`. PostgreSQL 18.6 answers `3`: it
grants the `EXECUTE` privilege on functions to `PUBLIC` by default, so the role needs no grant to call
`count`.
## Reproduction
[`repro.sql`](https://github.com/Reliable-Collaboration/repro-doltgresql-bug-function-execute-default/blob/main/repro.sql):
```sql
-- As postgres: a table, and a role that may read it.
CREATE TABLE t (x int);
INSERT INTO t VALUES (1), (2), (3);
CREATE ROLE reader LOGIN PASSWORD 'password';
GRANT SELECT ON t TO reader;
-- As reader: the rows, then how many there are.
\connect - reader
SELECT * FROM t ORDER BY x;
SELECT COUNT(*) FROM t;
```
## Expected behavior
`reader` reads the three rows and counts them. This is what PostgreSQL 18.6 does, from the line where
`reader` logs in:
```
-- As reader: the rows, then how many there are.
\connect - reader
You are now connected to database "postgres" as user "reader".
SELECT * FROM t ORDER BY x;
x
---
1
2
3
(3 rows)
SELECT COUNT(*) FROM t;
count
-------
3
(1 row)
```
No grant beyond `SELECT` is needed, because PostgreSQL grants `EXECUTE` on functions to `PUBLIC`. From
[its documentation](https://www.postgresql.org/docs/18/ddl-priv.html): "For other types of objects, the
default privileges granted to `PUBLIC` are as follows: `CONNECT` and `TEMPORARY` (create temporary
tables) privileges for databases; `EXECUTE` privilege for functions and procedures; and `USAGE`
privilege for languages and data types (including domains)."
## Actual behavior
`reader` reads the three rows, but counting them fails. This is what DoltgreSQL 1.3.1 does, from the line
where `reader` logs in:
```
-- As reader: the rows, then how many there are.
\connect - reader
You are now connected to database "postgres" as user "reader".
SELECT * FROM t ORDER BY x;
x
---
1
2
3
(3 rows)
SELECT COUNT(*) FROM t;
psql:/tmp/repro.sql:10: ERROR: permission denied for routine count
```
## Run it
A runnable reproduction is at https://github.com/Reliable-Collaboration/repro-doltgresql-bug-function-execute-default. Its script runs the test on PostgreSQL and DoltgreSQL in throwaway containers and prints the two outputs side by side:
```sh
git clone https://github.com/Reliable-Collaboration/repro-doltgresql-bug-function-execute-default.git
cd repro-doltgresql-bug-function-execute-default
./repro.sh
```
## Other observations
Each was run on both servers, with the `psql` client in each image. PostgreSQL ran every statement named
here without an error; the results below are DoltgreSQL's, for a role without any `EXECUTE` grant unless
a grant is named.
- With `GRANT USAGE ON SCHEMA public` and `GRANT SELECT ON ALL TABLES IN SCHEMA public` instead of
`GRANT SELECT ON t`, `COUNT(*)` is refused the same way.
- Other aggregates and a window function are refused the same way: `count(x)`, `sum(x)`, `min(x)`,
`max(x)`, `avg(x)`, `bool_and(x > 0)` and `row_number() OVER (ORDER BY x)`, which answers
`permission denied for routine row_number`.
- No table is needed: `SELECT count(*) FROM (VALUES (1), (2)) AS v(x)` is refused too.
- `SELECT current_user` and `SELECT session_user` are both refused with
`permission denied for routine current_user`.
- A built-in named with its schema is refused: `SELECT pg_catalog.lower('A')` answers
`permission denied for routine lower`, while `SELECT lower('A')` answers `a`.
- These run without an error: `now()`, `lower('A')`, `upper('a')`, `length('a')`, `abs(-1)`,
`coalesce(NULL, 1)`, `version()`, `current_database()`, `generate_series(1, 2)`, `array_agg(x)` and
`string_agg(x::text, ',')`.
- A function created by `postgres` is refused too: after
`CREATE FUNCTION add1(i int) RETURNS int LANGUAGE plpgsql AS $$ BEGIN RETURN i + 1; END $$`,
`SELECT add1(1)` answers `permission denied for routine add1`.
- Granting the role `EXECUTE ON ALL FUNCTIONS IN SCHEMA pg_catalog` changes nothing: `COUNT(*)`,
`sum(x)`, `current_user` and `add1(1)` are still refused.
- Granting the role `EXECUTE ON ALL FUNCTIONS IN SCHEMA public` makes `COUNT(*)`, `sum(x)`,
`current_user` and `add1(1)` run, although `count` and `sum` are built-in functions. The same grant to
`PUBLIC` makes `COUNT(*)` and `add1(1)` run for a role that holds only `SELECT` on `t`.
- Once `current_user` runs, it answers `r3@127.0.0.1` for a role named `r3`, where PostgreSQL answers
`r3`.
- Granting the role `EXECUTE ON FUNCTION add1(int)` makes `add1(1)` run and leaves `COUNT(*)` refused.
- On PostgreSQL, `SELECT acldefault('f', 'postgres'::regrole)` answers
`{=X/postgres,postgres=X/postgres}`, `EXECUTE` for `PUBLIC`. DoltgreSQL answers
`function: 'acldefault' not found`, and `function: 'has_function_privilege' not found` to
`has_function_privilege('reader', 'lower(text)', 'EXECUTE')`, which PostgreSQL answers `t`.
## Possibly related
None found. #2317 (closed) is a different privilege problem, about `pg_type` over the extended protocol.
## Environment
- DoltgreSQL 1.3.1, the newest release when this was written: image `dolthub/doltgresql:1.3.1`, digest
`sha256:6c85cb1f35beabf47f094336a420255130b841b1645f36d79ef046276af36851`. Its bundled `psql` is 17.11.
- PostgreSQL 18.6: image `postgres:18.6-bookworm`, digest
`sha256:1c59e2c3c818eaa0f0628f695b36e7c9e362d6b219b36a54a32df645cbd7e1af`. Its bundled `psql` is 18.6.
- Reproduced on 2026-09-11 (UTC) with Docker 29.7.2 on Linux x86_64 (Ubuntu 26.04.1 LTS under WSL 2).
Contributor guide
Assessment
This issue has not been assessed yet.