uqa_sql/semantics/effects/
read_only.rs1use super::{command_payload_may_write_database, query_may_write_database, QueryEffectContext};
10use crate::{
11 ast::RelationPersistence,
12 plan::{CommandPlan, UnifiedPlan},
13 result::completion::command_tag_name,
14 SQLError,
15};
16
17pub fn read_only_error(command: &str) -> SQLError {
18 SQLError::Routine {
19 sqlstate: "25006".into(),
20 message: format!("cannot execute {command} in a read-only transaction"),
21 }
22}
23
24fn table_is_temporary(
25 context: &QueryEffectContext<'_>,
26 table: &str,
27) -> Result<Option<bool>, SQLError> {
28 context
29 .catalog
30 .table_persistence(table)
31 .map(|persistence| persistence.map(|value| value == RelationPersistence::Temporary))
32 .map_err(|error| SQLError::Internal(format!("resolve read-only target `{table}`: {error}")))
33}
34
35fn dml_command(
36 context: &QueryEffectContext<'_>,
37 table: &str,
38 command: &CommandPlan,
39) -> Result<Option<&'static str>, SQLError> {
40 match table_is_temporary(context, table)? {
41 Some(false) => Ok(Some(command_tag_name(command))),
42 Some(true) if command_payload_may_write_database(context, command)? => {
43 Ok(Some(command_tag_name(command)))
44 }
45 Some(true) | None => Ok(None),
46 }
47}
48
49pub fn forbidden_command(
51 context: &QueryEffectContext<'_>,
52 plan: &UnifiedPlan,
53) -> Result<Option<&'static str>, SQLError> {
54 let UnifiedPlan::Command(command) = plan else {
55 let UnifiedPlan::Query(query) = plan else {
56 unreachable!();
57 };
58 return query_may_write_database(context, query).map(|mutates| mutates.then_some("SELECT"));
59 };
60 let command = command.as_ref();
61 match command {
62 CommandPlan::Insert(insert) => dml_command(context, &insert.table, command),
63 CommandPlan::Update(update) => dml_command(context, &update.table, command),
64 CommandPlan::Delete(delete) => dml_command(context, &delete.table, command),
65 CommandPlan::Merge(merge) => dml_command(context, &merge.target, command),
66 CommandPlan::DeclareCursor { query, .. } => {
67 query_may_write_database(context, query).map(|mutates| mutates.then_some("SELECT"))
68 }
69 CommandPlan::Execute { name, .. } => context
70 .catalog
71 .lookup_prepared(name)
72 .map_or(Ok(None), |prepared| forbidden_command(context, &prepared)),
73 CommandPlan::Explain {
74 analyze: true,
75 body,
76 ..
77 } => forbidden_command(context, body),
78 CommandPlan::Analyze { .. }
80 | CommandPlan::LockTable(_)
81 | CommandPlan::Vacuum(_)
82 | CommandPlan::SetVariable { .. }
83 | CommandPlan::Notify { .. }
84 | CommandPlan::Listen { .. }
85 | CommandPlan::Unlisten { .. }
86 | CommandPlan::ResetVariable { .. }
87 | CommandPlan::ResetAllVariables
88 | CommandPlan::SetConstraints { .. }
89 | CommandPlan::ShowVariable { .. }
90 | CommandPlan::Discard { .. }
91 | CommandPlan::Load { .. }
92 | CommandPlan::Transaction(_)
93 | CommandPlan::FetchCursor(_)
94 | CommandPlan::CloseCursor { .. }
95 | CommandPlan::Prepare { .. }
96 | CommandPlan::Deallocate { .. }
97 | CommandPlan::Explain { analyze: false, .. }
98 | CommandPlan::DoBlock { .. }
99 | CommandPlan::Call { .. } => Ok(None),
100 CommandPlan::CreateTable(_)
101 | CommandPlan::CreateTableIfNotExists(_)
102 | CommandPlan::CreateTableAs { .. }
103 | CommandPlan::CreateIndex(_)
104 | CommandPlan::RenameIndex(_)
105 | CommandPlan::Drop(_)
106 | CommandPlan::AlterTable(_)
107 | CommandPlan::AlterForeignTable(_)
108 | CommandPlan::AlterView(_)
109 | CommandPlan::CreateView { .. }
110 | CommandPlan::CreateMaterializedView { .. }
111 | CommandPlan::RefreshMaterializedView { .. }
112 | CommandPlan::CreateSchema { .. }
113 | CommandPlan::AlterSchemaOwner { .. }
114 | CommandPlan::RenameSchema { .. }
115 | CommandPlan::Truncate { .. }
116 | CommandPlan::CreateSequence(_)
117 | CommandPlan::AlterSequence(_)
118 | CommandPlan::CreateDomain(_)
119 | CommandPlan::AlterDomain(_)
120 | CommandPlan::CreateEnum(_)
121 | CommandPlan::CreateCompositeType(_)
122 | CommandPlan::AlterEnum(_)
123 | CommandPlan::AlterTypeObject(_)
124 | CommandPlan::GrantType(_)
125 | CommandPlan::CreateForeignWrapper(_)
126 | CommandPlan::CreateForeignServer(_)
127 | CommandPlan::CreateForeignTable(_)
128 | CommandPlan::CreateForeignTableDefinition(_)
129 | CommandPlan::CreateFunction(_)
130 | CommandPlan::DropFunction(_)
131 | CommandPlan::AlterRoutine(_)
132 | CommandPlan::AlterRoutineOwner(_)
133 | CommandPlan::RenameRoutine(_)
134 | CommandPlan::GrantRoutine(_)
135 | CommandPlan::GrantTable(_)
136 | CommandPlan::GrantSequence(_)
137 | CommandPlan::GrantDatabase(_)
138 | CommandPlan::GrantSchema(_)
139 | CommandPlan::GrantRole(_)
140 | CommandPlan::CreateRole(_)
141 | CommandPlan::AlterRole(_)
142 | CommandPlan::RenameRole(_)
143 | CommandPlan::DropRole(_)
144 | CommandPlan::CreateTrigger(_)
145 | CommandPlan::DropTrigger(_)
146 | CommandPlan::CreateRule(_)
147 | CommandPlan::DropRule(_) => Ok(Some(command_tag_name(command))),
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::LockTable(_)
167 | CommandPlan::FetchCursor(_)
168 | CommandPlan::CloseCursor { .. }
169 | CommandPlan::Deallocate { .. }
170 | CommandPlan::Load { .. }
171 | CommandPlan::Discard { .. }
172 )
173 )
174}