Skip to main content

risingwave_common/session_config/
mod.rs

1// Copyright 2022 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15mod iceberg_query_storage_mode;
16mod locality_backfill_mode;
17mod non_zero64;
18mod opt;
19pub mod parallelism;
20mod query_mode;
21mod search_path;
22pub mod sink_decouple;
23mod statement_timeout;
24mod transaction_isolation_level;
25mod visibility_mode;
26
27use chrono_tz::Tz;
28pub use iceberg_query_storage_mode::IcebergQueryStorageMode;
29use itertools::Itertools;
30pub use locality_backfill_mode::LocalityBackfillMode;
31pub use opt::OptionConfig;
32pub use query_mode::QueryMode;
33use risingwave_common_proc_macro::{ConfigDoc, SessionConfig};
34pub use search_path::{SearchPath, USER_NAME_WILD_CARD};
35use serde::{Deserialize, Serialize};
36pub use statement_timeout::StatementTimeout;
37use thiserror::Error;
38
39use self::non_zero64::ConfigNonZeroU64;
40use crate::config::mutate::TomlTableMutateExt;
41use crate::config::streaming::{CacheRefillPolicy, JoinEncodingType, OverWindowCachePolicy};
42use crate::config::{ConfigMergeError, StreamingConfig, merge_streaming_config_section};
43use crate::hash::VirtualNode;
44use crate::session_config::parallelism::{ConfigBackfillParallelism, ConfigParallelism};
45use crate::session_config::sink_decouple::SinkDecouple;
46use crate::session_config::transaction_isolation_level::IsolationLevel;
47pub use crate::session_config::visibility_mode::VisibilityMode;
48use crate::{PG_VERSION, SERVER_ENCODING, SERVER_VERSION_NUM, STANDARD_CONFORMING_STRINGS};
49
50pub const SESSION_CONFIG_LIST_SEP: &str = ", ";
51
52#[derive(Error, Debug)]
53pub enum SessionConfigError {
54    #[error("Invalid value `{value}` for `{entry}`")]
55    InvalidValue {
56        entry: &'static str,
57        value: String,
58        source: anyhow::Error,
59    },
60
61    #[error("Unrecognized config entry `{0}`")]
62    UnrecognizedEntry(String),
63}
64
65type SessionConfigResult<T> = std::result::Result<T, SessionConfigError>;
66
67const AUTO_LOCALITY_BACKFILL_MIN_SIZE: u64 = 10 * 1024 * 1024 * 1024;
68
69fn default_auto_locality_backfill_min_size() -> u64 {
70    AUTO_LOCALITY_BACKFILL_MIN_SIZE
71}
72
73fn default_legacy_locality_backfill_mode() -> LocalityBackfillMode {
74    LocalityBackfillMode::Always
75}
76
77// NOTE(kwannoel): We declare it separately as a constant,
78// otherwise seems like it can't infer the type of -1 when written inline.
79const DISABLE_BACKFILL_RATE_LIMIT: i32 = -1;
80const DISABLE_SOURCE_RATE_LIMIT: i32 = -1;
81const DISABLE_DML_RATE_LIMIT: i32 = -1;
82const DISABLE_SINK_RATE_LIMIT: i32 = -1;
83
84/// Default to bypass cluster limits iff in debug mode.
85const BYPASS_CLUSTER_LIMITS: bool = cfg!(debug_assertions);
86
87/// This is the Session Config of RisingWave.
88///
89/// All config entries implement `Display` and `FromStr` for getter and setter, to be read and
90/// altered within a session.
91///
92/// Users can change the default value of a configuration entry using `ALTER SYSTEM SET`. To
93/// facilitate this, a `serde` implementation is used as the wire format for retrieving initial
94/// configurations and updates from the meta service. It's important to note that the meta
95/// service stores the overridden value of each configuration entry per row with `Display` in
96/// the meta store, rather than using the `serde` format. However, we still delegate the `serde`
97/// impl of all fields to `Display`/`FromStr` to make it consistent.
98#[serde_with::apply(_ => #[serde_as(as = "serde_with::DisplayFromStr")] )]
99#[serde_with::serde_as]
100#[derive(Clone, Debug, Deserialize, Serialize, SessionConfig, ConfigDoc, PartialEq)]
101pub struct SessionConfig {
102    /// If `RW_IMPLICIT_FLUSH` is on, then every INSERT/UPDATE/DELETE statement will block
103    /// until the entire dataflow is refreshed. In other words, every related table & MV will
104    /// be able to see the write.
105    #[parameter(default = false, alias = "rw_implicit_flush")]
106    implicit_flush: bool,
107
108    /// If `DML_WAIT_PERSISTENCE` is on, then every INSERT/UPDATE/DELETE statement waits until
109    /// the transaction is included in a checkpoint. This is ignored when `IMPLICIT_FLUSH` is on.
110    #[parameter(default = false)]
111    dml_wait_persistence: bool,
112
113    /// If `CREATE_COMPACTION_GROUP_FOR_MV` is on, dedicated compaction groups will be created in
114    /// MV creation.
115    #[parameter(default = false)]
116    create_compaction_group_for_mv: bool,
117
118    /// A temporary config variable to force query running in either local or distributed mode.
119    /// The default value is auto which means let the system decide to run batch queries in local
120    /// or distributed mode automatically.
121    #[parameter(default = QueryMode::default())]
122    query_mode: QueryMode,
123
124    /// For Iceberg engine tables, which storage to use for batch SELECT: Iceberg (columnar) or
125    /// Hummock (row). Only affects batch SELECT on tables with ENGINE = ICEBERG.
126    #[parameter(default = IcebergQueryStorageMode::default())]
127    iceberg_query_storage_mode: IcebergQueryStorageMode,
128
129    /// Sets the number of digits displayed for floating-point values.
130    /// See <https://www.postgresql.org/docs/current/runtime-config-client.html#:~:text=for%20more%20information.-,extra_float_digits,-(integer)>
131    #[parameter(default = 1)]
132    extra_float_digits: i32,
133
134    /// Sets the application name to be reported in statistics and logs.
135    /// See <https://www.postgresql.org/docs/14/runtime-config-logging.html#:~:text=What%20to%20Log-,application_name,-(string)>
136    #[parameter(default = "", flags = "REPORT")]
137    application_name: String,
138
139    /// It is typically set by an application upon connection to the server.
140    /// see <https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-DATESTYLE>
141    #[parameter(default = "", rename = "datestyle")]
142    date_style: String,
143
144    /// Force the use of lookup join instead of hash join when possible for local batch execution.
145    #[parameter(default = true, alias = "rw_batch_enable_lookup_join")]
146    batch_enable_lookup_join: bool,
147
148    /// Enable usage of sortAgg instead of hash agg when order property is satisfied in batch
149    /// execution
150    #[parameter(default = true, alias = "rw_batch_enable_sort_agg")]
151    batch_enable_sort_agg: bool,
152
153    /// Enable distributed DML, so an insert, delete, and update statement can be executed in a distributed way (e.g. running in multiple compute nodes).
154    /// No atomicity guarantee in this mode. Its goal is to gain the best ingestion performance for initial batch ingestion where users always can drop their table when failure happens.
155    #[parameter(default = false, rename = "batch_enable_distributed_dml")]
156    batch_enable_distributed_dml: bool,
157
158    /// Evaluate expression in strict mode for batch queries.
159    /// If set to false, an expression failure will not cause an error but leave a null value
160    /// on the result set.
161    #[parameter(default = true)]
162    batch_expr_strict_mode: bool,
163
164    /// The max gap allowed to transform small range scan into multi point lookup.
165    #[parameter(default = 8)]
166    max_split_range_gap: i32,
167
168    /// Sets the order in which schemas are searched when an object (table, data type, function, etc.)
169    /// is referenced by a simple name with no schema specified.
170    /// See <https://www.postgresql.org/docs/14/runtime-config-client.html#GUC-SEARCH-PATH>
171    #[parameter(default = SearchPath::default())]
172    search_path: SearchPath,
173
174    /// If `VISIBILITY_MODE` is all, we will support querying data without checkpoint.
175    #[parameter(default = VisibilityMode::default())]
176    visibility_mode: VisibilityMode,
177
178    /// See <https://www.postgresql.org/docs/current/transaction-iso.html>
179    #[parameter(default = IsolationLevel::default())]
180    transaction_isolation: IsolationLevel,
181
182    /// Select as of specific epoch.
183    /// Sets the historical epoch for querying data. If 0, querying latest data.
184    #[parameter(default = ConfigNonZeroU64::default())]
185    query_epoch: ConfigNonZeroU64,
186
187    /// Session timezone. Defaults to UTC.
188    #[parameter(default = "UTC", check_hook = check_timezone)]
189    timezone: String,
190
191    /// The execution parallelism for streaming queries, including tables, materialized views,
192    /// indexes, and sinks. Defaults to `default`, which preserves the legacy adaptive
193    /// scheduling behavior during effective resolution.
194    #[parameter(default = ConfigParallelism::Default, flags = "SESSION_INIT")]
195    streaming_parallelism: ConfigParallelism,
196
197    /// Specific parallelism for backfill. Only `default` and a fixed positive integer are
198    /// supported here. Adaptive backfill strategies are deferred to a later change.
199    #[parameter(
200        default = ConfigBackfillParallelism::Default,
201        check_hook = check_streaming_parallelism_for_backfill,
202        flags = "SESSION_INIT"
203    )]
204    streaming_parallelism_for_backfill: ConfigBackfillParallelism,
205
206    /// Specific parallelism for table. Defaults to `default`, which preserves the legacy
207    /// bounded adaptive behavior only when the global parallelism itself remains `default`.
208    /// Otherwise it follows the explicit global parallelism.
209    #[parameter(default = ConfigParallelism::Default, flags = "SESSION_INIT")]
210    streaming_parallelism_for_table: ConfigParallelism,
211
212    /// Specific parallelism for sink. By default, it will fall back to `STREAMING_PARALLELISM`.
213    #[parameter(default = ConfigParallelism::Default, flags = "SESSION_INIT")]
214    streaming_parallelism_for_sink: ConfigParallelism,
215
216    /// Specific parallelism for index. By default, it will fall back to `STREAMING_PARALLELISM`.
217    #[parameter(default = ConfigParallelism::Default, flags = "SESSION_INIT")]
218    streaming_parallelism_for_index: ConfigParallelism,
219
220    /// Specific parallelism for source. Defaults to `default`, which preserves the legacy
221    /// bounded adaptive behavior only when the global parallelism itself remains `default`.
222    /// Otherwise it follows the explicit global parallelism.
223    #[parameter(default = ConfigParallelism::Default, flags = "SESSION_INIT")]
224    streaming_parallelism_for_source: ConfigParallelism,
225
226    /// Specific parallelism for materialized view. By default, it will fall back to `STREAMING_PARALLELISM`.
227    #[parameter(default = ConfigParallelism::Default, flags = "SESSION_INIT")]
228    streaming_parallelism_for_materialized_view: ConfigParallelism,
229
230    /// Enable delta join for streaming queries. Defaults to false.
231    #[parameter(default = false, alias = "rw_streaming_enable_delta_join")]
232    streaming_enable_delta_join: bool,
233
234    /// Enable bushy join for streaming queries. Defaults to true.
235    #[parameter(default = true, alias = "rw_streaming_enable_bushy_join")]
236    streaming_enable_bushy_join: bool,
237
238    /// Force filtering to be done inside the join whenever there's a choice between optimizations.
239    /// Defaults to false.
240    #[parameter(default = false, alias = "rw_streaming_force_filter_inside_join")]
241    streaming_force_filter_inside_join: bool,
242
243    /// Deprecated. Arrangement backfill is always used as the fallback backfill type for new
244    /// streaming jobs, and this setting is ignored.
245    #[parameter(
246        default = true,
247        deprecated = "The session variable STREAMING_USE_ARRANGEMENT_BACKFILL has been deprecated and is ignored. Arrangement backfill is always used as the fallback backfill type for new streaming jobs."
248    )]
249    streaming_use_arrangement_backfill: bool,
250
251    #[parameter(default = true)]
252    streaming_use_snapshot_backfill: bool,
253
254    /// Enable serverless backfill for streaming queries. Defaults to false.
255    #[parameter(default = false)]
256    enable_serverless_backfill: bool,
257
258    /// Allow `jsonb` in stream key
259    #[parameter(default = false, alias = "rw_streaming_allow_jsonb_in_stream_key")]
260    streaming_allow_jsonb_in_stream_key: bool,
261
262    /// Unsafe: allow impure expressions on non-append-only streams without materialization.
263    ///
264    /// This may lead to inconsistent results or panics due to re-evaluation on updates/retracts.
265    #[parameter(default = false)]
266    streaming_unsafe_allow_unmaterialized_impure_expr: bool,
267
268    /// Unsafe: allow an upsert sink to use downstream primary-key columns that are not part of
269    /// the upstream stream key.
270    ///
271    /// This may leave stale rows in the downstream system if a downstream primary-key column
272    /// changes without the upsert stream providing its old value.
273    #[parameter(default = false)]
274    streaming_unsafe_allow_upsert_sink_pk_mismatch: bool,
275
276    /// Separate consecutive `StreamHashJoin` by no-shuffle `StreamExchange`
277    #[parameter(default = false)]
278    streaming_separate_consecutive_join: bool,
279
280    /// Separate `StreamSink` by no-shuffle `StreamExchange`
281    #[parameter(default = false)]
282    streaming_separate_sink: bool,
283
284    /// Determine which encoding will be used to encode join rows in operator cache.
285    ///
286    /// This overrides the corresponding entry from the `[streaming.developer]` section in the config file,
287    /// taking effect for new streaming jobs created in the current session.
288    #[parameter(default = None)]
289    streaming_join_encoding: OptionConfig<JoinEncodingType>,
290
291    /// Enable join ordering for streaming and batch queries. Defaults to true.
292    #[parameter(default = true, alias = "rw_enable_join_ordering")]
293    enable_join_ordering: bool,
294
295    /// Enable two phase agg optimization. Defaults to true.
296    /// Setting this to true will always set `FORCE_TWO_PHASE_AGG` to false.
297    #[parameter(default = true, flags = "SETTER", alias = "rw_enable_two_phase_agg")]
298    enable_two_phase_agg: bool,
299
300    /// Force two phase agg optimization whenever there's a choice between
301    /// optimizations. Defaults to false.
302    /// Setting this to true will always set `ENABLE_TWO_PHASE_AGG` to false.
303    #[parameter(default = false, flags = "SETTER", alias = "rw_force_two_phase_agg")]
304    force_two_phase_agg: bool,
305
306    /// Enable sharing of common sub-plans.
307    /// This means that DAG structured query plans can be constructed,
308    #[parameter(default = true, alias = "rw_enable_share_plan")]
309    /// rather than only tree structured query plans.
310    enable_share_plan: bool,
311
312    /// Enable split distinct agg
313    #[parameter(default = false, alias = "rw_force_split_distinct_agg")]
314    force_split_distinct_agg: bool,
315
316    /// See <https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-INTERVALSTYLE>
317    #[parameter(default = "", rename = "intervalstyle")]
318    interval_style: String,
319
320    /// If `BATCH_PARALLELISM` is non-zero, batch queries will use this parallelism.
321    #[parameter(default = ConfigNonZeroU64::default())]
322    batch_parallelism: ConfigNonZeroU64,
323
324    /// The version of PostgreSQL that Risingwave claims to be.
325    #[parameter(default = PG_VERSION)]
326    server_version: String,
327
328    /// The version of PostgreSQL that Risingwave claims to be.
329    #[parameter(default = SERVER_VERSION_NUM)]
330    server_version_num: i32,
331
332    /// see <https://www.postgresql.org/docs/15/runtime-config-client.html#GUC-CLIENT-MIN-MESSAGES>
333    #[parameter(default = "notice")]
334    client_min_messages: String,
335
336    /// see <https://www.postgresql.org/docs/15/runtime-config-client.html#GUC-CLIENT-ENCODING>
337    #[parameter(default = SERVER_ENCODING, check_hook = check_client_encoding)]
338    client_encoding: String,
339
340    /// Enable decoupling sink and internal streaming graph or not
341    #[parameter(default = SinkDecouple::default())]
342    sink_decouple: SinkDecouple,
343
344    /// See <https://www.postgresql.org/docs/current/runtime-config-compatible.html#RUNTIME-CONFIG-COMPATIBLE-VERSION>
345    /// Unused in RisingWave, support for compatibility.
346    #[parameter(default = false)]
347    synchronize_seqscans: bool,
348
349    /// Abort query statement that takes more than the specified amount of time in sec. If
350    /// `log_min_error_statement` is set to ERROR or lower, the statement that timed out will also be
351    /// logged. If this value is specified without units, it is taken as milliseconds. A value of
352    /// zero (the default) disables the timeout.
353    #[parameter(default = StatementTimeout::default())]
354    statement_timeout: StatementTimeout,
355
356    /// Terminate any session that has been idle (that is, waiting for a client query) within an open transaction for longer than the specified amount of time in milliseconds.
357    #[parameter(default = 60000u32)]
358    idle_in_transaction_session_timeout: u32,
359
360    /// See <https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-LOCK-TIMEOUT>
361    /// Unused in RisingWave, support for compatibility.
362    #[parameter(default = 0)]
363    lock_timeout: i32,
364
365    /// For limiting the startup time of a shareable CDC streaming source when the source is being created. Unit: seconds.
366    #[parameter(default = 60)]
367    cdc_source_wait_streaming_start_timeout: i32,
368
369    /// see <https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-ROW-SECURITY>.
370    /// Unused in RisingWave, support for compatibility.
371    #[parameter(default = true)]
372    row_security: bool,
373
374    /// see <https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-STANDARD-CONFORMING-STRINGS>
375    #[parameter(default = STANDARD_CONFORMING_STRINGS)]
376    standard_conforming_strings: String,
377
378    /// Set streaming rate limit (rows per second) for each parallelism for mv / source / sink backfilling
379    /// If set to -1, disable rate limit.
380    /// If set to 0, this pauses the snapshot read / source read.
381    #[parameter(default = DISABLE_BACKFILL_RATE_LIMIT)]
382    backfill_rate_limit: i32,
383
384    /// Set streaming rate limit (rows per second) for each parallelism for mv / source backfilling, source reads.
385    /// If set to -1, disable rate limit.
386    /// If set to 0, this pauses the snapshot read / source read.
387    #[parameter(default = DISABLE_SOURCE_RATE_LIMIT)]
388    source_rate_limit: i32,
389
390    /// Set streaming rate limit (rows per second) for each parallelism for table DML.
391    /// If set to -1, disable rate limit.
392    /// If set to 0, this pauses the DML.
393    #[parameter(default = DISABLE_DML_RATE_LIMIT)]
394    dml_rate_limit: i32,
395
396    /// Set sink rate limit (rows per second) for each parallelism for external sink.
397    /// If set to -1, disable rate limit.
398    /// If set to 0, this pauses the sink.
399    #[parameter(default = DISABLE_SINK_RATE_LIMIT)]
400    sink_rate_limit: i32,
401
402    /// Cache policy for partition cache in streaming over window.
403    /// Can be `full`, `recent`, `recent_first_n` or `recent_last_n`.
404    ///
405    /// This overrides the corresponding entry from the `[streaming.developer]` section in the config file,
406    /// taking effect for new streaming jobs created in the current session.
407    #[parameter(default = None, alias = "rw_streaming_over_window_cache_policy")]
408    streaming_over_window_cache_policy: OptionConfig<OverWindowCachePolicy>,
409
410    /// Cache refill policy for streaming cache refill feature.
411    /// Can be `enabled`, `disabled`, `streaming`, `serving` or `both`.
412    ///
413    /// This overrides the corresponding entry from the `[streaming.developer]` section in the config file,
414    /// taking effect for new streaming jobs created in the current session.
415    #[parameter(default = None)]
416    streaming_cache_refill_policy: OptionConfig<CacheRefillPolicy>,
417
418    /// Run DDL statements in background
419    #[parameter(default = false)]
420    background_ddl: bool,
421
422    /// Enable shared source. Currently only for Kafka.
423    ///
424    /// When enabled, `CREATE SOURCE` will create a source streaming job, and `CREATE MATERIALIZED VIEWS` from the source
425    /// will forward the data from the same source streaming job, and also backfill prior data from the external source.
426    #[parameter(default = true)]
427    streaming_use_shared_source: bool,
428
429    /// Enable in-memory cache for `AsOf` join executor.
430    ///
431    /// When enabled (default), `AsOf` join uses the cache-based implementation.
432    ///
433    /// When disabled, `AsOf` join uses a no-cache implementation that directly queries
434    /// the state table on-demand, reducing unnecessary data fetches for cache.
435    #[parameter(default = true)]
436    streaming_asof_join_use_cache: bool,
437
438    /// Shows the server-side character set encoding. At present, this parameter can be shown but not set, because the encoding is determined at database creation time.
439    #[parameter(default = SERVER_ENCODING)]
440    server_encoding: String,
441
442    #[parameter(default = "hex", check_hook = check_bytea_output)]
443    bytea_output: String,
444
445    /// Bypass checks on cluster limits
446    ///
447    /// When enabled, `CREATE MATERIALIZED VIEW` will not fail if the cluster limit is hit.
448    #[parameter(default = BYPASS_CLUSTER_LIMITS)]
449    bypass_cluster_limits: bool,
450
451    /// The maximum number of parallelism a streaming query can use. Defaults to 256.
452    ///
453    /// Compared to `STREAMING_PARALLELISM`, which configures the initial parallelism, this configures
454    /// the maximum parallelism a streaming query can use in the future, if the cluster size changes or
455    /// users manually change the parallelism with `ALTER .. SET PARALLELISM`.
456    ///
457    /// It's not always a good idea to set this to a very large number, as it may cause performance
458    /// degradation when performing range scans on the table or the materialized view.
459    // a.k.a. vnode count
460    #[parameter(default = VirtualNode::COUNT_FOR_COMPAT, check_hook = check_streaming_max_parallelism)]
461    streaming_max_parallelism: usize,
462
463    /// Used to provide the connection information for the iceberg engine.
464    /// Format: `iceberg_engine_connection` = `schema_name.connection_name`.
465    #[parameter(default = "", check_hook = check_iceberg_engine_connection)]
466    iceberg_engine_connection: String,
467
468    /// Whether the streaming join should be unaligned or not.
469    #[parameter(default = false)]
470    streaming_enable_unaligned_join: bool,
471
472    /// The timeout for reading from the buffer of the sync log store on barrier.
473    /// Every epoch we will attempt to read the full buffer of the sync log store.
474    /// If we hit the timeout, we will stop reading and continue.
475    ///
476    /// This overrides the corresponding entry from the `[streaming.developer]` section in the config file,
477    /// taking effect for new streaming jobs created in the current session.
478    #[parameter(default = None)]
479    streaming_sync_log_store_pause_duration_ms: OptionConfig<usize>,
480
481    /// The max buffer size for sync logstore, before we start flushing.
482    ///
483    /// This overrides the corresponding entry from the `[streaming.developer]` section in the config file,
484    /// taking effect for new streaming jobs created in the current session.
485    #[parameter(default = None)]
486    streaming_sync_log_store_buffer_size: OptionConfig<usize>,
487
488    /// Whether to disable purifying the definition of the table or source upon retrieval.
489    /// Only set this if encountering issues with functionalities like `SHOW` or `ALTER TABLE/SOURCE`.
490    /// This config may be removed in the future.
491    #[parameter(default = false, flags = "NO_ALTER_SYS")]
492    disable_purify_definition: bool,
493
494    /// The `ef_search` used in querying hnsw vector index
495    #[parameter(default = 40_usize)] // default value borrowed from pg_vector
496    batch_hnsw_ef_search: usize,
497
498    /// Enable index selection for queries
499    #[parameter(default = true)]
500    enable_index_selection: bool,
501
502    /// Enable mv selection for queries
503    #[parameter(default = false)]
504    enable_mv_selection: bool,
505
506    /// Whether to enable locality backfill. When enabled, `locality_backfill_mode` controls
507    /// whether it is selected automatically or always used.
508    #[parameter(default = true)]
509    enable_locality_backfill: bool,
510
511    /// How to apply locality backfill when it is enabled. `auto` uses the estimated backfill size,
512    /// while `always` skips the size check. Missing values from older versions mean `always` to
513    /// preserve the previous boolean behavior.
514    #[serde(default = "default_legacy_locality_backfill_mode")]
515    #[parameter(default = LocalityBackfillMode::Auto)]
516    locality_backfill_mode: LocalityBackfillMode,
517
518    /// Auto-enable locality backfill when estimated scan backfill data reaches this size in bytes.
519    /// Defaults to 10 `GiB` (10737418240 bytes).
520    #[serde(default = "default_auto_locality_backfill_min_size")]
521    #[parameter(default = AUTO_LOCALITY_BACKFILL_MIN_SIZE)]
522    auto_locality_backfill_min_size: u64,
523
524    /// Duration in seconds before notifying the user that a long-running DDL operation (e.g., DROP TABLE, CANCEL JOBS)
525    /// is still running. Set to 0 to disable notifications. Defaults to 30 seconds.
526    #[parameter(default = 30u32)]
527    slow_ddl_notification_secs: u32,
528
529    /// Unsafe: Enable storage retention for non-append-only tables.
530    /// Enabling this can lead to streaming inconsistency and node panic
531    /// if there is any row INSERT/UPDATE/DELETE operation corresponding to the ttled primary key.
532    #[parameter(default = false)]
533    unsafe_enable_storage_retention_for_non_append_only_tables: bool,
534
535    /// Enable DataFusion Engine
536    /// When enabled, queries involving Iceberg tables will be executed using the DataFusion engine.
537    #[parameter(default = true)]
538    enable_datafusion_engine: bool,
539
540    /// Prefer hash join over sort merge join in DataFusion engine
541    /// When enabled, the DataFusion engine will prioritize hash joins for query execution plans,
542    /// potentially improving performance for certain workloads, but may cause OOM for large datasets.
543    #[parameter(default = true)]
544    datafusion_prefer_hash_join: bool,
545
546    /// Emit chunks in upsert format for `UPDATE` and `DELETE` DMLs.
547    /// May lead to undefined behavior if the table is created with `ON CONFLICT DO NOTHING`.
548    ///
549    /// When enabled:
550    /// - `UPDATE` will only emit `Insert` records for new rows, instead of `Update` records.
551    /// - `DELETE` will only include key columns and pad the rest with NULL, instead of emitting complete rows.
552    #[parameter(default = false)]
553    upsert_dml: bool,
554}
555
556fn check_iceberg_engine_connection(val: &str) -> Result<(), String> {
557    if val.is_empty() {
558        return Ok(());
559    }
560
561    let parts: Vec<&str> = val.split('.').collect();
562    if parts.len() != 2 {
563        return Err("Invalid iceberg engine connection format, Should be set to this format: schema_name.connection_name.".to_owned());
564    }
565
566    Ok(())
567}
568
569fn check_timezone(val: &str) -> Result<(), String> {
570    // Check if the provided string is a valid timezone.
571    Tz::from_str_insensitive(val).map_err(|_e| "Not a valid timezone")?;
572    Ok(())
573}
574
575fn check_client_encoding(val: &str) -> Result<(), String> {
576    // https://github.com/postgres/postgres/blob/REL_15_3/src/common/encnames.c#L525
577    let clean = val.replace(|c: char| !c.is_ascii_alphanumeric(), "");
578    if !clean.eq_ignore_ascii_case("UTF8") {
579        Err("Only support 'UTF8' for CLIENT_ENCODING".to_owned())
580    } else {
581        Ok(())
582    }
583}
584
585fn check_bytea_output(val: &str) -> Result<(), String> {
586    if val == "hex" {
587        Ok(())
588    } else {
589        Err("Only support 'hex' for BYTEA_OUTPUT".to_owned())
590    }
591}
592
593/// Check if the provided value is a valid max parallelism.
594fn check_streaming_max_parallelism(val: &usize) -> Result<(), String> {
595    match val {
596        // TODO(var-vnode): this is to prevent confusion with singletons, after we distinguish
597        // them better, we may allow 1 as the max parallelism (though not much point).
598        0 | 1 => Err("STREAMING_MAX_PARALLELISM must be greater than 1".to_owned()),
599        2..=VirtualNode::MAX_COUNT => Ok(()),
600        _ => Err(format!(
601            "STREAMING_MAX_PARALLELISM must be less than or equal to {}",
602            VirtualNode::MAX_COUNT
603        )),
604    }
605}
606
607fn check_streaming_parallelism_for_backfill(val: &ConfigBackfillParallelism) -> Result<(), String> {
608    match val {
609        ConfigBackfillParallelism::Default | ConfigBackfillParallelism::Fixed(_) => Ok(()),
610        ConfigBackfillParallelism::Adaptive
611        | ConfigBackfillParallelism::Bounded(_)
612        | ConfigBackfillParallelism::Ratio(_) => Err(
613            "Only `default` or fixed backfill parallelism is supported here; adaptive backfill strategy is deferred to a later change.".to_owned(),
614        ),
615    }
616}
617
618impl SessionConfig {
619    pub fn set_force_two_phase_agg(
620        &mut self,
621        val: bool,
622        reporter: &mut impl ConfigReporter,
623    ) -> SessionConfigResult<bool> {
624        let set_val = self.set_force_two_phase_agg_inner(val, reporter)?;
625        if self.force_two_phase_agg {
626            self.set_enable_two_phase_agg(true, reporter)
627        } else {
628            Ok(set_val)
629        }
630    }
631
632    pub fn set_enable_two_phase_agg(
633        &mut self,
634        val: bool,
635        reporter: &mut impl ConfigReporter,
636    ) -> SessionConfigResult<bool> {
637        let set_val = self.set_enable_two_phase_agg_inner(val, reporter)?;
638        if !self.force_two_phase_agg {
639            self.set_force_two_phase_agg(false, reporter)
640        } else {
641            Ok(set_val)
642        }
643    }
644}
645
646pub struct VariableInfo {
647    pub name: String,
648    pub setting: String,
649    pub description: String,
650}
651
652/// Report status or notice to caller.
653pub trait ConfigReporter {
654    fn report_status(&mut self, key: &str, new_val: String);
655}
656
657// Report nothing.
658impl ConfigReporter for () {
659    fn report_status(&mut self, _key: &str, _new_val: String) {}
660}
661
662def_anyhow_newtype! {
663    pub SessionConfigToOverrideError,
664    toml::ser::Error => "failed to serialize session config",
665    ConfigMergeError => transparent,
666}
667
668impl SessionConfig {
669    /// Generate an initial override for the streaming config from the session config.
670    pub fn to_initial_streaming_config_override(
671        &self,
672    ) -> Result<String, SessionConfigToOverrideError> {
673        let mut table = toml::Table::new();
674
675        // TODO: make this more type safe.
676        // We `unwrap` here to assert the hard-coded keys are correct.
677        if let Some(v) = self.streaming_join_encoding.as_ref() {
678            table
679                .upsert("streaming.developer.join_encoding_type", v)
680                .unwrap();
681        }
682        if let Some(v) = self.streaming_sync_log_store_pause_duration_ms.as_ref() {
683            table
684                .upsert("streaming.developer.sync_log_store_pause_duration_ms", v)
685                .unwrap();
686        }
687        if let Some(v) = self.streaming_sync_log_store_buffer_size.as_ref() {
688            table
689                .upsert("streaming.developer.sync_log_store_buffer_size", v)
690                .unwrap();
691        }
692        if let Some(v) = self.streaming_over_window_cache_policy.as_ref() {
693            table
694                .upsert("streaming.developer.over_window_cache_policy", v)
695                .unwrap();
696        }
697        if let Some(v) = self.streaming_cache_refill_policy.as_ref() {
698            table
699                .upsert("streaming.developer.cache_refill_policy", v)
700                .unwrap();
701        }
702
703        let res = toml::to_string(&table)?;
704
705        // Validate all fields are valid by trying to merge it to the default config.
706        if !res.is_empty() {
707            let merged =
708                merge_streaming_config_section(&StreamingConfig::default(), res.as_str())?.unwrap();
709
710            let unrecognized_keys = merged.unrecognized_keys().collect_vec();
711            if !unrecognized_keys.is_empty() {
712                bail!("unrecognized configs: {:?}", unrecognized_keys);
713            }
714        }
715
716        Ok(res)
717    }
718}
719
720#[cfg(test)]
721mod test {
722    use expect_test::expect;
723
724    use super::*;
725
726    #[derive(SessionConfig)]
727    struct TestConfig {
728        #[parameter(default = 1, flags = "NO_ALTER_SYS", alias = "test_param_alias" | "alias_param_test")]
729        test_param: i32,
730        #[parameter(default = false, deprecated = "deprecated test notice")]
731        deprecated_test_param: bool,
732    }
733
734    #[test]
735    fn test_session_config_alias() {
736        let mut config = TestConfig::default();
737        config.set("test_param", "2".to_owned(), &mut ()).unwrap();
738        assert_eq!(config.get("test_param_alias").unwrap(), "2");
739        config
740            .set("alias_param_test", "3".to_owned(), &mut ())
741            .unwrap();
742        assert_eq!(config.get("test_param_alias").unwrap(), "3");
743        assert!(TestConfig::check_no_alter_sys("test_param").unwrap());
744        assert_eq!(
745            TestConfig::deprecated_notice("deprecated_test_param").unwrap(),
746            Some("deprecated test notice")
747        );
748        assert_eq!(TestConfig::deprecated_notice("test_param").unwrap(), None);
749    }
750
751    #[test]
752    fn test_initial_streaming_config_override() {
753        let mut config = SessionConfig::default();
754        config
755            .set_streaming_join_encoding(Some(JoinEncodingType::Cpu).into(), &mut ())
756            .unwrap();
757        config
758            .set_streaming_over_window_cache_policy(
759                Some(OverWindowCachePolicy::RecentFirstN).into(),
760                &mut (),
761            )
762            .unwrap();
763        config
764            .set_streaming_cache_refill_policy(Some(CacheRefillPolicy::Both).into(), &mut ())
765            .unwrap();
766
767        // Check the converted config override string.
768        let override_str = config.to_initial_streaming_config_override().unwrap();
769        expect![[r#"
770            [streaming.developer]
771            cache_refill_policy = "both"
772            join_encoding_type = "cpu_optimized"
773            over_window_cache_policy = "recent_first_n"
774        "#]]
775        .assert_eq(&override_str);
776
777        // Try merging it to the default streaming config.
778        let merged = merge_streaming_config_section(&StreamingConfig::default(), &override_str)
779            .unwrap()
780            .unwrap();
781        assert_eq!(merged.developer.join_encoding_type, JoinEncodingType::Cpu);
782        assert_eq!(
783            merged.developer.over_window_cache_policy,
784            OverWindowCachePolicy::RecentFirstN
785        );
786        assert_eq!(
787            merged.developer.cache_refill_policy,
788            CacheRefillPolicy::Both
789        );
790    }
791
792    #[test]
793    fn test_streaming_parallelism_defaults() {
794        let config = SessionConfig::default();
795
796        assert_eq!(config.streaming_parallelism(), ConfigParallelism::Default);
797        assert_eq!(
798            config.streaming_parallelism_for_table(),
799            ConfigParallelism::Default
800        );
801        assert_eq!(
802            config.streaming_parallelism_for_source(),
803            ConfigParallelism::Default
804        );
805        assert_eq!(
806            config.streaming_parallelism_for_sink(),
807            ConfigParallelism::Default
808        );
809        assert_eq!(
810            config.streaming_parallelism_for_index(),
811            ConfigParallelism::Default
812        );
813        assert_eq!(
814            config.streaming_parallelism_for_materialized_view(),
815            ConfigParallelism::Default
816        );
817        assert!(!config.streaming_unsafe_allow_upsert_sink_pk_mismatch());
818    }
819
820    #[test]
821    fn test_streaming_parallelism_default_round_trip() {
822        let mut config = SessionConfig::default();
823
824        assert_eq!(config.get("streaming_parallelism").unwrap(), "default");
825        assert_eq!(
826            config.get("streaming_parallelism_for_table").unwrap(),
827            "default"
828        );
829        assert_eq!(
830            config.get("streaming_parallelism_for_source").unwrap(),
831            "default"
832        );
833
834        config
835            .set("streaming_parallelism", "default".to_owned(), &mut ())
836            .unwrap();
837        assert_eq!(config.get("streaming_parallelism").unwrap(), "default");
838
839        config
840            .set("streaming_parallelism", "bounded(16)".to_owned(), &mut ())
841            .unwrap();
842        config
843            .set(
844                "streaming_parallelism_for_table",
845                "bounded(8)".to_owned(),
846                &mut (),
847            )
848            .unwrap();
849        config
850            .set(
851                "streaming_parallelism_for_source",
852                "bounded(8)".to_owned(),
853                &mut (),
854            )
855            .unwrap();
856
857        assert_eq!(
858            config.reset("streaming_parallelism", &mut ()).unwrap(),
859            "default"
860        );
861        assert_eq!(
862            config
863                .reset("streaming_parallelism_for_table", &mut ())
864                .unwrap(),
865            "default"
866        );
867        assert_eq!(
868            config
869                .reset("streaming_parallelism_for_source", &mut ())
870                .unwrap(),
871            "default"
872        );
873    }
874    #[test]
875    fn test_streaming_parallelism_for_backfill_accepts_default_and_fixed() {
876        let mut config = SessionConfig::default();
877
878        config
879            .set(
880                "streaming_parallelism_for_backfill",
881                "default".to_owned(),
882                &mut (),
883            )
884            .unwrap();
885        assert_eq!(
886            config.get("streaming_parallelism_for_backfill").unwrap(),
887            "default"
888        );
889
890        config
891            .set(
892                "streaming_parallelism_for_backfill",
893                "2".to_owned(),
894                &mut (),
895            )
896            .unwrap();
897        assert_eq!(config.streaming_parallelism_for_backfill().to_string(), "2");
898    }
899
900    #[test]
901    fn test_streaming_parallelism_for_backfill_rejects_adaptive_modes() {
902        let mut config = SessionConfig::default();
903        let expected = "Only `default` or fixed backfill parallelism is supported here; adaptive backfill strategy is deferred to a later change.";
904
905        for value in ["adaptive", "bounded(2)", "ratio(0.5)"] {
906            let err = config
907                .set(
908                    "streaming_parallelism_for_backfill",
909                    value.to_owned(),
910                    &mut (),
911                )
912                .unwrap_err();
913
914            match err {
915                SessionConfigError::InvalidValue {
916                    entry,
917                    value: actual_value,
918                    source,
919                } => {
920                    assert_eq!(entry, "streaming_parallelism_for_backfill");
921                    assert_eq!(actual_value, value);
922                    assert_eq!(source.to_string(), expected);
923                }
924                other => panic!("unexpected error: {other:?}"),
925            }
926        }
927    }
928}