diff --git a/apple/Packages/TableProCore/Sources/TableProSQLGrammar/SQLStateChangingFunctions.swift b/apple/Packages/TableProCore/Sources/TableProSQLGrammar/SQLStateChangingFunctions.swift index 675acc1900..36c8b13242 100644 --- a/apple/Packages/TableProCore/Sources/TableProSQLGrammar/SQLStateChangingFunctions.swift +++ b/apple/Packages/TableProCore/Sources/TableProSQLGrammar/SQLStateChangingFunctions.swift @@ -9,6 +9,10 @@ public enum SQLStateChangingFunctions { return names.contains(lowered) } + static func isDuckDBCall(_ name: String) -> Bool { + duckDBCalls.contains(name.uppercased().lowercased()) + } + // SQLite runs a table-valued pragma named as a table source too, so `SELECT * FROM main.pragma_optimize` runs // ANALYZE. A call is the call scanner's; a column or alias of that name runs nothing. static func namesStateChangingTable(in text: NSString, tokens: [TSQLTokens.Token], grammar: SQLLexicalGrammar) -> Bool { @@ -72,21 +76,29 @@ public enum SQLStateChangingFunctions { .union(catalogAndStorage) .union(indexMaintenance) .union(sqlTextRunners) + .union(scheduledJobs) + .union(hypertablesAndChunks) + .union(rolesAndGrants) static let serverSignals: Set = [ "pg_cancel_backend", "pg_terminate_backend", "pg_reload_conf", "pg_rotate_logfile", "pg_log_backend_memory_contexts" ] + // dblink_close commits the remote transaction dblink_open began, with whatever dblink_exec ran in it, and + // pg_background's cancels stop a worker before it commits. static let sessionControl: Set = [ "system$abort_session", "system$cancel_query", "system$cancel_all_queries", "system$abort_transaction", - "abortsessions" + "abortsessions", "dblink_cancel_query", "dblink_close", "pg_background_cancel", "pg_background_cancel_v2", + "pg_background_cancel_all", "pg_background_cancel_all_v2", "pg_background_cancel_by_label", + "pg_background_cancel_by_label_v2", "pg_background_cancel_v2_grace" ] static let recoveryAndBackup: Set = [ "pg_promote", "pg_wal_replay_pause", "pg_wal_replay_resume", "pg_backup_start", "pg_backup_stop", "pg_switch_wal", "pg_create_restore_point", "pg_log_standby_snapshot", "pg_start_backup", "pg_stop_backup", - "pg_switch_xlog", "pg_xlog_replay_pause", "pg_xlog_replay_resume" + "pg_switch_xlog", "pg_xlog_replay_pause", "pg_xlog_replay_resume", "timescaledb_pre_restore", + "timescaledb_post_restore" ] static let replication: Set = [ @@ -138,10 +150,48 @@ public enum SQLStateChangingFunctions { // SQL Server's OPENQUERY and OPENROWSET hand their last argument to a linked server, or back to this one through // a loopback link, as a batch of its own: a SELECT from either can carry a DELETE, as a SELECT from dblink can. - // crosstab, connectby and xpath_table paste their text arguments into the SQL they run. + // crosstab, connectby and xpath_table paste their text arguments into the SQL they run. pg_background runs its + // text in a worker that commits on its own, and pg_cron's schedule, schedule_in_database and alter_job store a + // command its worker runs later; TimescaleDB's alter_job shares the name. static let sqlTextRunners: Set = [ "query_to_xml", "query_to_xml_and_xmlschema", "ts_stat", "ts_rewrite", "dblink", "dblink_exec", "dblink_send_query", "dblink_open", "openquery", "openrowset", "crosstab", "crosstab2", "crosstab3", "crosstab4", - "connectby", "xpath_table" + "connectby", "xpath_table", "pg_background_launch", "pg_background_launch_v2", "pg_background_submit", + "pg_background_submit_v2", "pg_background_run", "pg_background_run_v2", "pg_background_run_query", + "pg_background_run_query_v2", "schedule", "schedule_in_database", "alter_job", "subscription_exec" + ] + + // pg_cron's unschedule, and TimescaleDB's jobs and the policies that add or remove one. + static let scheduledJobs: Set = [ + "unschedule", "add_job", "delete_job", "add_retention_policy", "remove_retention_policy", "add_reorder_policy", + "remove_reorder_policy", "add_compaction_policy", "remove_compaction_policy", "add_compression_policy", + "remove_compression_policy", "add_continuous_aggregate_policy", "remove_continuous_aggregate_policy", + "add_policies", "alter_policies", "remove_policies", "remove_all_policies" + ] + + // TimescaleDB's functions that change a hypertable, or drop, move or rewrite its chunks. The data node ones are + // gone since 2.14, and set_adaptive_chunking since 2.28, but older servers still have them. + static let hypertablesAndChunks: Set = [ + "create_hypertable", "add_dimension", "set_chunk_time_interval", "set_partitioning_interval", + "set_number_partitions", "set_integer_now_func", "set_adaptive_chunking", "enable_chunk_skipping", + "disable_chunk_skipping", "attach_tablespace", "detach_tablespace", "detach_tablespaces", "drop_chunks", + "drop_chunk", "create_chunk", "compress_chunk", "decompress_chunk", "reorder_chunk", "move_chunk", + "block_new_chunks", "allow_new_chunks", "create_distributed_hypertable", "add_data_node", "attach_data_node", + "detach_data_node", "delete_data_node", "alter_data_node", "set_replication_factor", + "create_distributed_restore_point" + ] + + static let rolesAndGrants: Set = [ + "pg_background_grant_privileges", "pg_background_revoke_privileges", "grant_pg_background_privileges", + "revoke_pg_background_privileges", "pg_background_drop_executor_role" + ] + + // DuckDB's query() and json_execute_serialized_sql() run the SELECT in their text, which can take a value from a + // sequence; the second does so outside a read-only transaction (both measured on 2.0.0). The postgres, mysql and + // sqlite extensions' query and execute functions run any statement on an attached database, and dbgen and dsdgen + // create and fill tables. Elsewhere `query` is any function's name. + static let duckDBCalls: Set = [ + "query", "json_execute_serialized_sql", "postgres_query", "postgres_execute", "mysql_query", "mysql_execute", + "sqlite_query", "dbgen", "dsdgen" ] } diff --git a/apple/Packages/TableProCore/Sources/TableProSQLGrammar/SQLStatementClassifier+Surfaces.swift b/apple/Packages/TableProCore/Sources/TableProSQLGrammar/SQLStatementClassifier+Surfaces.swift index f04e7edceb..dbb2cd9348 100644 --- a/apple/Packages/TableProCore/Sources/TableProSQLGrammar/SQLStatementClassifier+Surfaces.swift +++ b/apple/Packages/TableProCore/Sources/TableProSQLGrammar/SQLStatementClassifier+Surfaces.swift @@ -158,17 +158,6 @@ extension SQLStatementTiering { } } - // DuckDB's query() and json_execute_serialized_sql() run the SELECT in their text, which can take a value from a - // sequence; the second does so outside a read-only transaction (both measured on 2.0.0). The postgres, mysql and - // sqlite extensions' query and execute functions run any statement on an attached database, and dbgen and dsdgen - // create and fill tables. - static let stateChangingCalls: [String: Set] = [ - duckDB: [ - "QUERY", "JSON_EXECUTE_SERIALIZED_SQL", "POSTGRES_QUERY", "POSTGRES_EXECUTE", "MYSQL_QUERY", "MYSQL_EXECUTE", - "SQLITE_QUERY", "DBGEN", "DSDGEN", - ], - ] - // An EXPLAIN that runs its statement is tiered by that statement's own calls. func stateChangingCallTier( _ code: StatementCode, @@ -177,10 +166,10 @@ extension SQLStatementTiering { ) -> StatementTier { guard !SQLExplainStatement.keywords.contains(code.keyword) else { return .safe } if advancesSequence(code) { return .write } - let engineCalls = Self.stateChangingCalls[databaseTypeId] ?? [] let changesState = calls.contains { call in guard case .named(let name, _) = call.callee else { return true } - return SQLStateChangingFunctions.contains(name) || engineCalls.contains(name.uppercased()) + return SQLStateChangingFunctions.contains(name) + || (databaseTypeId == Self.duckDB && SQLStateChangingFunctions.isDuckDBCall(name)) } guard !changesState else { return .write } let namesTable = SQLStateChangingFunctions.namesStateChangingTable( diff --git a/apple/Packages/TableProCore/Tests/TableProSQLGrammarTests/SQLStateChangingFunctionsTests.swift b/apple/Packages/TableProCore/Tests/TableProSQLGrammarTests/SQLStateChangingFunctionsTests.swift index a1eb4e5980..bfc55a7d00 100644 --- a/apple/Packages/TableProCore/Tests/TableProSQLGrammarTests/SQLStateChangingFunctionsTests.swift +++ b/apple/Packages/TableProCore/Tests/TableProSQLGrammarTests/SQLStateChangingFunctionsTests.swift @@ -25,7 +25,11 @@ struct SQLStateChangingFunctionsTests { "files_and_extensions": SQLStateChangingFunctions.filesAndExtensions, "catalog_and_storage": SQLStateChangingFunctions.catalogAndStorage, "index_maintenance": SQLStateChangingFunctions.indexMaintenance, - "sql_text_runners": SQLStateChangingFunctions.sqlTextRunners + "sql_text_runners": SQLStateChangingFunctions.sqlTextRunners, + "scheduled_jobs": SQLStateChangingFunctions.scheduledJobs, + "hypertables_and_chunks": SQLStateChangingFunctions.hypertablesAndChunks, + "roles_and_grants": SQLStateChangingFunctions.rolesAndGrants, + "duckdb_calls": SQLStateChangingFunctions.duckDBCalls ] @Test("Each family lists the names the Linux app lists") @@ -33,9 +37,9 @@ struct SQLStateChangingFunctionsTests { let data = try Data(contentsOf: Self.sharedNames) let shared = try JSONDecoder().decode([String: [String]].self, from: data) let sharedFamilies = shared.mapValues { Set($0) } - let everyName = Set(shared.values.joined()) + let everyEngine = Set(shared.filter { $0.key != "duckdb_calls" }.values.joined()) #expect(Self.families == sharedFamilies) - #expect(SQLStateChangingFunctions.names == everyName) + #expect(SQLStateChangingFunctions.names == everyEngine) } @Test("A listed name matches in any case, under sys_, and with a letter MariaDB folds", arguments: [ diff --git a/apple/Packages/TableProCore/Tests/TableProSQLGrammarTests/SQLStatementClassifierTests.swift b/apple/Packages/TableProCore/Tests/TableProSQLGrammarTests/SQLStatementClassifierTests.swift index ef1d4fcb44..215e428eef 100644 --- a/apple/Packages/TableProCore/Tests/TableProSQLGrammarTests/SQLStatementClassifierTests.swift +++ b/apple/Packages/TableProCore/Tests/TableProSQLGrammarTests/SQLStatementClassifierTests.swift @@ -147,6 +147,39 @@ struct SQLStatementClassifierTests { #expect(Self.tier(sql, engine) == .safe) } + @Test("An extension function that runs SQL, changes a job or a chunk, or stops a worker is a write", arguments: [ + "SELECT pg_background_launch('SELECT 1')", + "SELECT * FROM pg_background_run_query_v2('SELECT 1') AS r(n int)", + "SELECT pg_background_cancel_all()", + "SELECT pg_background_grant_privileges('app')", + "SELECT cron.schedule('0 3 * * *', 'VACUUM orders')", + "SELECT cron.unschedule('nightly')", + "SELECT drop_chunks('conditions', INTERVAL '3 months')", + "SELECT compress_chunk(c) FROM show_chunks('conditions') c", + "SELECT create_hypertable('conditions', by_range('time'))", + "SELECT add_retention_policy('conditions', INTERVAL '1 year')", + "SELECT alter_job(1000, schedule_interval => INTERVAL '1 day')", + "SELECT timescaledb_experimental.remove_all_policies('conditions')", + "SELECT timescaledb_pre_restore()", + "SELECT dblink_close('app', 'c')", + "SELECT dblink_cancel_query('app')", + ]) + func extensionFunctionWrites(sql: String) { + #expect(Self.tier(sql, "PostgreSQL") == .write) + } + + @Test("An extension function that only reads stays a read", arguments: [ + "SELECT * FROM pg_background_result(123) AS r(n int)", + "SELECT pg_background_detach(123)", + "SELECT show_chunks('conditions')", + "SELECT * FROM cron.job", + "SELECT dblink_connect('app', 'dbname=shop')", + "SELECT schedule FROM jobs", + ]) + func extensionReadStaysSafe(sql: String) { + #expect(Self.tier(sql, "PostgreSQL") == .safe) + } + @Test("SurrealQL's REMOVE drops what it names, wherever the statement stands", arguments: [ "REMOVE TABLE person", "remove table if exists person;", diff --git a/apple/changelog.d/4043.fixed.md b/apple/changelog.d/4043.fixed.md new file mode 100644 index 0000000000..2a341116ed --- /dev/null +++ b/apple/changelog.d/4043.fixed.md @@ -0,0 +1 @@ +Read-Only and Safe Mode treated pg_background, pg_cron and TimescaleDB calls, drop_chunks included, as reads. diff --git a/core/crates/core/src/sql_syntax/state_changing_functions.rs b/core/crates/core/src/sql_syntax/state_changing_functions.rs index faad16f1e6..e4900adb35 100644 --- a/core/crates/core/src/sql_syntax/state_changing_functions.rs +++ b/core/crates/core/src/sql_syntax/state_changing_functions.rs @@ -1,7 +1,9 @@ -//! Built-in functions that change state from inside a SELECT, by family. +//! Functions, built in or from a common extension, that change state +//! from inside a SELECT, by family, matched by name whatever schema +//! qualifies the call. //! -//! A read-only transaction stops none of them on PostgreSQL 15 and -//! earlier: they signal other sessions, take locks, run SQL text, or +//! A read-only transaction stops none of the built-ins on PostgreSQL 15 +//! and earlier: they signal other sessions, take locks, run SQL text, or //! write large objects, files or catalog rows. A function the user wrote //! is beyond what the words show. The Apple apps list the same families, //! and `tests/fixtures/state_changing_functions.json` holds them for both @@ -23,6 +25,17 @@ const SESSION_CONTROL: &[&str] = &[ "system$cancel_all_queries", "system$abort_transaction", "abortsessions", + // dblink_close commits the remote transaction dblink_open began, with + // whatever dblink_exec ran in it. + "dblink_cancel_query", + "dblink_close", + "pg_background_cancel", + "pg_background_cancel_v2", + "pg_background_cancel_all", + "pg_background_cancel_all_v2", + "pg_background_cancel_by_label", + "pg_background_cancel_by_label_v2", + "pg_background_cancel_v2_grace", ]; const RECOVERY_AND_BACKUP: &[&str] = &[ @@ -39,6 +52,8 @@ const RECOVERY_AND_BACKUP: &[&str] = &[ "pg_switch_xlog", "pg_xlog_replay_pause", "pg_xlog_replay_resume", + "timescaledb_pre_restore", + "timescaledb_post_restore", ]; const REPLICATION: &[&str] = &[ @@ -138,7 +153,8 @@ const INDEX_MAINTENANCE: &[&str] = &[ /// Functions that run SQL text: what that text does is not in the words. /// crosstab, connectby and xpath_table paste their text arguments into -/// the SQL they run. +/// the SQL they run. pg_background runs its text in a worker that +/// commits on its own, and pg_cron's worker runs a stored command later. const SQL_TEXT_RUNNERS: &[&str] = &[ "query_to_xml", "query_to_xml_and_xmlschema", @@ -156,9 +172,104 @@ const SQL_TEXT_RUNNERS: &[&str] = &[ "crosstab4", "connectby", "xpath_table", + "pg_background_launch", + "pg_background_launch_v2", + "pg_background_submit", + "pg_background_submit_v2", + "pg_background_run", + "pg_background_run_v2", + "pg_background_run_query", + "pg_background_run_query_v2", + // TimescaleDB's alter_job shares the name. + "schedule", + "schedule_in_database", + "alter_job", + "subscription_exec", ]; -const FAMILIES: [(&str, &[&str]); 12] = [ +/// pg_cron's unschedule, and TimescaleDB's jobs and the policies that add +/// or remove one. +const SCHEDULED_JOBS: &[&str] = &[ + "unschedule", + "add_job", + "delete_job", + "add_retention_policy", + "remove_retention_policy", + "add_reorder_policy", + "remove_reorder_policy", + "add_compaction_policy", + "remove_compaction_policy", + "add_compression_policy", + "remove_compression_policy", + "add_continuous_aggregate_policy", + "remove_continuous_aggregate_policy", + "add_policies", + "alter_policies", + "remove_policies", + "remove_all_policies", +]; + +/// TimescaleDB's functions that change a hypertable, or drop, move or +/// rewrite its chunks. The data node ones are gone since 2.14, and +/// set_adaptive_chunking since 2.28, but older servers still have them. +const HYPERTABLES_AND_CHUNKS: &[&str] = &[ + "create_hypertable", + "add_dimension", + "set_chunk_time_interval", + "set_partitioning_interval", + "set_number_partitions", + "set_integer_now_func", + "set_adaptive_chunking", + "enable_chunk_skipping", + "disable_chunk_skipping", + "attach_tablespace", + "detach_tablespace", + "detach_tablespaces", + "drop_chunks", + "drop_chunk", + "create_chunk", + "compress_chunk", + "decompress_chunk", + "reorder_chunk", + "move_chunk", + "block_new_chunks", + "allow_new_chunks", + "create_distributed_hypertable", + "add_data_node", + "attach_data_node", + "detach_data_node", + "delete_data_node", + "alter_data_node", + "set_replication_factor", + "create_distributed_restore_point", +]; + +const ROLES_AND_GRANTS: &[&str] = &[ + "pg_background_grant_privileges", + "pg_background_revoke_privileges", + "grant_pg_background_privileges", + "revoke_pg_background_privileges", + "pg_background_drop_executor_role", +]; + +/// DuckDB's own calls. `query` and `json_execute_serialized_sql` run the +/// SELECT in their text, the postgres, mysql and sqlite extensions' +/// query and execute functions run any statement on an attached +/// database, and dbgen and dsdgen create and fill tables. Elsewhere +/// `query` is any function's name. +const DUCKDB_CALLS: &[&str] = &[ + "query", + "json_execute_serialized_sql", + "postgres_query", + "postgres_execute", + "mysql_query", + "mysql_execute", + "sqlite_query", + "dbgen", + "dsdgen", +]; + +const FAMILIES: [(&str, &[&str]); 15] = [ ("server_signals", SERVER_SIGNALS), ("session_control", SESSION_CONTROL), ("recovery_and_backup", RECOVERY_AND_BACKUP), @@ -171,20 +282,28 @@ const FAMILIES: [(&str, &[&str]); 12] = [ ("catalog_and_storage", CATALOG_AND_STORAGE), ("index_maintenance", INDEX_MAINTENANCE), ("sql_text_runners", SQL_TEXT_RUNNERS), + ("scheduled_jobs", SCHEDULED_JOBS), + ("hypertables_and_chunks", HYPERTABLES_AND_CHUNKS), + ("roles_and_grants", ROLES_AND_GRANTS), ]; /// Whether a call of `name` can change state, compared without regard /// to case. MariaDB looks `releaſe_lock` up as `RELEASE_LOCK`, so a /// name is compared upper-cased. pub(crate) fn changes_state(name: &str) -> bool { + FAMILIES.iter().any(|(_, names)| lists(names, name)) +} + +pub(crate) fn duckdb_call(name: &str) -> bool { + lists(DUCKDB_CALLS, name) +} + +fn lists(names: &[&str], name: &str) -> bool { let folded = match name.is_ascii() { true => Cow::Borrowed(name), false => Cow::Owned(name.to_uppercase()), }; - FAMILIES - .iter() - .flat_map(|(_, names)| names.iter()) - .any(|known| folded.eq_ignore_ascii_case(known)) + names.iter().any(|known| folded.eq_ignore_ascii_case(known)) } #[cfg(test)] @@ -216,9 +335,22 @@ mod tests { "pragma_optimize", "relea\u{17f}e_lock", "RELEA\u{17f}E_ALL_LOCK\u{17f}", + "pg_background_launch", + "SCHEDULE", + "unschedule", + "drop_chunks", + "create_hypertable", + "dblink_close", + "pg_background_grant_privileges", + "timescaledb_pre_restore", + "pg_background_submit_v2", + "alter_job", ] { assert!(changes_state(name), "{name}"); } + for name in ["QUERY", "postgres_execute", "sqlite_query", "dbgen"] { + assert!(duckdb_call(name) && !changes_state(name), "{name}"); + } for name in [ "currval", "pg_backend_pid", @@ -227,6 +359,11 @@ mod tests { "dblink_build_sql_insert", "query_to_xmlschema", "query", + "pg_background_result", + "pg_background_detach", + "show_chunks", + "dblink_connect", + "postgres_scan", ] { assert!(!changes_state(name), "{name}"); } @@ -236,6 +373,7 @@ mod tests { fn no_name_is_listed_twice() { let mut names: Vec = FAMILIES .iter() + .chain([("duckdb_calls", DUCKDB_CALLS)].iter()) .flat_map(|(_, family)| family.iter()) .map(|name| name.to_ascii_lowercase()) .collect(); @@ -251,6 +389,7 @@ mod tests { serde_json::from_str(include_str!("../../tests/fixtures/state_changing_functions.json")).unwrap(); let listed: BTreeMap> = FAMILIES .iter() + .chain([("duckdb_calls", DUCKDB_CALLS)].iter()) .map(|(family, names)| { let mut names: Vec = names.iter().map(|name| name.to_string()).collect(); names.sort(); diff --git a/core/crates/core/src/sql_syntax/statement_access.rs b/core/crates/core/src/sql_syntax/statement_access.rs index 9e196a0603..f44ad74d5f 100644 --- a/core/crates/core/src/sql_syntax/statement_access.rs +++ b/core/crates/core/src/sql_syntax/statement_access.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use crate::sql_syntax::SqlGrammar; use crate::sql_syntax::script::{ScriptPlan, ScriptToken, ScriptTokenKind}; -use crate::sql_syntax::state_changing_functions::changes_state; +use crate::sql_syntax::state_changing_functions::{changes_state, duckdb_call}; use crate::unicode_blank::is_blank; /// What running a piece of SQL can do, from its own words. Ordered, so @@ -412,16 +412,14 @@ fn names_pragma_optimize(token: &Code<'_>, grammar: SqlGrammar) -> bool { fn calls_mutator(part: &[Code<'_>], index: usize, grammar: SqlGrammar) -> bool { let arguments = match decodes_unicode_names(grammar) && spelled_in_escapes(part, index) { true => after_uescape(part, index + 1), - false if changes_state(part[index].text) || runs_query_text(&part[index], grammar) => index + 1, + false if changes_state(part[index].text) || runs_duckdb_call(&part[index], grammar) => index + 1, false => return false, }; part.get(arguments).is_some_and(|next| next.kind == Kind::Open) } -/// DuckDB's `query('…')` runs the SELECT in its text, which can take a -/// value from a sequence. Elsewhere `query` is any function's name. -fn runs_query_text(token: &Code<'_>, grammar: SqlGrammar) -> bool { - grammar == SqlGrammar::DuckDb && is(token.text, &["QUERY"]) +fn runs_duckdb_call(token: &Code<'_>, grammar: SqlGrammar) -> bool { + grammar == SqlGrammar::DuckDb && duckdb_call(token.text) } /// The grammars whose scanner is PostgreSQL's, which reads `U&"…"`. @@ -1506,6 +1504,21 @@ mod tests { "SELECT pg_import_system_collations('public'::regnamespace)", "SELECT heap_force_kill('orders'::regclass, ARRAY['(0,1)']::tid[])", "SELECT pg_file_write('postgresql.auto.conf', 'x', true)", + "SELECT pg_background_launch('SELECT 1')", + "SELECT * FROM pg_background_run_query_v2('SELECT 1') AS r(n int)", + "SELECT pg_background_cancel_all()", + "SELECT pg_background_grant_privileges('app')", + "SELECT cron.schedule('0 3 * * *', 'VACUUM orders')", + "SELECT cron.unschedule('nightly')", + "SELECT drop_chunks('conditions', INTERVAL '3 months')", + "SELECT compress_chunk(c) FROM show_chunks('conditions') c", + "SELECT create_hypertable('conditions', by_range('time'))", + "SELECT add_retention_policy('conditions', INTERVAL '1 year')", + "SELECT alter_job(1000, schedule_interval => INTERVAL '1 day')", + "SELECT timescaledb_experimental.remove_all_policies('conditions')", + "SELECT timescaledb_pre_restore()", + "SELECT dblink_close('app', 'c')", + "SELECT dblink_cancel_query('app')", ] { assert_eq!(access(sql, SqlGrammar::PostgreSql), StatementAccess::Write, "{sql}"); } @@ -1517,17 +1530,40 @@ mod tests { ] { assert_eq!(access(sql, SqlGrammar::MySql), StatementAccess::Write, "{sql}"); } - assert_eq!( - access("SELECT * FROM query('SELECT nextval(''s'')')", SqlGrammar::DuckDb), - StatementAccess::Write - ); + for sql in [ + "SELECT * FROM query('SELECT nextval(''s'')')", + "SELECT * FROM postgres_execute('pg', 'SELECT 1')", + "SELECT * FROM postgres_query('pg', 'SELECT nextval(''s'')')", + "SELECT * FROM mysql_query('my', 'SELECT 1')", + "CALL mysql_execute('my', 'UPDATE orders SET total = 0 WHERE id = 1')", + "SELECT * FROM sqlite_query('lite', 'SELECT 1')", + "SELECT * FROM json_execute_serialized_sql(json_serialize_sql('SELECT nextval(''s'')'))", + "SELECT * FROM dbgen(sf = 0.01)", + "SELECT * FROM DSDGEN(sf = 0.01)", + ] { + assert_eq!(access(sql, SqlGrammar::DuckDb), StatementAccess::Write, "{sql}"); + } for sql in [ "SELECT dblink_build_sql_insert('t', '1', 1, '{1}', '{2}')", "SELECT dblink FROM links", "SELECT file, url, query(1) FROM downloads", + "SELECT * FROM postgres_execute('pg', 'SELECT 1')", + "SELECT * FROM pg_background_result(123) AS r(n int)", + "SELECT pg_background_detach(123)", + "SELECT show_chunks('conditions')", + "SELECT * FROM cron.job", + "SELECT dblink_connect('app', 'dbname=shop')", + "SELECT schedule FROM jobs", ] { assert_eq!(access(sql, SqlGrammar::PostgreSql), StatementAccess::Read, "{sql}"); } + assert_eq!( + access( + "SELECT * FROM postgres_scan('pg', 'public', 'orders')", + SqlGrammar::DuckDb + ), + StatementAccess::Read + ); } #[test] diff --git a/core/crates/core/tests/fixtures/state_changing_functions.json b/core/crates/core/tests/fixtures/state_changing_functions.json index a70e412c56..b47060305f 100644 --- a/core/crates/core/tests/fixtures/state_changing_functions.json +++ b/core/crates/core/tests/fixtures/state_changing_functions.json @@ -21,6 +21,17 @@ "pg_import_system_collations", "pg_truncate_visibility_map" ], + "duckdb_calls": [ + "dbgen", + "dsdgen", + "json_execute_serialized_sql", + "mysql_execute", + "mysql_query", + "postgres_execute", + "postgres_query", + "query", + "sqlite_query" + ], "files_and_extensions": [ "lo_export", "lo_import", @@ -30,6 +41,37 @@ "pg_file_write", "writefile" ], + "hypertables_and_chunks": [ + "add_data_node", + "add_dimension", + "allow_new_chunks", + "alter_data_node", + "attach_data_node", + "attach_tablespace", + "block_new_chunks", + "compress_chunk", + "create_chunk", + "create_distributed_hypertable", + "create_distributed_restore_point", + "create_hypertable", + "decompress_chunk", + "delete_data_node", + "detach_data_node", + "detach_tablespace", + "detach_tablespaces", + "disable_chunk_skipping", + "drop_chunk", + "drop_chunks", + "enable_chunk_skipping", + "move_chunk", + "reorder_chunk", + "set_adaptive_chunking", + "set_chunk_time_interval", + "set_integer_now_func", + "set_number_partitions", + "set_partitioning_interval", + "set_replication_factor" + ], "index_maintenance": [ "brin_desummarize_range", "brin_summarize_new_values", @@ -60,7 +102,9 @@ "pg_wal_replay_pause", "pg_wal_replay_resume", "pg_xlog_replay_pause", - "pg_xlog_replay_resume" + "pg_xlog_replay_resume", + "timescaledb_post_restore", + "timescaledb_pre_restore" ], "replication": [ "pg_copy_logical_replication_slot", @@ -81,6 +125,32 @@ "pg_replication_slot_advance", "pg_sync_replication_slots" ], + "roles_and_grants": [ + "grant_pg_background_privileges", + "pg_background_drop_executor_role", + "pg_background_grant_privileges", + "pg_background_revoke_privileges", + "revoke_pg_background_privileges" + ], + "scheduled_jobs": [ + "add_compaction_policy", + "add_compression_policy", + "add_continuous_aggregate_policy", + "add_job", + "add_policies", + "add_reorder_policy", + "add_retention_policy", + "alter_policies", + "delete_job", + "remove_all_policies", + "remove_compaction_policy", + "remove_compression_policy", + "remove_continuous_aggregate_policy", + "remove_policies", + "remove_reorder_policy", + "remove_retention_policy", + "unschedule" + ], "sequences": [ "nextval", "setval" @@ -94,6 +164,15 @@ ], "session_control": [ "abortsessions", + "dblink_cancel_query", + "dblink_close", + "pg_background_cancel", + "pg_background_cancel_all", + "pg_background_cancel_all_v2", + "pg_background_cancel_by_label", + "pg_background_cancel_by_label_v2", + "pg_background_cancel_v2", + "pg_background_cancel_v2_grace", "system$abort_session", "system$abort_transaction", "system$cancel_all_queries", @@ -117,6 +196,7 @@ "set_config" ], "sql_text_runners": [ + "alter_job", "connectby", "crosstab", "crosstab2", @@ -128,8 +208,19 @@ "dblink_send_query", "openquery", "openrowset", + "pg_background_launch", + "pg_background_launch_v2", + "pg_background_run", + "pg_background_run_query", + "pg_background_run_query_v2", + "pg_background_run_v2", + "pg_background_submit", + "pg_background_submit_v2", "query_to_xml", "query_to_xml_and_xmlschema", + "schedule", + "schedule_in_database", + "subscription_exec", "ts_rewrite", "ts_stat", "xpath_table" diff --git a/linux/changelog.d/4043.fixed.md b/linux/changelog.d/4043.fixed.md new file mode 100644 index 0000000000..fef4c1d752 --- /dev/null +++ b/linux/changelog.d/4043.fixed.md @@ -0,0 +1 @@ +Read-Only and Safe Mode treated pg_cron, pg_background, TimescaleDB and DuckDB postgres_execute calls as reads.