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    /// Read each serving node's latest locally committed data without waiting for a frontend epoch.
179    /// Each point lookup, range scan, lookup-join probe, and task may observe a different committed
180    /// epoch, even within one scan or statement. Statements in a transaction may also differ.
181    /// Each storage snapshot remains stable. Explicit historical reads and subscription snapshots
182    /// retain their existing semantics. Iceberg reads exclude pending log-store rows. Overrides
183    /// barrier reads. Enable only after all participating frontend and serving nodes support this
184    /// option.
185    #[parameter(default = false)]
186    batch_read_latest_committed: bool,
187
188    /// See <https://www.postgresql.org/docs/current/transaction-iso.html>
189    #[parameter(default = IsolationLevel::default())]
190    transaction_isolation: IsolationLevel,
191
192    /// Select as of specific epoch.
193    /// Sets the historical epoch for querying data. If 0, querying latest data.
194    #[parameter(default = ConfigNonZeroU64::default())]
195    query_epoch: ConfigNonZeroU64,
196
197    /// Session timezone. Defaults to UTC.
198    #[parameter(default = "UTC", check_hook = check_timezone)]
199    timezone: String,
200
201    /// The execution parallelism for streaming queries, including tables, materialized views,
202    /// indexes, and sinks. Defaults to `default`, which preserves the legacy adaptive
203    /// scheduling behavior during effective resolution.
204    #[parameter(default = ConfigParallelism::Default, flags = "SESSION_INIT")]
205    streaming_parallelism: ConfigParallelism,
206
207    /// Specific parallelism for backfill. Only `default` and a fixed positive integer are
208    /// supported here. Adaptive backfill strategies are deferred to a later change.
209    #[parameter(
210        default = ConfigBackfillParallelism::Default,
211        check_hook = check_streaming_parallelism_for_backfill,
212        flags = "SESSION_INIT"
213    )]
214    streaming_parallelism_for_backfill: ConfigBackfillParallelism,
215
216    /// Specific parallelism for table. Defaults to `default`, which preserves the legacy
217    /// bounded adaptive behavior only when the global parallelism itself remains `default`.
218    /// Otherwise it follows the explicit global parallelism.
219    #[parameter(default = ConfigParallelism::Default, flags = "SESSION_INIT")]
220    streaming_parallelism_for_table: ConfigParallelism,
221
222    /// Specific parallelism for sink. By default, it will fall back to `STREAMING_PARALLELISM`.
223    #[parameter(default = ConfigParallelism::Default, flags = "SESSION_INIT")]
224    streaming_parallelism_for_sink: ConfigParallelism,
225
226    /// Specific parallelism for index. By default, it will fall back to `STREAMING_PARALLELISM`.
227    #[parameter(default = ConfigParallelism::Default, flags = "SESSION_INIT")]
228    streaming_parallelism_for_index: ConfigParallelism,
229
230    /// Specific parallelism for source. Defaults to `default`, which preserves the legacy
231    /// bounded adaptive behavior only when the global parallelism itself remains `default`.
232    /// Otherwise it follows the explicit global parallelism.
233    #[parameter(default = ConfigParallelism::Default, flags = "SESSION_INIT")]
234    streaming_parallelism_for_source: ConfigParallelism,
235
236    /// Specific parallelism for materialized view. By default, it will fall back to `STREAMING_PARALLELISM`.
237    #[parameter(default = ConfigParallelism::Default, flags = "SESSION_INIT")]
238    streaming_parallelism_for_materialized_view: ConfigParallelism,
239
240    /// Enable delta join for streaming queries. Defaults to false.
241    #[parameter(default = false, alias = "rw_streaming_enable_delta_join")]
242    streaming_enable_delta_join: bool,
243
244    /// Enable bushy join for streaming queries. Defaults to true.
245    #[parameter(default = true, alias = "rw_streaming_enable_bushy_join")]
246    streaming_enable_bushy_join: bool,
247
248    /// Force filtering to be done inside the join whenever there's a choice between optimizations.
249    /// Defaults to false.
250    #[parameter(default = false, alias = "rw_streaming_force_filter_inside_join")]
251    streaming_force_filter_inside_join: bool,
252
253    /// Deprecated. Arrangement backfill is always used as the fallback backfill type for new
254    /// streaming jobs, and this setting is ignored.
255    #[parameter(
256        default = true,
257        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."
258    )]
259    streaming_use_arrangement_backfill: bool,
260
261    #[parameter(default = true)]
262    streaming_use_snapshot_backfill: bool,
263
264    /// Enable serverless backfill for streaming queries. Defaults to false.
265    #[parameter(default = false)]
266    enable_serverless_backfill: bool,
267
268    /// Allow `jsonb` in stream key
269    #[parameter(default = false, alias = "rw_streaming_allow_jsonb_in_stream_key")]
270    streaming_allow_jsonb_in_stream_key: bool,
271
272    /// Unsafe: allow impure expressions on non-append-only streams without materialization.
273    ///
274    /// This may lead to inconsistent results or panics due to re-evaluation on updates/retracts.
275    #[parameter(default = false)]
276    streaming_unsafe_allow_unmaterialized_impure_expr: bool,
277
278    /// Unsafe: allow an upsert sink to use downstream primary-key columns that are not part of
279    /// the upstream stream key.
280    ///
281    /// This may leave stale rows in the downstream system if a downstream primary-key column
282    /// changes without the upsert stream providing its old value.
283    #[parameter(default = false)]
284    streaming_unsafe_allow_upsert_sink_pk_mismatch: bool,
285
286    /// Separate consecutive `StreamHashJoin` by no-shuffle `StreamExchange`
287    #[parameter(default = false)]
288    streaming_separate_consecutive_join: bool,
289
290    /// Separate `StreamSink` by no-shuffle `StreamExchange`
291    #[parameter(default = false)]
292    streaming_separate_sink: bool,
293
294    /// Determine which encoding will be used to encode join rows in operator cache.
295    ///
296    /// This overrides the corresponding entry from the `[streaming.developer]` section in the config file,
297    /// taking effect for new streaming jobs created in the current session.
298    #[parameter(default = None)]
299    streaming_join_encoding: OptionConfig<JoinEncodingType>,
300
301    /// Enable join ordering for streaming and batch queries. Defaults to true.
302    #[parameter(default = true, alias = "rw_enable_join_ordering")]
303    enable_join_ordering: bool,
304
305    /// Enable two phase agg optimization. Defaults to true.
306    /// Setting this to true will always set `FORCE_TWO_PHASE_AGG` to false.
307    #[parameter(default = true, flags = "SETTER", alias = "rw_enable_two_phase_agg")]
308    enable_two_phase_agg: bool,
309
310    /// Force two phase agg optimization whenever there's a choice between
311    /// optimizations. Defaults to false.
312    /// Setting this to true will always set `ENABLE_TWO_PHASE_AGG` to false.
313    #[parameter(default = false, flags = "SETTER", alias = "rw_force_two_phase_agg")]
314    force_two_phase_agg: bool,
315
316    /// Enable sharing of common sub-plans.
317    /// This means that DAG structured query plans can be constructed,
318    #[parameter(default = true, alias = "rw_enable_share_plan")]
319    /// rather than only tree structured query plans.
320    enable_share_plan: bool,
321
322    /// Enable split distinct agg
323    #[parameter(default = false, alias = "rw_force_split_distinct_agg")]
324    force_split_distinct_agg: bool,
325
326    /// See <https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-INTERVALSTYLE>
327    #[parameter(default = "", rename = "intervalstyle")]
328    interval_style: String,
329
330    /// If `BATCH_PARALLELISM` is non-zero, batch queries will use this parallelism.
331    #[parameter(default = ConfigNonZeroU64::default())]
332    batch_parallelism: ConfigNonZeroU64,
333
334    /// The version of PostgreSQL that Risingwave claims to be.
335    #[parameter(default = PG_VERSION)]
336    server_version: String,
337
338    /// The version of PostgreSQL that Risingwave claims to be.
339    #[parameter(default = SERVER_VERSION_NUM)]
340    server_version_num: i32,
341
342    /// see <https://www.postgresql.org/docs/15/runtime-config-client.html#GUC-CLIENT-MIN-MESSAGES>
343    #[parameter(default = "notice")]
344    client_min_messages: String,
345
346    /// see <https://www.postgresql.org/docs/15/runtime-config-client.html#GUC-CLIENT-ENCODING>
347    #[parameter(default = SERVER_ENCODING, check_hook = check_client_encoding)]
348    client_encoding: String,
349
350    /// Enable decoupling sink and internal streaming graph or not
351    #[parameter(default = SinkDecouple::default())]
352    sink_decouple: SinkDecouple,
353
354    /// See <https://www.postgresql.org/docs/current/runtime-config-compatible.html#RUNTIME-CONFIG-COMPATIBLE-VERSION>
355    /// Unused in RisingWave, support for compatibility.
356    #[parameter(default = false)]
357    synchronize_seqscans: bool,
358
359    /// Abort query statement that takes more than the specified amount of time in sec. If
360    /// `log_min_error_statement` is set to ERROR or lower, the statement that timed out will also be
361    /// logged. If this value is specified without units, it is taken as milliseconds. A value of
362    /// zero (the default) disables the timeout.
363    #[parameter(default = StatementTimeout::default())]
364    statement_timeout: StatementTimeout,
365
366    /// 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.
367    #[parameter(default = 60000u32)]
368    idle_in_transaction_session_timeout: u32,
369
370    /// See <https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-LOCK-TIMEOUT>
371    /// Unused in RisingWave, support for compatibility.
372    #[parameter(default = 0)]
373    lock_timeout: i32,
374
375    /// For limiting the startup time of a shareable CDC streaming source when the source is being created. Unit: seconds.
376    #[parameter(default = 60)]
377    cdc_source_wait_streaming_start_timeout: i32,
378
379    /// see <https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-ROW-SECURITY>.
380    /// Unused in RisingWave, support for compatibility.
381    #[parameter(default = true)]
382    row_security: bool,
383
384    /// see <https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-STANDARD-CONFORMING-STRINGS>
385    #[parameter(default = STANDARD_CONFORMING_STRINGS)]
386    standard_conforming_strings: String,
387
388    /// Set streaming rate limit (rows per second) for each parallelism for mv / source / sink backfilling
389    /// If set to -1, disable rate limit.
390    /// If set to 0, this pauses the snapshot read / source read.
391    #[parameter(default = DISABLE_BACKFILL_RATE_LIMIT)]
392    backfill_rate_limit: i32,
393
394    /// Set streaming rate limit (rows per second) for each parallelism for mv / source backfilling, source reads.
395    /// If set to -1, disable rate limit.
396    /// If set to 0, this pauses the snapshot read / source read.
397    #[parameter(default = DISABLE_SOURCE_RATE_LIMIT)]
398    source_rate_limit: i32,
399
400    /// Set streaming rate limit (rows per second) for each parallelism for table DML.
401    /// If set to -1, disable rate limit.
402    /// If set to 0, this pauses the DML.
403    #[parameter(default = DISABLE_DML_RATE_LIMIT)]
404    dml_rate_limit: i32,
405
406    /// Set sink rate limit (rows per second) for each parallelism for external sink.
407    /// If set to -1, disable rate limit.
408    /// If set to 0, this pauses the sink.
409    #[parameter(default = DISABLE_SINK_RATE_LIMIT)]
410    sink_rate_limit: i32,
411
412    /// Cache policy for partition cache in streaming over window.
413    /// Can be `full`, `recent`, `recent_first_n` or `recent_last_n`.
414    ///
415    /// This overrides the corresponding entry from the `[streaming.developer]` section in the config file,
416    /// taking effect for new streaming jobs created in the current session.
417    #[parameter(default = None, alias = "rw_streaming_over_window_cache_policy")]
418    streaming_over_window_cache_policy: OptionConfig<OverWindowCachePolicy>,
419
420    /// Cache refill policy for streaming cache refill feature.
421    /// Can be `enabled`, `disabled`, `streaming`, `serving` or `both`.
422    ///
423    /// This overrides the corresponding entry from the `[streaming.developer]` section in the config file,
424    /// taking effect for new streaming jobs created in the current session.
425    #[parameter(default = None)]
426    streaming_cache_refill_policy: OptionConfig<CacheRefillPolicy>,
427
428    /// Run DDL statements in background
429    #[parameter(default = false)]
430    background_ddl: bool,
431
432    /// Enable shared source. Currently only for Kafka.
433    ///
434    /// When enabled, `CREATE SOURCE` will create a source streaming job, and `CREATE MATERIALIZED VIEWS` from the source
435    /// will forward the data from the same source streaming job, and also backfill prior data from the external source.
436    #[parameter(default = true)]
437    streaming_use_shared_source: bool,
438
439    /// Enable in-memory cache for `AsOf` join executor.
440    ///
441    /// When enabled (default), `AsOf` join uses the cache-based implementation.
442    ///
443    /// When disabled, `AsOf` join uses a no-cache implementation that directly queries
444    /// the state table on-demand, reducing unnecessary data fetches for cache.
445    #[parameter(default = true)]
446    streaming_asof_join_use_cache: bool,
447
448    /// 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.
449    #[parameter(default = SERVER_ENCODING)]
450    server_encoding: String,
451
452    #[parameter(default = "hex", check_hook = check_bytea_output)]
453    bytea_output: String,
454
455    /// Bypass checks on cluster limits
456    ///
457    /// When enabled, `CREATE MATERIALIZED VIEW` will not fail if the cluster limit is hit.
458    #[parameter(default = BYPASS_CLUSTER_LIMITS)]
459    bypass_cluster_limits: bool,
460
461    /// The maximum number of parallelism a streaming query can use. Defaults to 256.
462    ///
463    /// Compared to `STREAMING_PARALLELISM`, which configures the initial parallelism, this configures
464    /// the maximum parallelism a streaming query can use in the future, if the cluster size changes or
465    /// users manually change the parallelism with `ALTER .. SET PARALLELISM`.
466    ///
467    /// It's not always a good idea to set this to a very large number, as it may cause performance
468    /// degradation when performing range scans on the table or the materialized view.
469    // a.k.a. vnode count
470    #[parameter(default = VirtualNode::COUNT_FOR_COMPAT, check_hook = check_streaming_max_parallelism)]
471    streaming_max_parallelism: usize,
472
473    /// Used to provide the connection information for the iceberg engine.
474    /// Format: `iceberg_engine_connection` = `schema_name.connection_name`.
475    #[parameter(default = "", check_hook = check_iceberg_engine_connection)]
476    iceberg_engine_connection: String,
477
478    /// Whether the streaming join should be unaligned or not.
479    #[parameter(default = false)]
480    streaming_enable_unaligned_join: bool,
481
482    /// The timeout for reading from the buffer of the sync log store on barrier.
483    /// Every epoch we will attempt to read the full buffer of the sync log store.
484    /// If we hit the timeout, we will stop reading and continue.
485    ///
486    /// This overrides the corresponding entry from the `[streaming.developer]` section in the config file,
487    /// taking effect for new streaming jobs created in the current session.
488    #[parameter(default = None)]
489    streaming_sync_log_store_pause_duration_ms: OptionConfig<usize>,
490
491    /// The max buffer size for sync logstore, before we start flushing.
492    ///
493    /// This overrides the corresponding entry from the `[streaming.developer]` section in the config file,
494    /// taking effect for new streaming jobs created in the current session.
495    #[parameter(default = None)]
496    streaming_sync_log_store_buffer_size: OptionConfig<usize>,
497
498    /// Whether to disable purifying the definition of the table or source upon retrieval.
499    /// Only set this if encountering issues with functionalities like `SHOW` or `ALTER TABLE/SOURCE`.
500    /// This config may be removed in the future.
501    #[parameter(default = false, flags = "NO_ALTER_SYS")]
502    disable_purify_definition: bool,
503
504    /// The `ef_search` used in querying hnsw vector index
505    #[parameter(default = 40_usize)] // default value borrowed from pg_vector
506    batch_hnsw_ef_search: usize,
507
508    /// Enable index selection for queries
509    #[parameter(default = true)]
510    enable_index_selection: bool,
511
512    /// Enable mv selection for queries
513    #[parameter(default = false)]
514    enable_mv_selection: bool,
515
516    /// Whether to enable locality backfill. When enabled, `locality_backfill_mode` controls
517    /// whether it is selected automatically or always used.
518    #[parameter(default = true)]
519    enable_locality_backfill: bool,
520
521    /// How to apply locality backfill when it is enabled. `auto` uses the estimated backfill size,
522    /// while `always` skips the size check. Missing values from older versions mean `always` to
523    /// preserve the previous boolean behavior.
524    #[serde(default = "default_legacy_locality_backfill_mode")]
525    #[parameter(default = LocalityBackfillMode::Auto)]
526    locality_backfill_mode: LocalityBackfillMode,
527
528    /// Auto-enable locality backfill when estimated scan backfill data reaches this size in bytes.
529    /// Defaults to 10 `GiB` (10737418240 bytes).
530    #[serde(default = "default_auto_locality_backfill_min_size")]
531    #[parameter(default = AUTO_LOCALITY_BACKFILL_MIN_SIZE)]
532    auto_locality_backfill_min_size: u64,
533
534    /// Enable the sort buffer of locality providers for streaming queries created in this session.
535    /// When enabled, locality providers buffer and re-order amplified intermediate results (e.g.,
536    /// caused by join amplification) within an epoch to provide better locality for downstream
537    /// operators, with bounded memory usage. Only takes effect on streaming jobs planned with
538    /// locality providers (see `enable_locality_backfill`). Defaults to true.
539    #[parameter(default = true)]
540    enable_locality_sort_buffer: bool,
541
542    /// Duration in seconds before notifying the user that a long-running DDL operation (e.g., DROP TABLE, CANCEL JOBS)
543    /// is still running. Set to 0 to disable notifications. Defaults to 30 seconds.
544    #[parameter(default = 30u32)]
545    slow_ddl_notification_secs: u32,
546
547    /// Unsafe: Enable storage retention for non-append-only tables.
548    /// Enabling this can lead to streaming inconsistency and node panic
549    /// if there is any row INSERT/UPDATE/DELETE operation corresponding to the ttled primary key.
550    #[parameter(default = false)]
551    unsafe_enable_storage_retention_for_non_append_only_tables: bool,
552
553    /// Enable DataFusion Engine
554    /// When enabled, queries involving Iceberg tables will be executed using the DataFusion engine.
555    #[parameter(default = true)]
556    enable_datafusion_engine: bool,
557
558    /// Prefer hash join over sort merge join in DataFusion engine
559    /// When enabled, the DataFusion engine will prioritize hash joins for query execution plans,
560    /// potentially improving performance for certain workloads, but may cause OOM for large datasets.
561    #[parameter(default = true)]
562    datafusion_prefer_hash_join: bool,
563
564    /// Emit chunks in upsert format for `UPDATE` and `DELETE` DMLs.
565    /// May lead to undefined behavior if the table is created with `ON CONFLICT DO NOTHING`.
566    ///
567    /// When enabled:
568    /// - `UPDATE` will only emit `Insert` records for new rows, instead of `Update` records.
569    /// - `DELETE` will only include key columns and pad the rest with NULL, instead of emitting complete rows.
570    #[parameter(default = false)]
571    upsert_dml: bool,
572}
573
574fn check_iceberg_engine_connection(val: &str) -> Result<(), String> {
575    if val.is_empty() {
576        return Ok(());
577    }
578
579    let parts: Vec<&str> = val.split('.').collect();
580    if parts.len() != 2 {
581        return Err("Invalid iceberg engine connection format, Should be set to this format: schema_name.connection_name.".to_owned());
582    }
583
584    Ok(())
585}
586
587fn check_timezone(val: &str) -> Result<(), String> {
588    // Check if the provided string is a valid timezone.
589    Tz::from_str_insensitive(val).map_err(|_e| "Not a valid timezone")?;
590    Ok(())
591}
592
593fn check_client_encoding(val: &str) -> Result<(), String> {
594    // https://github.com/postgres/postgres/blob/REL_15_3/src/common/encnames.c#L525
595    let clean = val.replace(|c: char| !c.is_ascii_alphanumeric(), "");
596    if !clean.eq_ignore_ascii_case("UTF8") {
597        Err("Only support 'UTF8' for CLIENT_ENCODING".to_owned())
598    } else {
599        Ok(())
600    }
601}
602
603fn check_bytea_output(val: &str) -> Result<(), String> {
604    if val == "hex" {
605        Ok(())
606    } else {
607        Err("Only support 'hex' for BYTEA_OUTPUT".to_owned())
608    }
609}
610
611/// Check if the provided value is a valid max parallelism.
612fn check_streaming_max_parallelism(val: &usize) -> Result<(), String> {
613    match val {
614        // TODO(var-vnode): this is to prevent confusion with singletons, after we distinguish
615        // them better, we may allow 1 as the max parallelism (though not much point).
616        0 | 1 => Err("STREAMING_MAX_PARALLELISM must be greater than 1".to_owned()),
617        2..=VirtualNode::MAX_COUNT => Ok(()),
618        _ => Err(format!(
619            "STREAMING_MAX_PARALLELISM must be less than or equal to {}",
620            VirtualNode::MAX_COUNT
621        )),
622    }
623}
624
625fn check_streaming_parallelism_for_backfill(val: &ConfigBackfillParallelism) -> Result<(), String> {
626    match val {
627        ConfigBackfillParallelism::Default | ConfigBackfillParallelism::Fixed(_) => Ok(()),
628        ConfigBackfillParallelism::Adaptive
629        | ConfigBackfillParallelism::Bounded(_)
630        | ConfigBackfillParallelism::Ratio(_) => Err(
631            "Only `default` or fixed backfill parallelism is supported here; adaptive backfill strategy is deferred to a later change.".to_owned(),
632        ),
633    }
634}
635
636impl SessionConfig {
637    pub fn set_force_two_phase_agg(
638        &mut self,
639        val: bool,
640        reporter: &mut impl ConfigReporter,
641    ) -> SessionConfigResult<bool> {
642        let set_val = self.set_force_two_phase_agg_inner(val, reporter)?;
643        if self.force_two_phase_agg {
644            self.set_enable_two_phase_agg(true, reporter)
645        } else {
646            Ok(set_val)
647        }
648    }
649
650    pub fn set_enable_two_phase_agg(
651        &mut self,
652        val: bool,
653        reporter: &mut impl ConfigReporter,
654    ) -> SessionConfigResult<bool> {
655        let set_val = self.set_enable_two_phase_agg_inner(val, reporter)?;
656        if !self.force_two_phase_agg {
657            self.set_force_two_phase_agg(false, reporter)
658        } else {
659            Ok(set_val)
660        }
661    }
662}
663
664pub struct VariableInfo {
665    pub name: String,
666    pub setting: String,
667    pub description: String,
668}
669
670/// Report status or notice to caller.
671pub trait ConfigReporter {
672    fn report_status(&mut self, key: &str, new_val: String);
673}
674
675// Report nothing.
676impl ConfigReporter for () {
677    fn report_status(&mut self, _key: &str, _new_val: String) {}
678}
679
680def_anyhow_newtype! {
681    pub SessionConfigToOverrideError,
682    toml::ser::Error => "failed to serialize session config",
683    ConfigMergeError => transparent,
684}
685
686impl SessionConfig {
687    /// Generate an initial override for the streaming config from the session config.
688    pub fn to_initial_streaming_config_override(
689        &self,
690    ) -> Result<String, SessionConfigToOverrideError> {
691        let mut table = toml::Table::new();
692
693        // TODO: make this more type safe.
694        // We `unwrap` here to assert the hard-coded keys are correct.
695        if let Some(v) = self.streaming_join_encoding.as_ref() {
696            table
697                .upsert("streaming.developer.join_encoding_type", v)
698                .unwrap();
699        }
700        if let Some(v) = self.streaming_sync_log_store_pause_duration_ms.as_ref() {
701            table
702                .upsert("streaming.developer.sync_log_store_pause_duration_ms", v)
703                .unwrap();
704        }
705        if let Some(v) = self.streaming_sync_log_store_buffer_size.as_ref() {
706            table
707                .upsert("streaming.developer.sync_log_store_buffer_size", v)
708                .unwrap();
709        }
710        if let Some(v) = self.streaming_over_window_cache_policy.as_ref() {
711            table
712                .upsert("streaming.developer.over_window_cache_policy", v)
713                .unwrap();
714        }
715        if let Some(v) = self.streaming_cache_refill_policy.as_ref() {
716            table
717                .upsert("streaming.developer.cache_refill_policy", v)
718                .unwrap();
719        }
720
721        let res = toml::to_string(&table)?;
722
723        // Validate all fields are valid by trying to merge it to the default config.
724        if !res.is_empty() {
725            let merged =
726                merge_streaming_config_section(&StreamingConfig::default(), res.as_str())?.unwrap();
727
728            let unrecognized_keys = merged.unrecognized_keys().collect_vec();
729            if !unrecognized_keys.is_empty() {
730                bail!("unrecognized configs: {:?}", unrecognized_keys);
731            }
732        }
733
734        Ok(res)
735    }
736}
737
738#[cfg(test)]
739mod test {
740    use expect_test::expect;
741
742    use super::*;
743
744    #[derive(SessionConfig)]
745    struct TestConfig {
746        #[parameter(default = 1, flags = "NO_ALTER_SYS", alias = "test_param_alias" | "alias_param_test")]
747        test_param: i32,
748        #[parameter(default = false, deprecated = "deprecated test notice")]
749        deprecated_test_param: bool,
750    }
751
752    #[test]
753    fn test_batch_read_latest_committed() {
754        let mut config = SessionConfig::default();
755        assert!(!config.batch_read_latest_committed());
756        assert_eq!(config.get("batch_read_latest_committed").unwrap(), "false");
757        config
758            .set("batch_read_latest_committed", "true".to_owned(), &mut ())
759            .unwrap();
760        assert!(config.batch_read_latest_committed());
761        assert_eq!(config.get("batch_read_latest_committed").unwrap(), "true");
762        config
763            .reset("batch_read_latest_committed", &mut ())
764            .unwrap();
765        assert!(!config.batch_read_latest_committed());
766    }
767
768    #[test]
769    fn test_session_config_alias() {
770        let mut config = TestConfig::default();
771        config.set("test_param", "2".to_owned(), &mut ()).unwrap();
772        assert_eq!(config.get("test_param_alias").unwrap(), "2");
773        config
774            .set("alias_param_test", "3".to_owned(), &mut ())
775            .unwrap();
776        assert_eq!(config.get("test_param_alias").unwrap(), "3");
777        assert!(TestConfig::check_no_alter_sys("test_param").unwrap());
778        assert_eq!(
779            TestConfig::deprecated_notice("deprecated_test_param").unwrap(),
780            Some("deprecated test notice")
781        );
782        assert_eq!(TestConfig::deprecated_notice("test_param").unwrap(), None);
783    }
784
785    #[test]
786    fn test_initial_streaming_config_override() {
787        let mut config = SessionConfig::default();
788        config
789            .set_streaming_join_encoding(Some(JoinEncodingType::Cpu).into(), &mut ())
790            .unwrap();
791        config
792            .set_streaming_over_window_cache_policy(
793                Some(OverWindowCachePolicy::RecentFirstN).into(),
794                &mut (),
795            )
796            .unwrap();
797        config
798            .set_streaming_cache_refill_policy(Some(CacheRefillPolicy::Both).into(), &mut ())
799            .unwrap();
800
801        // Check the converted config override string.
802        let override_str = config.to_initial_streaming_config_override().unwrap();
803        expect![[r#"
804            [streaming.developer]
805            cache_refill_policy = "both"
806            join_encoding_type = "cpu_optimized"
807            over_window_cache_policy = "recent_first_n"
808        "#]]
809        .assert_eq(&override_str);
810
811        // Try merging it to the default streaming config.
812        let merged = merge_streaming_config_section(&StreamingConfig::default(), &override_str)
813            .unwrap()
814            .unwrap();
815        assert_eq!(merged.developer.join_encoding_type, JoinEncodingType::Cpu);
816        assert_eq!(
817            merged.developer.over_window_cache_policy,
818            OverWindowCachePolicy::RecentFirstN
819        );
820        assert_eq!(
821            merged.developer.cache_refill_policy,
822            CacheRefillPolicy::Both
823        );
824    }
825
826    #[test]
827    fn test_streaming_parallelism_defaults() {
828        let config = SessionConfig::default();
829
830        assert_eq!(config.streaming_parallelism(), ConfigParallelism::Default);
831        assert_eq!(
832            config.streaming_parallelism_for_table(),
833            ConfigParallelism::Default
834        );
835        assert_eq!(
836            config.streaming_parallelism_for_source(),
837            ConfigParallelism::Default
838        );
839        assert_eq!(
840            config.streaming_parallelism_for_sink(),
841            ConfigParallelism::Default
842        );
843        assert_eq!(
844            config.streaming_parallelism_for_index(),
845            ConfigParallelism::Default
846        );
847        assert_eq!(
848            config.streaming_parallelism_for_materialized_view(),
849            ConfigParallelism::Default
850        );
851        assert!(!config.streaming_unsafe_allow_upsert_sink_pk_mismatch());
852    }
853
854    #[test]
855    fn test_streaming_parallelism_default_round_trip() {
856        let mut config = SessionConfig::default();
857
858        assert_eq!(config.get("streaming_parallelism").unwrap(), "default");
859        assert_eq!(
860            config.get("streaming_parallelism_for_table").unwrap(),
861            "default"
862        );
863        assert_eq!(
864            config.get("streaming_parallelism_for_source").unwrap(),
865            "default"
866        );
867
868        config
869            .set("streaming_parallelism", "default".to_owned(), &mut ())
870            .unwrap();
871        assert_eq!(config.get("streaming_parallelism").unwrap(), "default");
872
873        config
874            .set("streaming_parallelism", "bounded(16)".to_owned(), &mut ())
875            .unwrap();
876        config
877            .set(
878                "streaming_parallelism_for_table",
879                "bounded(8)".to_owned(),
880                &mut (),
881            )
882            .unwrap();
883        config
884            .set(
885                "streaming_parallelism_for_source",
886                "bounded(8)".to_owned(),
887                &mut (),
888            )
889            .unwrap();
890
891        assert_eq!(
892            config.reset("streaming_parallelism", &mut ()).unwrap(),
893            "default"
894        );
895        assert_eq!(
896            config
897                .reset("streaming_parallelism_for_table", &mut ())
898                .unwrap(),
899            "default"
900        );
901        assert_eq!(
902            config
903                .reset("streaming_parallelism_for_source", &mut ())
904                .unwrap(),
905            "default"
906        );
907    }
908    #[test]
909    fn test_streaming_parallelism_for_backfill_accepts_default_and_fixed() {
910        let mut config = SessionConfig::default();
911
912        config
913            .set(
914                "streaming_parallelism_for_backfill",
915                "default".to_owned(),
916                &mut (),
917            )
918            .unwrap();
919        assert_eq!(
920            config.get("streaming_parallelism_for_backfill").unwrap(),
921            "default"
922        );
923
924        config
925            .set(
926                "streaming_parallelism_for_backfill",
927                "2".to_owned(),
928                &mut (),
929            )
930            .unwrap();
931        assert_eq!(config.streaming_parallelism_for_backfill().to_string(), "2");
932    }
933
934    #[test]
935    fn test_streaming_parallelism_for_backfill_rejects_adaptive_modes() {
936        let mut config = SessionConfig::default();
937        let expected = "Only `default` or fixed backfill parallelism is supported here; adaptive backfill strategy is deferred to a later change.";
938
939        for value in ["adaptive", "bounded(2)", "ratio(0.5)"] {
940            let err = config
941                .set(
942                    "streaming_parallelism_for_backfill",
943                    value.to_owned(),
944                    &mut (),
945                )
946                .unwrap_err();
947
948            match err {
949                SessionConfigError::InvalidValue {
950                    entry,
951                    value: actual_value,
952                    source,
953                } => {
954                    assert_eq!(entry, "streaming_parallelism_for_backfill");
955                    assert_eq!(actual_value, value);
956                    assert_eq!(source.to_string(), expected);
957                }
958                other => panic!("unexpected error: {other:?}"),
959            }
960        }
961    }
962}