risingwave_frontend/binder/mod.rs
1// Copyright 2025 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
15use std::collections::{HashMap, HashSet};
16use std::sync::Arc;
17
18use itertools::Itertools;
19use parking_lot::RwLock;
20use risingwave_common::catalog::FunctionId;
21use risingwave_common::session_config::{SearchPath, SessionConfig};
22use risingwave_common::types::DataType;
23use risingwave_common::util::iter_util::ZipEqDebug;
24use risingwave_sqlparser::ast::Statement;
25
26use crate::error::Result;
27
28mod bind_context;
29mod bind_param;
30mod create;
31mod create_view;
32mod declare_cursor;
33mod delete;
34mod expr;
35pub mod fetch_cursor;
36mod for_system;
37mod insert;
38mod query;
39mod relation;
40mod select;
41mod set_expr;
42mod statement;
43mod struct_field;
44mod update;
45mod values;
46
47pub use bind_context::{BindContext, Clause, LateralBindContext};
48pub use create_view::BoundCreateView;
49pub use delete::BoundDelete;
50pub use expr::bind_data_type;
51pub use insert::BoundInsert;
52use pgwire::pg_server::{Session, SessionId};
53pub use query::BoundQuery;
54pub use relation::{
55 BoundBackCteRef, BoundBaseTable, BoundJoin, BoundShare, BoundShareInput, BoundSource,
56 BoundSystemTable, BoundWatermark, BoundWindowTableFunction, Relation,
57 ResolveQualifiedNameError, WindowTableFunctionKind,
58};
59pub use select::{BoundDistinct, BoundSelect};
60pub use set_expr::*;
61pub use statement::BoundStatement;
62pub use update::{BoundUpdate, UpdateProject};
63pub use values::BoundValues;
64
65use crate::catalog::catalog_service::CatalogReadGuard;
66use crate::catalog::root_catalog::SchemaPath;
67use crate::catalog::schema_catalog::SchemaCatalog;
68use crate::catalog::{CatalogResult, DatabaseId, TableId, ViewId};
69use crate::error::ErrorCode;
70use crate::session::{AuthContext, SessionImpl, TemporarySourceManager};
71use crate::user::user_service::UserInfoReadGuard;
72
73pub type ShareId = usize;
74
75/// The type of binding statement.
76enum BindFor {
77 /// Binding MV/SINK
78 Stream,
79 /// Binding a batch query
80 Batch,
81 /// Binding a DDL (e.g. CREATE TABLE/SOURCE)
82 Ddl,
83 /// Binding a system query (e.g. SHOW)
84 System,
85}
86
87/// `Binder` binds the identifiers in AST to columns in relations
88pub struct Binder {
89 // TODO: maybe we can only lock the database, but not the whole catalog.
90 catalog: CatalogReadGuard,
91 user: UserInfoReadGuard,
92 db_name: String,
93 database_id: DatabaseId,
94 session_id: SessionId,
95 context: BindContext,
96 auth_context: Arc<AuthContext>,
97 /// A stack holding contexts of outer queries when binding a subquery.
98 /// It also holds all of the lateral contexts for each respective
99 /// subquery.
100 ///
101 /// See [`Binder::bind_subquery_expr`] for details.
102 upper_subquery_contexts: Vec<(BindContext, Vec<LateralBindContext>)>,
103
104 /// A stack holding contexts of left-lateral `TableFactor`s.
105 ///
106 /// We need a separate stack as `CorrelatedInputRef` depth is
107 /// determined by the upper subquery context depth, not the lateral context stack depth.
108 lateral_contexts: Vec<LateralBindContext>,
109
110 next_subquery_id: usize,
111 next_values_id: usize,
112 /// The `ShareId` is used to identify the share relation which could be a CTE, a source, a view
113 /// and so on.
114 next_share_id: ShareId,
115
116 session_config: Arc<RwLock<SessionConfig>>,
117
118 search_path: SearchPath,
119 /// The type of binding statement.
120 bind_for: BindFor,
121
122 /// `ShareId`s identifying shared views.
123 shared_views: HashMap<ViewId, ShareId>,
124
125 /// The included relations while binding a query.
126 included_relations: HashSet<TableId>,
127
128 /// The included user-defined functions while binding a query.
129 included_udfs: HashSet<FunctionId>,
130
131 param_types: ParameterTypes,
132
133 /// The temporary sources that will be used during binding phase
134 temporary_source_manager: TemporarySourceManager,
135
136 /// Information for `secure_compare` function. It's ONLY available when binding the
137 /// `VALIDATE` clause of Webhook source i.e. `VALIDATE SECRET ... AS SECURE_COMPARE(...)`.
138 secure_compare_context: Option<SecureCompareContext>,
139}
140
141// There's one more hidden name, `HEADERS`, which is a reserved identifier for HTTP headers. Its type is `JSONB`.
142#[derive(Default, Clone, Debug)]
143pub struct SecureCompareContext {
144 /// The column name to store the whole payload in `JSONB`, but during validation it will be used as `bytea`
145 pub column_name: String,
146 /// The secret (usually a token provided by the webhook source user) to validate the calls
147 pub secret_name: Option<String>,
148}
149
150/// `ParameterTypes` is used to record the types of the parameters during binding prepared stataments.
151/// It works by following the rules:
152/// 1. At the beginning, it contains the user specified parameters type.
153/// 2. When the binder encounters a parameter, it will record it as unknown(call `record_new_param`)
154/// if it didn't exist in `ParameterTypes`.
155/// 3. When the binder encounters a cast on parameter, if it's a unknown type, the cast function
156/// will record the target type as infer type for that parameter(call `record_infer_type`). If the
157/// parameter has been inferred, the cast function will act as a normal cast.
158/// 4. After bind finished:
159/// (a) parameter not in `ParameterTypes` means that the user didn't specify it and it didn't
160/// occur in the query. `export` will return error if there is a kind of
161/// parameter. This rule is compatible with PostgreSQL
162/// (b) parameter is None means that it's a unknown type. The user didn't specify it
163/// and we can't infer it in the query. We will treat it as VARCHAR type finally. This rule is
164/// compatible with PostgreSQL.
165/// (c) parameter is Some means that it's a known type.
166#[derive(Clone, Debug)]
167pub struct ParameterTypes(Arc<RwLock<HashMap<u64, Option<DataType>>>>);
168
169impl ParameterTypes {
170 pub fn new(specified_param_types: Vec<Option<DataType>>) -> Self {
171 let map = specified_param_types
172 .into_iter()
173 .enumerate()
174 .map(|(index, data_type)| ((index + 1) as u64, data_type))
175 .collect::<HashMap<u64, Option<DataType>>>();
176 Self(Arc::new(RwLock::new(map)))
177 }
178
179 pub fn has_infer(&self, index: u64) -> bool {
180 self.0.read().get(&index).unwrap().is_some()
181 }
182
183 pub fn read_type(&self, index: u64) -> Option<DataType> {
184 self.0.read().get(&index).unwrap().clone()
185 }
186
187 pub fn record_new_param(&mut self, index: u64) {
188 self.0.write().entry(index).or_insert(None);
189 }
190
191 pub fn record_infer_type(&mut self, index: u64, data_type: &DataType) {
192 assert!(
193 !self.has_infer(index),
194 "The parameter has been inferred, should not be inferred again."
195 );
196 self.0
197 .write()
198 .get_mut(&index)
199 .unwrap()
200 .replace(data_type.clone());
201 }
202
203 pub fn export(&self) -> Result<Vec<DataType>> {
204 let types = self
205 .0
206 .read()
207 .clone()
208 .into_iter()
209 .sorted_by_key(|(index, _)| *index)
210 .collect::<Vec<_>>();
211
212 // Check if all the parameters have been inferred.
213 for ((index, _), expect_index) in types.iter().zip_eq_debug(1_u64..=types.len() as u64) {
214 if *index != expect_index {
215 return Err(ErrorCode::InvalidInputSyntax(format!(
216 "Cannot infer the type of the parameter {}.",
217 expect_index
218 ))
219 .into());
220 }
221 }
222
223 Ok(types
224 .into_iter()
225 .map(|(_, data_type)| data_type.unwrap_or(DataType::Varchar))
226 .collect::<Vec<_>>())
227 }
228}
229
230impl Binder {
231 fn new_inner(
232 session: &SessionImpl,
233 bind_for: BindFor,
234 param_types: Vec<Option<DataType>>,
235 ) -> Binder {
236 Binder {
237 catalog: session.env().catalog_reader().read_guard(),
238 user: session.env().user_info_reader().read_guard(),
239 db_name: session.database(),
240 database_id: session.database_id(),
241 session_id: session.id(),
242 context: BindContext::new(),
243 auth_context: session.auth_context(),
244 upper_subquery_contexts: vec![],
245 lateral_contexts: vec![],
246 next_subquery_id: 0,
247 next_values_id: 0,
248 next_share_id: 0,
249 session_config: session.shared_config(),
250 search_path: session.config().search_path(),
251 bind_for,
252 shared_views: HashMap::new(),
253 included_relations: HashSet::new(),
254 included_udfs: HashSet::new(),
255 param_types: ParameterTypes::new(param_types),
256 temporary_source_manager: session.temporary_source_manager(),
257 secure_compare_context: None,
258 }
259 }
260
261 pub fn new(session: &SessionImpl) -> Binder {
262 Self::new_inner(session, BindFor::Batch, vec![])
263 }
264
265 pub fn new_with_param_types(
266 session: &SessionImpl,
267 param_types: Vec<Option<DataType>>,
268 ) -> Binder {
269 Self::new_inner(session, BindFor::Batch, param_types)
270 }
271
272 pub fn new_for_stream(session: &SessionImpl) -> Binder {
273 Self::new_inner(session, BindFor::Stream, vec![])
274 }
275
276 pub fn new_for_ddl(session: &SessionImpl) -> Binder {
277 Self::new_inner(session, BindFor::Ddl, vec![])
278 }
279
280 pub fn new_for_ddl_with_secure_compare(
281 session: &SessionImpl,
282 ctx: SecureCompareContext,
283 ) -> Binder {
284 let mut binder = Self::new_inner(session, BindFor::Ddl, vec![]);
285 binder.secure_compare_context = Some(ctx);
286 binder
287 }
288
289 pub fn new_for_system(session: &SessionImpl) -> Binder {
290 Self::new_inner(session, BindFor::System, vec![])
291 }
292
293 fn is_for_stream(&self) -> bool {
294 matches!(self.bind_for, BindFor::Stream)
295 }
296
297 #[allow(dead_code)]
298 fn is_for_batch(&self) -> bool {
299 matches!(self.bind_for, BindFor::Batch)
300 }
301
302 fn is_for_ddl(&self) -> bool {
303 matches!(self.bind_for, BindFor::Ddl)
304 }
305
306 /// Bind a [`Statement`].
307 pub fn bind(&mut self, stmt: Statement) -> Result<BoundStatement> {
308 self.bind_statement(stmt)
309 }
310
311 pub fn export_param_types(&self) -> Result<Vec<DataType>> {
312 self.param_types.export()
313 }
314
315 /// Get included relations in the query after binding. This is used for resolving relation
316 /// dependencies. Note that it only contains referenced relations discovered during binding.
317 /// After the plan is built, the referenced relations may be changed. We cannot rely on the
318 /// collection result of plan, because we still need to record the dependencies that have been
319 /// optimised away.
320 pub fn included_relations(&self) -> &HashSet<TableId> {
321 &self.included_relations
322 }
323
324 /// Get included user-defined functions in the query after binding.
325 pub fn included_udfs(&self) -> &HashSet<FunctionId> {
326 &self.included_udfs
327 }
328
329 fn push_context(&mut self) {
330 let new_context = std::mem::take(&mut self.context);
331 self.context
332 .cte_to_relation
333 .clone_from(&new_context.cte_to_relation);
334 self.context.disable_security_invoker = new_context.disable_security_invoker;
335 let new_lateral_contexts = std::mem::take(&mut self.lateral_contexts);
336 self.upper_subquery_contexts
337 .push((new_context, new_lateral_contexts));
338 }
339
340 fn pop_context(&mut self) -> Result<()> {
341 let (old_context, old_lateral_contexts) = self
342 .upper_subquery_contexts
343 .pop()
344 .ok_or_else(|| ErrorCode::InternalError("Popping non-existent context".to_owned()))?;
345 self.context = old_context;
346 self.lateral_contexts = old_lateral_contexts;
347 Ok(())
348 }
349
350 fn push_lateral_context(&mut self) {
351 let new_context = std::mem::take(&mut self.context);
352 self.context
353 .cte_to_relation
354 .clone_from(&new_context.cte_to_relation);
355 self.context.disable_security_invoker = new_context.disable_security_invoker;
356 self.lateral_contexts.push(LateralBindContext {
357 is_visible: false,
358 context: new_context,
359 });
360 }
361
362 fn pop_and_merge_lateral_context(&mut self) -> Result<()> {
363 let mut old_context = self
364 .lateral_contexts
365 .pop()
366 .ok_or_else(|| ErrorCode::InternalError("Popping non-existent context".to_owned()))?
367 .context;
368 old_context.merge_context(self.context.clone())?;
369 self.context = old_context;
370 Ok(())
371 }
372
373 fn try_mark_lateral_as_visible(&mut self) {
374 if let Some(mut ctx) = self.lateral_contexts.pop() {
375 ctx.is_visible = true;
376 self.lateral_contexts.push(ctx);
377 }
378 }
379
380 fn try_mark_lateral_as_invisible(&mut self) {
381 if let Some(mut ctx) = self.lateral_contexts.pop() {
382 ctx.is_visible = false;
383 self.lateral_contexts.push(ctx);
384 }
385 }
386
387 /// Returns a reverse iterator over the upper subquery contexts that are visible to the current
388 /// context. Not to be confused with `is_visible` in [`LateralBindContext`].
389 ///
390 /// In most cases, this should include all the upper subquery contexts. However, when binding
391 /// SQL UDFs, we should avoid resolving the context outside the UDF for hygiene.
392 fn visible_upper_subquery_contexts_rev(
393 &self,
394 ) -> impl Iterator<Item = &(BindContext, Vec<LateralBindContext>)> + '_ {
395 self.upper_subquery_contexts
396 .iter()
397 .rev()
398 .take_while(|(context, _)| context.sql_udf_arguments.is_none())
399 }
400
401 fn next_subquery_id(&mut self) -> usize {
402 let id = self.next_subquery_id;
403 self.next_subquery_id += 1;
404 id
405 }
406
407 fn next_values_id(&mut self) -> usize {
408 let id = self.next_values_id;
409 self.next_values_id += 1;
410 id
411 }
412
413 fn next_share_id(&mut self) -> ShareId {
414 let id = self.next_share_id;
415 self.next_share_id += 1;
416 id
417 }
418
419 fn first_valid_schema(&self) -> CatalogResult<&SchemaCatalog> {
420 self.catalog.first_valid_schema(
421 &self.db_name,
422 &self.search_path,
423 &self.auth_context.user_name,
424 )
425 }
426
427 fn bind_schema_path<'a>(&'a self, schema_name: Option<&'a str>) -> SchemaPath<'a> {
428 SchemaPath::new(schema_name, &self.search_path, &self.auth_context.user_name)
429 }
430
431 pub fn set_clause(&mut self, clause: Option<Clause>) {
432 self.context.clause = clause;
433 }
434}
435
436/// The column name stored in [`BindContext`] for a column without an alias.
437pub const UNNAMED_COLUMN: &str = "?column?";
438/// The table name stored in [`BindContext`] for a subquery without an alias.
439const UNNAMED_SUBQUERY: &str = "?subquery?";
440/// The table name stored in [`BindContext`] for a column group.
441const COLUMN_GROUP_PREFIX: &str = "?column_group_id?";
442
443#[cfg(test)]
444pub mod test_utils {
445 use risingwave_common::types::DataType;
446
447 use super::Binder;
448 use crate::session::SessionImpl;
449
450 #[cfg(test)]
451 pub fn mock_binder() -> Binder {
452 Binder::new(&SessionImpl::mock())
453 }
454
455 #[cfg(test)]
456 pub fn mock_binder_with_param_types(param_types: Vec<Option<DataType>>) -> Binder {
457 Binder::new_with_param_types(&SessionImpl::mock(), param_types)
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use expect_test::expect;
464
465 use super::test_utils::*;
466
467 #[tokio::test]
468 async fn test_rcte() {
469 let stmt = risingwave_sqlparser::parser::Parser::parse_sql(
470 "WITH RECURSIVE t1 AS (SELECT 1 AS a UNION ALL SELECT a + 1 FROM t1 WHERE a < 10) SELECT * FROM t1",
471 ).unwrap().into_iter().next().unwrap();
472 let mut binder = mock_binder();
473 let bound = binder.bind(stmt).unwrap();
474
475 let expected = expect![[r#"
476 Query(
477 BoundQuery {
478 body: Select(
479 BoundSelect {
480 distinct: All,
481 select_items: [
482 InputRef(
483 InputRef {
484 index: 0,
485 data_type: Int32,
486 },
487 ),
488 ],
489 aliases: [
490 Some(
491 "a",
492 ),
493 ],
494 from: Some(
495 Share(
496 BoundShare {
497 share_id: 0,
498 input: Query(
499 Right(
500 RecursiveUnion {
501 all: true,
502 base: Select(
503 BoundSelect {
504 distinct: All,
505 select_items: [
506 Literal(
507 Literal {
508 data: Some(
509 Int32(
510 1,
511 ),
512 ),
513 data_type: Some(
514 Int32,
515 ),
516 },
517 ),
518 ],
519 aliases: [
520 Some(
521 "a",
522 ),
523 ],
524 from: None,
525 where_clause: None,
526 group_by: GroupKey(
527 [],
528 ),
529 having: None,
530 window: {},
531 schema: Schema {
532 fields: [
533 a:Int32,
534 ],
535 },
536 },
537 ),
538 recursive: Select(
539 BoundSelect {
540 distinct: All,
541 select_items: [
542 FunctionCall(
543 FunctionCall {
544 func_type: Add,
545 return_type: Int32,
546 inputs: [
547 InputRef(
548 InputRef {
549 index: 0,
550 data_type: Int32,
551 },
552 ),
553 Literal(
554 Literal {
555 data: Some(
556 Int32(
557 1,
558 ),
559 ),
560 data_type: Some(
561 Int32,
562 ),
563 },
564 ),
565 ],
566 },
567 ),
568 ],
569 aliases: [
570 None,
571 ],
572 from: Some(
573 BackCteRef(
574 BoundBackCteRef {
575 share_id: 0,
576 base: Select(
577 BoundSelect {
578 distinct: All,
579 select_items: [
580 Literal(
581 Literal {
582 data: Some(
583 Int32(
584 1,
585 ),
586 ),
587 data_type: Some(
588 Int32,
589 ),
590 },
591 ),
592 ],
593 aliases: [
594 Some(
595 "a",
596 ),
597 ],
598 from: None,
599 where_clause: None,
600 group_by: GroupKey(
601 [],
602 ),
603 having: None,
604 window: {},
605 schema: Schema {
606 fields: [
607 a:Int32,
608 ],
609 },
610 },
611 ),
612 },
613 ),
614 ),
615 where_clause: Some(
616 FunctionCall(
617 FunctionCall {
618 func_type: LessThan,
619 return_type: Boolean,
620 inputs: [
621 InputRef(
622 InputRef {
623 index: 0,
624 data_type: Int32,
625 },
626 ),
627 Literal(
628 Literal {
629 data: Some(
630 Int32(
631 10,
632 ),
633 ),
634 data_type: Some(
635 Int32,
636 ),
637 },
638 ),
639 ],
640 },
641 ),
642 ),
643 group_by: GroupKey(
644 [],
645 ),
646 having: None,
647 window: {},
648 schema: Schema {
649 fields: [
650 ?column?:Int32,
651 ],
652 },
653 },
654 ),
655 schema: Schema {
656 fields: [
657 a:Int32,
658 ],
659 },
660 },
661 ),
662 ),
663 },
664 ),
665 ),
666 where_clause: None,
667 group_by: GroupKey(
668 [],
669 ),
670 having: None,
671 window: {},
672 schema: Schema {
673 fields: [
674 a:Int32,
675 ],
676 },
677 },
678 ),
679 order: [],
680 limit: None,
681 offset: None,
682 with_ties: false,
683 extra_order_exprs: [],
684 },
685 )"#]];
686
687 expected.assert_eq(&format!("{:#?}", bound));
688 }
689
690 #[tokio::test]
691 async fn test_bind_approx_percentile() {
692 let stmt = risingwave_sqlparser::parser::Parser::parse_sql(
693 "SELECT approx_percentile(0.5, 0.01) WITHIN GROUP (ORDER BY generate_series) FROM generate_series(1, 100)",
694 ).unwrap().into_iter().next().unwrap();
695 let parse_expected = expect![[r#"
696 Query(
697 Query {
698 with: None,
699 body: Select(
700 Select {
701 distinct: All,
702 projection: [
703 UnnamedExpr(
704 Function(
705 Function {
706 scalar_as_agg: false,
707 name: ObjectName(
708 [
709 Ident {
710 value: "approx_percentile",
711 quote_style: None,
712 },
713 ],
714 ),
715 arg_list: FunctionArgList {
716 distinct: false,
717 args: [
718 Unnamed(
719 Expr(
720 Value(
721 Number(
722 "0.5",
723 ),
724 ),
725 ),
726 ),
727 Unnamed(
728 Expr(
729 Value(
730 Number(
731 "0.01",
732 ),
733 ),
734 ),
735 ),
736 ],
737 variadic: false,
738 order_by: [],
739 ignore_nulls: false,
740 },
741 within_group: Some(
742 OrderByExpr {
743 expr: Identifier(
744 Ident {
745 value: "generate_series",
746 quote_style: None,
747 },
748 ),
749 asc: None,
750 nulls_first: None,
751 },
752 ),
753 filter: None,
754 over: None,
755 },
756 ),
757 ),
758 ],
759 from: [
760 TableWithJoins {
761 relation: TableFunction {
762 name: ObjectName(
763 [
764 Ident {
765 value: "generate_series",
766 quote_style: None,
767 },
768 ],
769 ),
770 alias: None,
771 args: [
772 Unnamed(
773 Expr(
774 Value(
775 Number(
776 "1",
777 ),
778 ),
779 ),
780 ),
781 Unnamed(
782 Expr(
783 Value(
784 Number(
785 "100",
786 ),
787 ),
788 ),
789 ),
790 ],
791 with_ordinality: false,
792 },
793 joins: [],
794 },
795 ],
796 lateral_views: [],
797 selection: None,
798 group_by: [],
799 having: None,
800 window: [],
801 },
802 ),
803 order_by: [],
804 limit: None,
805 offset: None,
806 fetch: None,
807 },
808 )"#]];
809 parse_expected.assert_eq(&format!("{:#?}", stmt));
810
811 let mut binder = mock_binder();
812 let bound = binder.bind(stmt).unwrap();
813
814 let expected = expect![[r#"
815 Query(
816 BoundQuery {
817 body: Select(
818 BoundSelect {
819 distinct: All,
820 select_items: [
821 AggCall(
822 AggCall {
823 agg_type: Builtin(
824 ApproxPercentile,
825 ),
826 return_type: Float64,
827 args: [
828 FunctionCall(
829 FunctionCall {
830 func_type: Cast,
831 return_type: Float64,
832 inputs: [
833 InputRef(
834 InputRef {
835 index: 0,
836 data_type: Int32,
837 },
838 ),
839 ],
840 },
841 ),
842 ],
843 filter: Condition {
844 conjunctions: [],
845 },
846 distinct: false,
847 order_by: OrderBy {
848 sort_exprs: [
849 OrderByExpr {
850 expr: InputRef(
851 InputRef {
852 index: 0,
853 data_type: Int32,
854 },
855 ),
856 order_type: OrderType {
857 direction: Ascending,
858 nulls_are: Largest,
859 },
860 },
861 ],
862 },
863 direct_args: [
864 Literal {
865 data: Some(
866 Float64(
867 OrderedFloat(
868 0.5,
869 ),
870 ),
871 ),
872 data_type: Some(
873 Float64,
874 ),
875 },
876 Literal {
877 data: Some(
878 Float64(
879 OrderedFloat(
880 0.01,
881 ),
882 ),
883 ),
884 data_type: Some(
885 Float64,
886 ),
887 },
888 ],
889 },
890 ),
891 ],
892 aliases: [
893 Some(
894 "approx_percentile",
895 ),
896 ],
897 from: Some(
898 TableFunction {
899 expr: TableFunction(
900 FunctionCall {
901 function_type: GenerateSeries,
902 return_type: Int32,
903 args: [
904 Literal(
905 Literal {
906 data: Some(
907 Int32(
908 1,
909 ),
910 ),
911 data_type: Some(
912 Int32,
913 ),
914 },
915 ),
916 Literal(
917 Literal {
918 data: Some(
919 Int32(
920 100,
921 ),
922 ),
923 data_type: Some(
924 Int32,
925 ),
926 },
927 ),
928 ],
929 },
930 ),
931 with_ordinality: false,
932 },
933 ),
934 where_clause: None,
935 group_by: GroupKey(
936 [],
937 ),
938 having: None,
939 window: {},
940 schema: Schema {
941 fields: [
942 approx_percentile:Float64,
943 ],
944 },
945 },
946 ),
947 order: [],
948 limit: None,
949 offset: None,
950 with_ties: false,
951 extra_order_exprs: [],
952 },
953 )"#]];
954
955 expected.assert_eq(&format!("{:#?}", bound));
956 }
957}