Read-only enforcement for the native-query path (RunNativeQuery).
External Data Sources are read-only by contract, but a stored Query's fully-rendered SQL is
executed verbatim on a read/write connection. The provider-layer write backstop and
ReadOnlyExternalBaseEntity only cover Save/Delete — they do NOT see this path — so the
guarantee is enforced HERE, at the driver/engine boundary, not delegated to a caller.
cannot be parsed/validated in its dialect (can't prove it's read-only → refuse),
contains any write/DDL statement (INSERT/UPDATE/DELETE/MERGE/DROP/EXEC/CALL/… —
HasWriteStatement; unparseable data-modifying CTEs are refused by the parse gate above), or
is a SELECT ... INTO <newtable> — it parses as a select (so HasWriteStatement is false),
but it CREATES a table as a side effect, so it's caught here via StatementKind, or
starts with a DCL verb (GRANT/REVOKE/DENY), after leading comments/whitespace are stripped —
a backstop because the underlying parser doesn't reliably surface these as a write type per-dialect.
Known limitation — defense-in-depth, NOT a substitute for a least-privilege source credential:
A side-effecting routine invoked from a read shape (SELECT writing_func(),
SELECT nextval('s')) is indistinguishable from a pure read in the AST and is NOT blocked.
Configure External Data Sources with a read-only/least-privilege credential as the real authority;
this screen is the app-level backstop against the common write/DDL/injection vectors.
Read-only enforcement for the native-query path (
RunNativeQuery).External Data Sources are read-only by contract, but a stored Query's fully-rendered SQL is executed verbatim on a read/write connection. The provider-layer write backstop and
ReadOnlyExternalBaseEntityonly coverSave/Delete— they do NOT see this path — so the guarantee is enforced HERE, at the driver/engine boundary, not delegated to a caller.Fail-closed. Throws when the SQL:
HasWriteStatement; unparseable data-modifying CTEs are refused by the parse gate above), orSELECT ... INTO <newtable>— it parses as aselect(soHasWriteStatementis false), but it CREATES a table as a side effect, so it's caught here viaStatementKind, orGRANT/REVOKE/DENY), after leading comments/whitespace are stripped — a backstop because the underlying parser doesn't reliably surface these as a write type per-dialect.Known limitation — defense-in-depth, NOT a substitute for a least-privilege source credential:
SELECT writing_func(),SELECT nextval('s')) is indistinguishable from a pure read in the AST and is NOT blocked. Configure External Data Sources with a read-only/least-privilege credential as the real authority; this screen is the app-level backstop against the common write/DDL/injection vectors.