uqa_sql/semantics/effects/
read_only.rs1use 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 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}