Skip to main content

uqa_sql/semantics/effects/
read_only.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! SQL commands forbidden by read-only transactions and snapshot-setting rules.
8
9use super::{command_payload_may_write_database, query_may_write_database, QueryEffectContext};
10use crate::{
11    ast::{DropKind, RelationPersistence},
12    plan::{CommandPlan, UnifiedPlan},
13    SQLError,
14};
15
16pub fn read_only_error(command: &str) -> SQLError {
17    SQLError::Routine {
18        sqlstate: "25006".into(),
19        message: format!("cannot execute {command} in a read-only transaction"),
20    }
21}
22
23fn table_is_temporary(
24    context: &QueryEffectContext<'_>,
25    table: &str,
26) -> Result<Option<bool>, SQLError> {
27    context
28        .catalog
29        .table_persistence(table)
30        .map(|persistence| persistence.map(|value| value == RelationPersistence::Temporary))
31        .map_err(|error| SQLError::Internal(format!("resolve read-only target `{table}`: {error}")))
32}
33
34fn dml_command<'a>(
35    context: &QueryEffectContext<'_>,
36    table: &str,
37    command_name: &'a str,
38    command: &CommandPlan,
39) -> Result<Option<&'a str>, SQLError> {
40    match table_is_temporary(context, table)? {
41        Some(false) => Ok(Some(command_name)),
42        Some(true) if command_payload_may_write_database(context, command)? => {
43            Ok(Some(command_name))
44        }
45        Some(true) | None => Ok(None),
46    }
47}
48
49pub fn forbidden_command(
50    context: &QueryEffectContext<'_>,
51    plan: &UnifiedPlan,
52) -> Result<Option<&'static str>, SQLError> {
53    let UnifiedPlan::Command(command) = plan else {
54        let UnifiedPlan::Query(query) = plan else {
55            unreachable!();
56        };
57        return query_may_write_database(context, query).map(|mutates| mutates.then_some("SELECT"));
58    };
59    match command.as_ref() {
60        CommandPlan::CreateTable(_) | CommandPlan::CreateTableIfNotExists(_) => {
61            Ok(Some("CREATE TABLE"))
62        }
63        CommandPlan::CreateIndex(_) => Ok(Some("CREATE INDEX")),
64        CommandPlan::Insert(insert) => dml_command(context, &insert.table, "INSERT", command),
65        CommandPlan::Update(update) => dml_command(context, &update.table, "UPDATE", command),
66        CommandPlan::Delete(delete) => dml_command(context, &delete.table, "DELETE", command),
67        CommandPlan::Merge(merge) => dml_command(context, &merge.target, "MERGE", command),
68        CommandPlan::Drop(drop) if drop.kind == DropKind::Table => Ok(Some("DROP TABLE")),
69        CommandPlan::Drop(drop) if drop.kind == DropKind::Sequence => Ok(Some("DROP SEQUENCE")),
70        CommandPlan::Drop(drop) if drop.kind == DropKind::Domain => Ok(Some("DROP DOMAIN")),
71        CommandPlan::Drop(_) => Ok(Some("DROP")),
72        CommandPlan::AlterTable(_) => Ok(Some("ALTER TABLE")),
73        CommandPlan::AlterForeignTable(_) => Ok(Some("ALTER FOREIGN TABLE")),
74        CommandPlan::AlterView(_) => Ok(Some("ALTER VIEW")),
75        CommandPlan::CreateView { .. } => Ok(Some("CREATE VIEW")),
76        CommandPlan::CreateMaterializedView { .. } => Ok(Some("CREATE MATERIALIZED VIEW")),
77        CommandPlan::RefreshMaterializedView { .. } => Ok(Some("REFRESH MATERIALIZED VIEW")),
78        CommandPlan::CreateSchema { .. } => Ok(Some("CREATE SCHEMA")),
79        CommandPlan::AlterSchemaOwner { .. } => Ok(Some("ALTER SCHEMA")),
80        CommandPlan::Analyze { .. } => Ok(None),
81        // VACUUM's transaction-block prohibition has precedence over read-only validation and is enforced by its executor.
82        CommandPlan::Vacuum(_) => Ok(None),
83        CommandPlan::Truncate { .. } => Ok(Some("TRUNCATE")),
84        CommandPlan::CreateSequence(_) => Ok(Some("CREATE SEQUENCE")),
85        CommandPlan::CreateDomain(_) => Ok(Some("CREATE DOMAIN")),
86        CommandPlan::AlterSequence(_) => Ok(Some("ALTER SEQUENCE")),
87        CommandPlan::CreateTableAs { .. } => Ok(Some("CREATE TABLE AS")),
88        CommandPlan::DeclareCursor { query, .. } => {
89            query_may_write_database(context, query).map(|mutates| mutates.then_some("SELECT"))
90        }
91        CommandPlan::Execute { name, .. } => context
92            .catalog
93            .lookup_prepared(name)
94            .map_or(Ok(None), |prepared| forbidden_command(context, &prepared)),
95        CommandPlan::Explain {
96            analyze: true,
97            body,
98            ..
99        } => forbidden_command(context, body),
100        CommandPlan::CreateForeignServer(_) => Ok(Some("CREATE SERVER")),
101        CommandPlan::CreateForeignTable(_) | CommandPlan::CreateForeignTableIfNotExists(_) => {
102            Ok(Some("CREATE FOREIGN TABLE"))
103        }
104        CommandPlan::CreateFunction(function) => Ok(Some(if function.is_procedure {
105            "CREATE PROCEDURE"
106        } else {
107            "CREATE FUNCTION"
108        })),
109        CommandPlan::DropFunction(function) => Ok(Some(if function.is_procedure {
110            "DROP PROCEDURE"
111        } else {
112            "DROP FUNCTION"
113        })),
114        CommandPlan::AlterRoutine(_) => Ok(Some("ALTER ROUTINE")),
115        CommandPlan::AlterRoutineOwner(_) => Ok(Some("ALTER ROUTINE")),
116        CommandPlan::RenameRoutine(_) => Ok(Some("ALTER ROUTINE")),
117        CommandPlan::GrantRoutine(_) => Ok(Some("GRANT ON ROUTINE")),
118        CommandPlan::GrantTable(_) => Ok(Some("GRANT ON TABLE")),
119        CommandPlan::GrantSequence(_) => Ok(Some("GRANT ON SEQUENCE")),
120        CommandPlan::GrantDatabase(_) => Ok(Some("GRANT ON DATABASE")),
121        CommandPlan::GrantSchema(_) => Ok(Some("GRANT ON SCHEMA")),
122        CommandPlan::GrantRole(_) => Ok(Some("GRANT ROLE")),
123        CommandPlan::CreateRole(_) => Ok(Some("CREATE ROLE")),
124        CommandPlan::AlterRole(_) => Ok(Some("ALTER ROLE")),
125        CommandPlan::DropRole(_) => Ok(Some("DROP ROLE")),
126        CommandPlan::CreateTrigger(_) => Ok(Some("CREATE TRIGGER")),
127        CommandPlan::DropTrigger(_) => Ok(Some("DROP TRIGGER")),
128        CommandPlan::CreateRule(_) => Ok(Some("CREATE RULE")),
129        CommandPlan::DropRule(_) => Ok(Some("DROP RULE")),
130        CommandPlan::SetVariable { .. }
131        | CommandPlan::Notify { .. }
132        | CommandPlan::Listen { .. }
133        | CommandPlan::Unlisten { .. }
134        | CommandPlan::ResetVariable { .. }
135        | CommandPlan::ResetAllVariables
136        | CommandPlan::SetConstraints { .. }
137        | CommandPlan::ShowVariable { .. }
138        | CommandPlan::Discard { .. }
139        | CommandPlan::Load { .. }
140        | CommandPlan::Transaction(_)
141        | CommandPlan::FetchCursor(_)
142        | CommandPlan::CloseCursor { .. }
143        | CommandPlan::Prepare { .. }
144        | CommandPlan::Deallocate { .. }
145        | CommandPlan::Explain { analyze: false, .. }
146        | CommandPlan::DoBlock { .. }
147        | CommandPlan::Call { .. } => Ok(None),
148    }
149}
150
151pub fn plan_sets_transaction_snapshot(plan: &UnifiedPlan) -> bool {
152    !matches!(
153        plan,
154        UnifiedPlan::Command(command)
155            if matches!(
156                command.as_ref(),
157                CommandPlan::SetVariable { .. }
158                    | CommandPlan::Notify { .. }
159                    | CommandPlan::Listen { .. }
160                    | CommandPlan::Unlisten { .. }
161                    | CommandPlan::ResetVariable { .. }
162                    | CommandPlan::ResetAllVariables
163                    | CommandPlan::SetConstraints { .. }
164                    | CommandPlan::ShowVariable { .. }
165                    | CommandPlan::Transaction(_)
166                    | CommandPlan::FetchCursor(_)
167                    | CommandPlan::CloseCursor { .. }
168                    | CommandPlan::Deallocate { .. }
169                    | CommandPlan::Load { .. }
170                    | CommandPlan::Discard { .. }
171            )
172    )
173}