1use std::collections::BTreeMap;
11use std::fmt;
12
13use mz_expr_derive::sqlfunc;
14use mz_repr::adt::jsonb::{Jsonb, JsonbRef};
15use mz_repr::adt::mz_acl_item::{AclMode, MzAclItem};
16use mz_repr::adt::numeric::{self, Numeric, NumericMaxScale};
17use mz_repr::role_id::RoleId;
18use mz_repr::{ArrayRustType, Datum, Row, RowPacker, SqlColumnType, SqlScalarType, strconv};
19use mz_sql_parser::ast::display::AstDisplay;
20use mz_sql_parser::ast::item_refs::collect_item_references;
21use mz_sql_parser::ast::{
22 AstInfo, AvroSchema, ConnectionOption, ConnectionOptionName, CreateConnectionType,
23 CreateSinkConnection, CreateSubsourceOptionName, Format, FormatSpecifier,
24 IcebergSinkConfigOptionName, IcebergSinkMode, KafkaSinkConfigOptionName,
25 KafkaSourceConfigOptionName, PgConfigOptionName, ProtobufSchema, RawClusterName, RawItemName,
26 SinkEnvelope, SourceEnvelope, SourceErrorPolicy, UnresolvedItemName, Value, WithOptionValue,
27};
28use prost::Message as _;
29use serde::{Deserialize, Serialize};
30use serde_json::json;
31
32use crate::EvalError;
33use crate::scalar::func::EagerUnaryFunc;
34use crate::scalar::func::impls::numeric::*;
35
36#[sqlfunc(
37 sqlname = "jsonb_to_text",
38 preserves_uniqueness = false,
39 inverse = to_unary!(super::CastStringToJsonb)
40)]
41pub fn cast_jsonb_to_string<'a>(a: JsonbRef<'a>) -> String {
42 let mut buf = String::new();
43 strconv::format_jsonb(&mut buf, a);
44 buf
45}
46
47#[sqlfunc(sqlname = "jsonb_to_smallint", is_monotone = true)]
48fn cast_jsonb_to_int16<'a>(a: JsonbRef<'a>) -> Result<i16, EvalError> {
49 match a.into_datum() {
50 Datum::Numeric(a) => cast_numeric_to_int16(a.into_inner()),
51 datum => Err(EvalError::InvalidJsonbCast {
52 from: jsonb_typeof(JsonbRef::from_datum(datum)).into(),
53 to: "smallint".into(),
54 }),
55 }
56}
57
58#[sqlfunc(sqlname = "jsonb_to_integer", is_monotone = true)]
59fn cast_jsonb_to_int32<'a>(a: JsonbRef<'a>) -> Result<i32, EvalError> {
60 match a.into_datum() {
61 Datum::Numeric(a) => cast_numeric_to_int32(a.into_inner()),
62 datum => Err(EvalError::InvalidJsonbCast {
63 from: jsonb_typeof(JsonbRef::from_datum(datum)).into(),
64 to: "integer".into(),
65 }),
66 }
67}
68
69#[sqlfunc(sqlname = "jsonb_to_bigint", is_monotone = true)]
70fn cast_jsonb_to_int64<'a>(a: JsonbRef<'a>) -> Result<i64, EvalError> {
71 match a.into_datum() {
72 Datum::Numeric(a) => cast_numeric_to_int64(a.into_inner()),
73 datum => Err(EvalError::InvalidJsonbCast {
74 from: jsonb_typeof(JsonbRef::from_datum(datum)).into(),
75 to: "bigint".into(),
76 }),
77 }
78}
79
80#[sqlfunc(sqlname = "jsonb_to_real", is_monotone = true)]
81fn cast_jsonb_to_float32<'a>(a: JsonbRef<'a>) -> Result<f32, EvalError> {
82 match a.into_datum() {
83 Datum::Numeric(a) => cast_numeric_to_float32(a.into_inner()),
84 datum => Err(EvalError::InvalidJsonbCast {
85 from: jsonb_typeof(JsonbRef::from_datum(datum)).into(),
86 to: "real".into(),
87 }),
88 }
89}
90
91#[sqlfunc(sqlname = "jsonb_to_double", is_monotone = true)]
92fn cast_jsonb_to_float64<'a>(a: JsonbRef<'a>) -> Result<f64, EvalError> {
93 match a.into_datum() {
94 Datum::Numeric(a) => cast_numeric_to_float64(a.into_inner()),
95 datum => Err(EvalError::InvalidJsonbCast {
96 from: jsonb_typeof(JsonbRef::from_datum(datum)).into(),
97 to: "double precision".into(),
98 }),
99 }
100}
101
102#[derive(
103 Ord,
104 PartialOrd,
105 Clone,
106 Debug,
107 Eq,
108 PartialEq,
109 Serialize,
110 Deserialize,
111 Hash
112)]
113pub struct CastJsonbToNumeric(pub Option<NumericMaxScale>);
114
115impl EagerUnaryFunc for CastJsonbToNumeric {
116 type Input<'a> = JsonbRef<'a>;
117 type Output<'a> = Result<Numeric, EvalError>;
118
119 fn call<'a>(&self, a: Self::Input<'a>) -> Self::Output<'a> {
120 match a.into_datum() {
121 Datum::Numeric(mut num) => match self.0 {
122 None => Ok(num.into_inner()),
123 Some(scale) => {
124 if numeric::rescale(&mut num.0, scale.into_u8()).is_err() {
125 return Err(EvalError::NumericFieldOverflow);
126 };
127 Ok(num.into_inner())
128 }
129 },
130 datum => Err(EvalError::InvalidJsonbCast {
131 from: jsonb_typeof(JsonbRef::from_datum(datum)).into(),
132 to: "numeric".into(),
133 }),
134 }
135 }
136
137 fn output_sql_type(&self, input: SqlColumnType) -> SqlColumnType {
138 SqlScalarType::Numeric { max_scale: self.0 }.nullable(input.nullable)
139 }
140
141 fn is_monotone(&self) -> bool {
142 true
143 }
144}
145
146impl fmt::Display for CastJsonbToNumeric {
147 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
148 f.write_str("jsonb_to_numeric")
149 }
150}
151
152#[sqlfunc(sqlname = "jsonb_to_boolean", is_monotone = true)]
153fn cast_jsonb_to_bool<'a>(a: JsonbRef<'a>) -> Result<bool, EvalError> {
154 match a.into_datum() {
155 Datum::True => Ok(true),
156 Datum::False => Ok(false),
157 datum => Err(EvalError::InvalidJsonbCast {
158 from: jsonb_typeof(JsonbRef::from_datum(datum)).into(),
159 to: "boolean".into(),
160 }),
161 }
162}
163
164#[sqlfunc(sqlname = "jsonbable_to_jsonb")]
165fn cast_jsonbable_to_jsonb<'a>(a: JsonbRef<'a>) -> JsonbRef<'a> {
166 match a.into_datum() {
167 Datum::Numeric(n) => {
168 let n = n.into_inner();
169 let datum = if n.is_finite() {
170 Datum::from(n)
171 } else if n.is_nan() {
172 Datum::String("NaN")
173 } else if n.is_negative() {
174 Datum::String("-Infinity")
175 } else {
176 Datum::String("Infinity")
177 };
178 JsonbRef::from_datum(datum)
179 }
180 datum => JsonbRef::from_datum(datum),
181 }
182}
183
184#[sqlfunc]
185fn jsonb_array_length<'a>(a: JsonbRef<'a>) -> Result<Option<i32>, EvalError> {
186 match a.into_datum() {
187 Datum::List(list) => {
188 let count = list.iter().count();
189 match i32::try_from(count) {
190 Ok(len) => Ok(Some(len)),
191 Err(_) => Err(EvalError::Int32OutOfRange(count.to_string().into())),
192 }
193 }
194 _ => Ok(None),
195 }
196}
197
198#[sqlfunc]
199fn jsonb_typeof<'a>(a: JsonbRef<'a>) -> &'a str {
200 match a.into_datum() {
201 Datum::Map(_) => "object",
202 Datum::List(_) => "array",
203 Datum::String(_) => "string",
204 Datum::Numeric(_) => "number",
205 Datum::True | Datum::False => "boolean",
206 Datum::JsonNull => "null",
207 d => panic!("Not jsonb: {:?}", d),
208 }
209}
210
211#[sqlfunc]
212fn jsonb_strip_nulls<'a>(a: JsonbRef<'a>) -> Jsonb {
213 fn strip_nulls(a: Datum, row: &mut RowPacker) {
214 match a {
215 Datum::Map(dict) => row.push_dict_with(|row| {
216 for (k, v) in dict.iter() {
217 match v {
218 Datum::JsonNull => (),
219 _ => {
220 row.push(Datum::String(k));
221 strip_nulls(v, row);
222 }
223 }
224 }
225 }),
226 Datum::List(list) => row.push_list_with(|row| {
227 for elem in list.iter() {
228 strip_nulls(elem, row);
229 }
230 }),
231 _ => row.push(a),
232 }
233 }
234 let mut row = Row::default();
235 strip_nulls(a.into_datum(), &mut row.packer());
236 Jsonb::from_row(row)
237}
238
239#[sqlfunc]
241fn jsonb_pretty<'a>(a: JsonbRef<'a>) -> String {
242 let mut buf = String::new();
243 strconv::format_jsonb_pretty(&mut buf, a);
244 buf
245}
246
247fn jsonb_datum_to_u64<'a>(d: Datum<'a>) -> Result<u64, String> {
249 let Datum::Numeric(n) = d else {
250 return Err("expected numeric value".into());
251 };
252
253 let mut cx = numeric::cx_datum();
254 cx.try_into_u64(n.0)
255 .map_err(|_| format!("number out of u64 range: {n}"))
256}
257
258fn jsonb_datum_to_acl_mode(d: Datum) -> Result<AclMode, String> {
264 let Datum::Map(dict) = d else {
265 return Err(format!("unexpected acl_mode: {d}"));
266 };
267 let mut bits = None;
268 for (key, val) in dict.iter() {
269 match key {
270 "bitflags" => bits = Some(jsonb_datum_to_u64(val)?),
271 other => return Err(format!("unexpected acl_mode field: {other}")),
272 }
273 }
274 let bits = bits.ok_or_else(|| "missing acl_mode bitflags".to_string())?;
275 AclMode::from_bits(bits).ok_or_else(|| format!("invalid acl_mode bitflags: {bits}"))
276}
277
278fn jsonb_datum_to_role_id(d: Datum) -> Result<RoleId, String> {
280 match d {
281 Datum::String("Public") => Ok(RoleId::Public),
282 Datum::String(other) => Err(format!("unexpected role ID variant: {other}")),
283 Datum::Map(dict) => {
284 let (key, val) = dict.iter().next().ok_or_else(|| "empty".to_string())?;
285 let n = jsonb_datum_to_u64(val)?;
286 match key {
287 "User" => Ok(RoleId::User(n)),
288 "System" => Ok(RoleId::System(n)),
289 "Predefined" => Ok(RoleId::Predefined(n)),
290 other => Err(format!("unexpected role ID variant: {other}")),
291 }
292 }
293 _ => Err("expected string or object".into()),
294 }
295}
296
297#[sqlfunc]
301fn parse_catalog_id<'a>(a: JsonbRef<'a>) -> Result<String, EvalError> {
302 let parse = || match a.into_datum() {
303 Datum::String(variant) => match variant {
305 "Explain" => Ok("e".to_string()),
306 "Public" => Ok("p".to_string()),
307 other => Err(format!("unexpected ID variant: {other}")),
308 },
309 Datum::Map(dict) => {
311 let (key, val) = dict.iter().next().ok_or_else(|| "empty".to_string())?;
312 let prefix = match key {
313 "IntrospectionSourceIndex" => "si",
314 "Predefined" => "g",
315 "System" => "s",
316 "Transient" => "t",
317 "User" => "u",
318 other => return Err(format!("unexpected ID variant: {other}")),
319 };
320 let n = jsonb_datum_to_u64(val)?;
321 Ok(format!("{prefix}{n}"))
322 }
323 _ => Err("expected string or object".into()),
324 };
325
326 parse().map_err(|e| EvalError::InvalidCatalogJson(e.into()))
327}
328
329#[sqlfunc]
331fn parse_catalog_privileges<'a>(a: JsonbRef<'a>) -> Result<ArrayRustType<MzAclItem>, EvalError> {
332 let parse_one = |datum| match datum {
333 Datum::Map(dict) => {
334 let mut grantee = None;
335 let mut grantor = None;
336 let mut acl_mode = None;
337 for (key, val) in dict.iter() {
338 match key {
339 "grantee" => {
340 let id = jsonb_datum_to_role_id(val)?;
341 grantee = Some(id);
342 }
343 "grantor" => {
344 let id = jsonb_datum_to_role_id(val)?;
345 grantor = Some(id);
346 }
347 "acl_mode" => {
348 acl_mode = Some(jsonb_datum_to_acl_mode(val)?);
349 }
350 other => return Err(format!("unexpected privilege field: {other}")),
351 }
352 }
353 Ok(MzAclItem {
354 grantee: grantee.ok_or_else(|| format!("missing grantee: {dict:?}"))?,
355 grantor: grantor.ok_or_else(|| "missing grantor in privilege".to_string())?,
356 acl_mode: acl_mode.ok_or_else(|| "missing acl_mode in privilege".to_string())?,
357 })
358 }
359 other => Err(format!("expected object in array, found: {other}")),
360 };
361
362 let parse = || match a.into_datum() {
363 Datum::List(list) => {
364 let mut result = Vec::new();
365 for item in list.iter() {
366 result.push(parse_one(item)?);
367 }
368 Ok(result)
369 }
370 _ => Err("expected array".to_string()),
371 };
372
373 parse()
374 .map(ArrayRustType)
375 .map_err(|e| EvalError::InvalidCatalogJson(e.into()))
376}
377
378#[sqlfunc]
381fn parse_catalog_acl_mode<'a>(a: JsonbRef<'a>) -> Result<String, EvalError> {
382 jsonb_datum_to_acl_mode(a.into_datum())
383 .map(|mode| mode.to_string())
384 .map_err(|e| EvalError::InvalidCatalogJson(e.into()))
385}
386
387fn option_string<T: AstInfo>(value: &WithOptionValue<T>) -> Option<String> {
398 match value {
399 WithOptionValue::Value(Value::String(s)) => Some(s.clone()),
400 WithOptionValue::Ident(ident) => Some(ident.clone().into_string()),
401 WithOptionValue::UnresolvedItemName(UnresolvedItemName(parts)) if parts.len() == 1 => {
402 Some(parts[0].clone().into_string())
403 }
404 _ => None,
405 }
406}
407
408#[sqlfunc]
415fn parse_catalog_create_sql<'a>(a: &'a str) -> Result<Jsonb, EvalError> {
416 fn get_cluster_id(in_cluster: RawClusterName) -> Result<String, &'static str> {
417 match in_cluster {
418 RawClusterName::Resolved(s) => Ok(s),
419 RawClusterName::Unresolved(_) => Err("unresolved cluster name"),
420 }
421 }
422
423 fn get_item_id(item: RawItemName) -> Result<String, &'static str> {
424 match item {
425 RawItemName::Id(id, _, _) => Ok(id),
426 RawItemName::Name(_) => Err("unresolved item name"),
427 }
428 }
429
430 fn format_name<T: AstInfo>(fmt: &Format<T>) -> &'static str {
431 match fmt {
432 Format::Bytes => "bytes",
433 Format::Avro(_) => "avro",
434 Format::Protobuf(_) => "protobuf",
435 Format::Regex(_) => "regex",
436 Format::Csv { .. } => "csv",
437 Format::Json { .. } => "json",
438 Format::Text => "text",
439 }
440 }
441
442 let parse = || -> Result<serde_json::Value, String> {
443 let mut stmts = mz_sql_parser::parser::parse_statements(a)
444 .map_err(|e| format!("failed to parse create_sql: {e}"))?;
445 let stmt = match stmts.len() {
446 1 => stmts.remove(0).ast,
447 n => return Err(format!("expected a single statement, found {n}")),
448 };
449
450 let mut info = BTreeMap::<&str, serde_json::Value>::new();
451
452 use mz_sql_parser::ast::Statement::*;
453 let item_type = match stmt {
454 CreateSecret(_) => "secret",
455 CreateConnection(stmt) => {
456 let connection_type = stmt.connection_type.as_str();
457 info.insert("connection_type", json!(connection_type));
458
459 "connection"
460 }
461 CreateView(stmt) => {
462 let mut definition = stmt.definition.query.to_ast_string_stable();
463 definition.push(';');
466 info.insert("definition", json!(definition));
467
468 "view"
469 }
470 CreateMaterializedView(stmt) => {
471 let Some(in_cluster) = stmt.in_cluster else {
472 return Err("missing IN CLUSTER".into());
473 };
474 let cluster_id = match in_cluster {
475 RawClusterName::Unresolved(ident) => ident.into_string(),
476 RawClusterName::Resolved(s) => s,
477 };
478 info.insert("cluster_id", json!(cluster_id));
479
480 let mut definition = stmt.query.to_ast_string_stable();
481 definition.push(';');
482 info.insert("definition", json!(definition));
483
484 "materialized-view"
485 }
486 CreateTable(_) => "table",
487 CreateTableFromSource(stmt) => {
488 let source_id = get_item_id(stmt.source)?;
489 info.insert("source_id", json!(source_id));
490
491 "table"
492 }
493 CreateSource(stmt) => {
494 let Some(in_cluster) = stmt.in_cluster else {
495 return Err("missing IN CLUSTER".into());
496 };
497 let cluster_id = get_cluster_id(in_cluster)?;
498 info.insert("cluster_id", json!(cluster_id));
499
500 use mz_sql_parser::ast::CreateSourceConnection::*;
501 let (source_type, connection) = match stmt.connection {
502 Kafka { connection, .. } => ("kafka", Some(connection)),
503 Postgres { connection, .. } => ("postgres", Some(connection)),
504 MySql { connection, .. } => ("mysql", Some(connection)),
505 SqlServer { connection, .. } => ("sql-server", Some(connection)),
506 LoadGenerator { .. } => ("load-generator", None),
507 };
508 info.insert("source_type", json!(source_type));
509 if let Some(conn) = connection {
510 let conn_id = get_item_id(conn)?;
511 info.insert("connection_id", json!(conn_id));
512 }
513
514 let is_debezium = matches!(
515 stmt.envelope,
516 Some(mz_sql_parser::ast::SourceEnvelope::Debezium)
517 );
518
519 let envelope_type = match &stmt.envelope {
530 Some(envelope) => {
531 use mz_sql_parser::ast::SourceEnvelope::*;
532 Some(match envelope {
533 None => "none",
534 Debezium => "debezium",
535 Upsert { .. } => "upsert",
536 CdcV2 => "materialize",
537 })
538 }
539 None if source_type == "kafka" && stmt.progress_subsource.is_some() => {
540 Some("none")
541 }
542 None => None,
543 };
544 if let Some(envelope_type) = envelope_type {
545 info.insert("envelope_type", json!(envelope_type));
546 }
547
548 if let Some(format_spec) = stmt.format {
549 match &format_spec {
550 FormatSpecifier::Bare(fmt) => {
551 if is_debezium {
554 info.insert("key_format", json!(format_name(fmt)));
555 }
556 info.insert("value_format", json!(format_name(fmt)));
557 }
558 FormatSpecifier::KeyValue { key, value } => {
559 info.insert("key_format", json!(format_name(key)));
560 info.insert("value_format", json!(format_name(value)));
561 }
562 }
563 }
564
565 "source"
566 }
567 CreateWebhookSource(stmt) => {
568 if stmt.is_table {
569 "table"
570 } else {
571 info.insert("source_type", json!("webhook"));
572 if let Some(in_cluster) = stmt.in_cluster {
573 let cluster_id = get_cluster_id(in_cluster)?;
574 info.insert("cluster_id", json!(cluster_id));
575 }
576 "source"
577 }
578 }
579 CreateSubsource(stmt) => {
580 use mz_sql_parser::ast::CreateSubsourceOptionName;
581 let is_progress = stmt
582 .with_options
583 .iter()
584 .any(|o| matches!(o.name, CreateSubsourceOptionName::Progress));
585 let source_type = if is_progress { "progress" } else { "subsource" };
586 info.insert("source_type", json!(source_type));
587
588 if let Some(of_source) = stmt.of_source {
589 let of_source_id = get_item_id(of_source)?;
590 info.insert("of_source_id", json!(of_source_id));
591 }
592
593 "subsource"
594 }
595 CreateSink(stmt) => {
605 let Some(in_cluster) = stmt.in_cluster else {
606 return Err("missing IN CLUSTER".into());
607 };
608 info.insert("cluster_id", json!(get_cluster_id(in_cluster)?));
609
610 match stmt.connection {
611 CreateSinkConnection::Kafka {
612 connection,
613 options,
614 key: sink_key,
615 ..
616 } => {
617 info.insert("sink_type", json!("kafka"));
618 info.insert("connection_id", json!(get_item_id(connection)?));
619
620 let topic = options
621 .into_iter()
622 .find(|o| o.name == KafkaSinkConfigOptionName::Topic)
623 .and_then(|o| o.value.as_ref().and_then(option_string))
624 .ok_or("kafka sink missing TOPIC")?;
625 info.insert("topic", json!(topic));
626
627 if let Some(envelope) = stmt.envelope {
628 let envelope_type = match envelope {
629 SinkEnvelope::Upsert => "upsert",
630 SinkEnvelope::Debezium => "debezium",
631 };
632 info.insert("envelope_type", json!(envelope_type));
633 }
634
635 if let Some(format_spec) = stmt.format {
636 let (key_format, value_format) =
641 match (&format_spec, sink_key.is_some()) {
642 (FormatSpecifier::Bare(fmt), false) => (None, format_name(fmt)),
643 (FormatSpecifier::Bare(fmt), true) => {
644 (Some(format_name(fmt)), format_name(fmt))
645 }
646 (FormatSpecifier::KeyValue { value, .. }, false) => {
647 (None, format_name(value))
648 }
649 (FormatSpecifier::KeyValue { key, value }, true) => {
650 (Some(format_name(key)), format_name(value))
651 }
652 };
653 if let Some(key_format) = key_format {
654 info.insert("key_format", json!(key_format));
655 }
656 info.insert("value_format", json!(value_format));
657
658 let combined = match key_format {
663 None => value_format.to_string(),
664 Some(key_format)
665 if key_format == value_format
666 && matches!(value_format, "avro" | "json") =>
667 {
668 value_format.to_string()
669 }
670 Some(key_format) => {
671 format!("key-{key_format}-value-{value_format}")
672 }
673 };
674 info.insert("format", json!(combined));
675 }
676 }
677 CreateSinkConnection::Iceberg {
678 catalog_connection,
679 options,
680 ..
681 } => {
682 info.insert("sink_type", json!("iceberg"));
683 info.insert("connection_id", json!(get_item_id(catalog_connection)?));
685
686 let mut namespace = None;
687 let mut table = None;
688 for option in options {
689 match option.name {
690 IcebergSinkConfigOptionName::Namespace => {
691 namespace = option.value.as_ref().and_then(option_string)
692 }
693 IcebergSinkConfigOptionName::Table => {
694 table = option.value.as_ref().and_then(option_string)
695 }
696 }
697 }
698 info.insert(
699 "namespace",
700 json!(namespace.ok_or("iceberg sink missing NAMESPACE")?),
701 );
702 info.insert("table", json!(table.ok_or("iceberg sink missing TABLE")?));
703
704 if let Some(mode) = stmt.mode {
707 let envelope_type = match mode {
708 IcebergSinkMode::Upsert => "upsert",
709 IcebergSinkMode::Append => "append",
710 };
711 info.insert("envelope_type", json!(envelope_type));
712 }
713 }
714 }
715
716 "sink"
717 }
718 CreateMetricSink(stmt) => {
719 let Some(in_cluster) = stmt.in_cluster else {
720 return Err("missing IN CLUSTER".into());
721 };
722 let cluster_id = get_cluster_id(in_cluster)?;
723 info.insert("cluster_id", json!(cluster_id));
724 let from_id = get_item_id(stmt.from)?;
725 info.insert("from_id", json!(from_id));
726 "metric-sink"
727 }
728 CreateIndex(stmt) => {
729 let Some(in_cluster) = stmt.in_cluster else {
730 return Err("missing IN CLUSTER".into());
731 };
732 let cluster_id = get_cluster_id(in_cluster)?;
733 info.insert("cluster_id", json!(cluster_id));
734 let on_id = get_item_id(stmt.on_name)?;
735 info.insert("on_id", json!(on_id));
736 "index"
737 }
738 CreateType(_) => "type",
739 Select(_)
745 | Insert(_)
746 | Copy(_)
747 | Update(_)
748 | Delete(_)
749 | CreateDatabase(_)
750 | CreateSchema(_)
751 | CreateRole(_)
752 | CreateCluster(_)
753 | CreateClusterReplica(_)
754 | CreateNetworkPolicy(_)
755 | AlterCluster(_)
756 | AlterOwner(_)
757 | AlterObjectRename(_)
758 | AlterObjectSwap(_)
759 | AlterRetainHistory(_)
760 | AlterIndex(_)
761 | AlterSecret(_)
762 | AlterSetCluster(_)
763 | AlterSink(_)
764 | AlterSource(_)
765 | AlterSystemSet(_)
766 | AlterSystemReset(_)
767 | AlterSystemResetAll(_)
768 | AlterConnection(_)
769 | AlterNetworkPolicy(_)
770 | AlterRole(_)
771 | AlterTableAddColumn(_)
772 | AlterMaterializedViewApplyReplacement(_)
773 | Discard(_)
774 | DropObjects(_)
775 | DropOwned(_)
776 | SetVariable(_)
777 | ResetVariable(_)
778 | Show(_)
779 | StartTransaction(_)
780 | SetTransaction(_)
781 | Commit(_)
782 | Rollback(_)
783 | Subscribe(_)
784 | ExplainPlan(_)
785 | ExplainPushdown(_)
786 | ExplainTimestamp(_)
787 | ExplainSinkSchema(_)
788 | ExplainAnalyzeObject(_)
789 | ExplainAnalyzeCluster(_)
790 | Declare(_)
791 | Fetch(_)
792 | Close(_)
793 | Prepare(_)
794 | Execute(_)
795 | ExecuteUnitTest(_)
796 | Deallocate(_)
797 | Raise(_)
798 | GrantRole(_)
799 | RevokeRole(_)
800 | GrantPrivileges(_)
801 | RevokePrivileges(_)
802 | AlterDefaultPrivileges(_)
803 | ReassignOwned(_)
804 | ValidateConnection(_)
805 | Comment(_) => return Err("not a CREATE item statement".into()),
806 };
807 info.insert("type", json!(item_type));
808
809 let info = info.into_iter().map(|(k, v)| (k.to_string(), v)).collect();
810 Ok(info)
811 };
812
813 let val = parse().map_err(|e| EvalError::InvalidCatalogJson(e.into()))?;
814 let jsonb = Jsonb::from_serde_json(val).expect("valid JSONB");
815 Ok(jsonb)
816}
817
818#[sqlfunc]
826fn parse_catalog_item_references<'a>(a: &'a str) -> Result<Jsonb, EvalError> {
827 fn qualified(name: &UnresolvedItemName) -> serde_json::Value {
828 match &name.0[..] {
829 [.., schema, item] => json!({"schema": schema.as_str(), "name": item.as_str()}),
830 [item] => json!({"schema": serde_json::Value::Null, "name": item.as_str()}),
831 [] => json!({"schema": serde_json::Value::Null, "name": ""}),
832 }
833 }
834
835 let parse = || -> Result<serde_json::Value, String> {
836 let mut stmts = mz_sql_parser::parser::parse_statements(a)
837 .map_err(|e| format!("failed to parse create_sql: {e}"))?;
838 let stmt = match stmts.len() {
839 1 => stmts.remove(0).ast,
840 n => return Err(format!("expected a single statement, found {n}")),
841 };
842
843 let refs = collect_item_references(&stmt);
844 mz_ore::soft_assert_or_log!(
845 refs.named_array_elements.is_empty(),
846 "persisted create_sql should never carry an array type T[] \
847 and resolve its array type in `ids`: {:?}",
848 refs.named_array_elements
849 );
850 Ok(json!({
851 "ids": refs.ids.iter().collect::<Vec<_>>(),
852 "named_funcs": refs.named_funcs.iter().map(qualified).collect::<Vec<_>>(),
853 "named_types": refs.named_types.iter().map(qualified).collect::<Vec<_>>(),
854 "named_relations": refs.named_relations.iter().map(qualified).collect::<Vec<_>>(),
855 }))
856 };
857
858 let val = parse().map_err(|e| EvalError::InvalidCatalogJson(e.into()))?;
859 let jsonb = Jsonb::from_serde_json(val).expect("valid JSONB");
860 Ok(jsonb)
861}
862
863#[derive(Clone, PartialEq, ::prost::Message)]
869struct PostgresPublicationDetailsSubset {
870 #[prost(string, tag = "2")]
871 slot: String,
872 #[prost(uint64, optional, tag = "3")]
873 timeline_id: Option<u64>,
874}
875
876#[sqlfunc]
887fn parse_postgres_source_details<'a>(a: &'a str) -> Result<Jsonb, EvalError> {
888 let parse = || -> Result<serde_json::Value, String> {
889 let mut stmts = mz_sql_parser::parser::parse_statements(a)
890 .map_err(|e| format!("failed to parse create_sql: {e}"))?;
891 let stmt = match stmts.len() {
892 1 => stmts.remove(0).ast,
893 n => return Err(format!("expected a single statement, found {n}")),
894 };
895
896 use mz_sql_parser::ast::CreateSourceConnection;
897 use mz_sql_parser::ast::Statement::CreateSource;
898 let options = match stmt {
899 CreateSource(stmt) => match stmt.connection {
900 CreateSourceConnection::Postgres { options, .. } => options,
901 _ => return Ok(serde_json::Value::Null),
902 },
903 _ => return Ok(serde_json::Value::Null),
904 };
905
906 let details_hex = options
907 .into_iter()
908 .find(|opt| opt.name == PgConfigOptionName::Details)
909 .and_then(|opt| match opt.value {
910 Some(WithOptionValue::Value(Value::String(s))) => Some(s),
911 _ => None,
912 })
913 .ok_or("missing DETAILS option on postgres source")?;
914
915 let details_bytes =
916 hex::decode(&details_hex).map_err(|e| format!("DETAILS is not valid hex: {e}"))?;
917
918 let details = PostgresPublicationDetailsSubset::decode(&*details_bytes)
919 .map_err(|e| format!("DETAILS is not a valid publication-details proto: {e}"))?;
920
921 Ok(json!({
922 "slot": details.slot,
923 "timeline_id": details.timeline_id,
924 }))
925 };
926
927 let val = parse().map_err(|e| EvalError::InvalidCatalogJson(e.into()))?;
928 let jsonb = Jsonb::from_serde_json(val).expect("valid JSONB");
929 Ok(jsonb)
930}
931
932#[sqlfunc]
944fn parse_kafka_source_details<'a>(a: &'a str) -> Result<Jsonb, EvalError> {
945 fn get_item_id(item: RawItemName) -> Result<String, &'static str> {
946 match item {
947 RawItemName::Id(id, _, _) => Ok(id),
948 RawItemName::Name(_) => Err("unresolved item name"),
949 }
950 }
951
952 let parse = || -> Result<serde_json::Value, String> {
953 let mut stmts = mz_sql_parser::parser::parse_statements(a)
954 .map_err(|e| format!("failed to parse create_sql: {e}"))?;
955 let stmt = match stmts.len() {
956 1 => stmts.remove(0).ast,
957 n => return Err(format!("expected a single statement, found {n}")),
958 };
959
960 use mz_sql_parser::ast::CreateSourceConnection;
961 use mz_sql_parser::ast::Statement::CreateSource;
962 let (connection, options) = match stmt {
963 CreateSource(stmt) => match stmt.connection {
964 CreateSourceConnection::Kafka {
965 connection,
966 options,
967 } => (connection, options),
968 _ => return Ok(serde_json::Value::Null),
969 },
970 _ => return Ok(serde_json::Value::Null),
971 };
972
973 let connection_id = get_item_id(connection)?;
974
975 let mut topic: Option<String> = None;
976 let mut group_id_prefix: Option<String> = None;
977 for opt in options {
978 let string_value = opt.value.as_ref().and_then(option_string);
979 match opt.name {
980 KafkaSourceConfigOptionName::Topic => topic = string_value,
981 KafkaSourceConfigOptionName::GroupIdPrefix => group_id_prefix = string_value,
982 _ => {}
983 }
984 }
985
986 let topic = topic.ok_or("missing TOPIC option on kafka source")?;
987
988 Ok(json!({
989 "topic": topic,
990 "group_id_prefix": group_id_prefix,
991 "connection_id": connection_id,
992 }))
993 };
994
995 let val = parse().map_err(|e| EvalError::InvalidCatalogJson(e.into()))?;
996 let jsonb = Jsonb::from_serde_json(val).expect("valid JSONB");
997 Ok(jsonb)
998}
999
1000#[sqlfunc]
1035fn parse_source_export_details<'a>(a: &'a str) -> Result<Jsonb, EvalError> {
1036 fn get_item_id(item: RawItemName) -> Result<String, &'static str> {
1037 match item {
1038 RawItemName::Id(id, _, _) => Ok(id),
1039 RawItemName::Name(_) => Err("unresolved item name"),
1040 }
1041 }
1042
1043 fn format_name<T: AstInfo>(fmt: &Format<T>) -> &'static str {
1044 match fmt {
1045 Format::Bytes => "bytes",
1046 Format::Avro(_) => "avro",
1047 Format::Protobuf(_) => "protobuf",
1048 Format::Regex(_) => "regex",
1049 Format::Csv { .. } => "csv",
1050 Format::Json { .. } => "json",
1051 Format::Text => "text",
1052 }
1053 }
1054
1055 fn bare_format_has_key<T: AstInfo>(fmt: &Format<T>) -> bool {
1059 match fmt {
1060 Format::Avro(AvroSchema::Csr { csr_connection }) => csr_connection
1061 .seed
1062 .as_ref()
1063 .is_some_and(|seed| seed.key_schema.is_some()),
1064 Format::Protobuf(ProtobufSchema::Csr { csr_connection }) => csr_connection
1065 .seed
1066 .as_ref()
1067 .is_some_and(|seed| seed.key.is_some()),
1068 _ => false,
1069 }
1070 }
1071
1072 fn key_value_formats<T: AstInfo>(
1073 spec: &FormatSpecifier<T>,
1074 ) -> (Option<&'static str>, Option<&'static str>) {
1075 match spec {
1076 FormatSpecifier::KeyValue { key, value } => {
1077 (Some(format_name(key)), Some(format_name(value)))
1078 }
1079 FormatSpecifier::Bare(fmt) => {
1080 let value = Some(format_name(fmt));
1081 let key = bare_format_has_key(fmt).then(|| format_name(fmt));
1082 (key, value)
1083 }
1084 }
1085 }
1086
1087 fn envelope_name(envelope: &SourceEnvelope) -> &'static str {
1088 match envelope {
1089 SourceEnvelope::None => "none",
1090 SourceEnvelope::Debezium => "debezium",
1091 SourceEnvelope::Upsert {
1092 value_decode_err_policy,
1093 } => {
1094 if value_decode_err_policy
1095 .iter()
1096 .any(|p| matches!(p, SourceErrorPolicy::Inline { .. }))
1097 {
1098 "upsert-value-err-inline"
1099 } else {
1100 "upsert"
1101 }
1102 }
1103 SourceEnvelope::CdcV2 => "materialize",
1104 }
1105 }
1106
1107 let parse = || -> Result<serde_json::Value, String> {
1108 let mut stmts = mz_sql_parser::parser::parse_statements(a)
1109 .map_err(|e| format!("failed to parse create_sql: {e}"))?;
1110 let stmt = match stmts.len() {
1111 1 => stmts.remove(0).ast,
1112 n => return Err(format!("expected a single statement, found {n}")),
1113 };
1114
1115 use mz_sql_parser::ast::Statement::{CreateSubsource, CreateTableFromSource};
1116 match stmt {
1117 CreateTableFromSource(stmt) => {
1118 let source_id = get_item_id(stmt.source)?;
1119 let external_reference = stmt
1120 .external_reference
1121 .ok_or("missing external reference on CREATE TABLE FROM SOURCE")?
1122 .0
1123 .into_iter()
1124 .map(|ident| ident.into_string())
1125 .collect::<Vec<_>>();
1126
1127 let envelope_type = stmt.envelope.as_ref().map(envelope_name);
1128 let (key_format, value_format) = match &stmt.format {
1129 Some(spec) => key_value_formats(spec),
1130 None => (None, None),
1131 };
1132
1133 Ok(json!({
1134 "source_id": source_id,
1135 "external_reference": external_reference,
1136 "envelope_type": envelope_type,
1137 "key_format": key_format,
1138 "value_format": value_format,
1139 }))
1140 }
1141 CreateSubsource(stmt) => {
1142 let is_progress = stmt
1145 .with_options
1146 .iter()
1147 .any(|o| matches!(o.name, CreateSubsourceOptionName::Progress));
1148 if is_progress {
1149 return Ok(serde_json::Value::Null);
1150 }
1151
1152 let source_id = stmt
1153 .of_source
1154 .ok_or("non-progress CREATE SUBSOURCE without OF SOURCE")
1155 .and_then(get_item_id)?;
1156
1157 let external_reference = stmt
1158 .with_options
1159 .into_iter()
1160 .find(|o| matches!(o.name, CreateSubsourceOptionName::ExternalReference))
1161 .and_then(|o| match o.value {
1162 Some(WithOptionValue::UnresolvedItemName(name)) => Some(name),
1163 _ => None,
1164 })
1165 .ok_or("CREATE SUBSOURCE missing EXTERNAL REFERENCE option")?
1166 .0
1167 .into_iter()
1168 .map(|ident| ident.into_string())
1169 .collect::<Vec<_>>();
1170
1171 Ok(json!({
1172 "source_id": source_id,
1173 "external_reference": external_reference,
1174 "envelope_type": serde_json::Value::Null,
1175 "key_format": serde_json::Value::Null,
1176 "value_format": serde_json::Value::Null,
1177 }))
1178 }
1179 _ => Ok(serde_json::Value::Null),
1180 }
1181 };
1182
1183 let val = parse().map_err(|e| EvalError::InvalidCatalogJson(e.into()))?;
1184 let jsonb = Jsonb::from_serde_json(val).expect("valid JSONB");
1185 Ok(jsonb)
1186}
1187
1188#[sqlfunc]
1233fn parse_connection_details<'a>(a: &'a str) -> Result<Jsonb, EvalError> {
1234 fn broker_addresses<T: AstInfo>(values: &[ConnectionOption<T>]) -> Vec<String> {
1239 let mut brokers = Vec::new();
1240 for opt in values {
1241 match (&opt.name, &opt.value) {
1242 (ConnectionOptionName::Broker, Some(WithOptionValue::ConnectionKafkaBroker(b))) => {
1243 brokers.push(b.address.clone());
1244 }
1245 (ConnectionOptionName::Brokers, Some(WithOptionValue::Sequence(seq))) => {
1246 for v in seq {
1247 if let WithOptionValue::ConnectionKafkaBroker(b) = v {
1248 brokers.push(b.address.clone());
1249 }
1250 }
1251 }
1252 _ => {}
1253 }
1254 }
1255 brokers
1256 }
1257
1258 fn string_option<T: AstInfo>(
1259 values: &[ConnectionOption<T>],
1260 name: ConnectionOptionName,
1261 ) -> Option<String> {
1262 values
1263 .iter()
1264 .find(|o| o.name == name)
1265 .and_then(|o| o.value.as_ref())
1266 .and_then(option_string)
1267 }
1268
1269 fn secret_id_option<T: AstInfo<ItemName = RawItemName>>(
1273 values: &[ConnectionOption<T>],
1274 name: ConnectionOptionName,
1275 ) -> Option<String> {
1276 values.iter().find_map(|o| match &o.value {
1277 Some(WithOptionValue::Secret(RawItemName::Id(id, _, _))) if o.name == name => {
1278 Some(id.clone())
1279 }
1280 _ => None,
1281 })
1282 }
1283
1284 let parse = || -> Result<serde_json::Value, String> {
1285 let mut stmts = mz_sql_parser::parser::parse_statements(a)
1286 .map_err(|e| format!("failed to parse create_sql: {e}"))?;
1287 let stmt = match stmts.len() {
1288 1 => stmts.remove(0).ast,
1289 n => return Err(format!("expected a single statement, found {n}")),
1290 };
1291
1292 use mz_sql_parser::ast::Statement::CreateConnection;
1293 let stmt = match stmt {
1294 CreateConnection(stmt) => stmt,
1295 _ => return Ok(serde_json::Value::Null),
1296 };
1297
1298 match stmt.connection_type {
1299 CreateConnectionType::Kafka => Ok(json!({
1300 "brokers": broker_addresses(&stmt.values),
1301 "progress_topic": string_option(&stmt.values, ConnectionOptionName::ProgressTopic),
1302 })),
1303 CreateConnectionType::Ssh => Ok(json!({
1304 "public_key_1": string_option(&stmt.values, ConnectionOptionName::PublicKey1),
1305 "public_key_2": string_option(&stmt.values, ConnectionOptionName::PublicKey2),
1306 })),
1307 CreateConnectionType::Aws => {
1308 let assume_role_arn =
1309 string_option(&stmt.values, ConnectionOptionName::AssumeRoleArn);
1310 let auth_kind = if assume_role_arn.is_some() {
1311 "assume-role"
1312 } else {
1313 "credentials"
1314 };
1315 Ok(json!({
1316 "auth_kind": auth_kind,
1317 "endpoint": string_option(&stmt.values, ConnectionOptionName::Endpoint)
1321 .filter(|s| !s.is_empty()),
1322 "region": string_option(&stmt.values, ConnectionOptionName::Region),
1323 "access_key_id": string_option(&stmt.values, ConnectionOptionName::AccessKeyId),
1324 "access_key_id_secret_id":
1325 secret_id_option(&stmt.values, ConnectionOptionName::AccessKeyId),
1326 "secret_access_key_secret_id":
1327 secret_id_option(&stmt.values, ConnectionOptionName::SecretAccessKey),
1328 "session_token": string_option(&stmt.values, ConnectionOptionName::SessionToken),
1329 "session_token_secret_id":
1330 secret_id_option(&stmt.values, ConnectionOptionName::SessionToken),
1331 "assume_role_arn": assume_role_arn,
1332 "assume_role_session_name":
1333 string_option(&stmt.values, ConnectionOptionName::AssumeRoleSessionName),
1334 }))
1335 }
1336 _ => Ok(serde_json::Value::Null),
1337 }
1338 };
1339
1340 let val = parse().map_err(|e| EvalError::InvalidCatalogJson(e.into()))?;
1341 let jsonb = Jsonb::from_serde_json(val).expect("valid JSONB");
1342 Ok(jsonb)
1343}
1344
1345#[cfg(test)]
1346mod tests {
1347 use mz_repr::adt::jsonb::Jsonb;
1348 use prost::Message as _;
1349 use serde_json::json;
1350
1351 use crate::EvalError;
1352
1353 fn encode_pg_details(slot: &str, timeline_id: Option<u64>) -> String {
1356 let details = super::PostgresPublicationDetailsSubset {
1357 slot: slot.to_string(),
1358 timeline_id,
1359 };
1360 hex::encode(details.encode_to_vec())
1361 }
1362
1363 fn pg_source_sql(details_hex: &str) -> String {
1364 format!(
1365 "CREATE SOURCE \"materialize\".\"public\".\"pg_src\" \
1366 IN CLUSTER [u42] \
1367 FROM POSTGRES CONNECTION [u10 AS \"materialize\".\"public\".\"pg_conn\"] \
1368 (DETAILS = '{details_hex}', PUBLICATION = 'mz_source') \
1369 FOR ALL TABLES"
1370 )
1371 }
1372
1373 fn kafka_source_sql(with_prefix: bool) -> String {
1374 let prefix_opt = if with_prefix {
1375 ", GROUP ID PREFIX 'my-prefix-'"
1376 } else {
1377 ""
1378 };
1379 format!(
1380 "CREATE SOURCE \"materialize\".\"public\".\"k_src\" \
1381 IN CLUSTER [u42] \
1382 FROM KAFKA CONNECTION [u11 AS \"materialize\".\"public\".\"k_conn\"] \
1383 (TOPIC 'test'{prefix_opt}) FORMAT TEXT"
1384 )
1385 }
1386
1387 fn as_serde(jsonb: Jsonb) -> serde_json::Value {
1388 jsonb.as_ref().to_serde_json()
1389 }
1390
1391 #[mz_ore::test]
1394 #[cfg_attr(miri, ignore)] fn pg_happy_path_with_timeline() {
1396 let hex = encode_pg_details("materialize_abc", Some(42));
1397 let sql = pg_source_sql(&hex);
1398 let out = super::parse_postgres_source_details(&sql).expect("ok");
1399 assert_eq!(
1400 as_serde(out),
1401 json!({ "slot": "materialize_abc", "timeline_id": 42 }),
1402 );
1403 }
1404
1405 #[mz_ore::test]
1406 fn pg_happy_path_null_timeline() {
1407 let hex = encode_pg_details("materialize_legacy", None);
1410 let sql = pg_source_sql(&hex);
1411 let out = super::parse_postgres_source_details(&sql).expect("ok");
1412 assert_eq!(
1413 as_serde(out),
1414 json!({ "slot": "materialize_legacy", "timeline_id": null }),
1415 );
1416 }
1417
1418 #[mz_ore::test]
1419 fn pg_non_postgres_source_returns_null_jsonb() {
1420 let sql = "CREATE SOURCE \"materialize\".\"public\".\"lg\" \
1421 IN CLUSTER [u42] FROM LOAD GENERATOR COUNTER";
1422 let out = super::parse_postgres_source_details(sql).expect("ok");
1423 assert_eq!(as_serde(out), serde_json::Value::Null);
1424 }
1425
1426 #[mz_ore::test]
1427 #[cfg_attr(miri, ignore)] fn pg_non_create_source_returns_null_jsonb() {
1429 let sql = "CREATE VIEW v AS SELECT 1";
1430 let out = super::parse_postgres_source_details(sql).expect("ok");
1431 assert_eq!(as_serde(out), serde_json::Value::Null);
1432 }
1433
1434 #[mz_ore::test]
1435 fn pg_missing_details_option_errors() {
1436 let sql = "CREATE SOURCE \"materialize\".\"public\".\"pg_src\" \
1437 IN CLUSTER [u42] \
1438 FROM POSTGRES CONNECTION [u10 AS \"materialize\".\"public\".\"pg_conn\"] \
1439 (PUBLICATION = 'mz_source') FOR ALL TABLES";
1440 let err = super::parse_postgres_source_details(sql).unwrap_err();
1441 assert!(
1442 matches!(err, EvalError::InvalidCatalogJson(msg) if msg.contains("missing DETAILS")),
1443 "wrong error variant/message"
1444 );
1445 }
1446
1447 #[mz_ore::test]
1448 fn pg_malformed_hex_errors() {
1449 let sql = pg_source_sql("not-hex!!");
1450 let err = super::parse_postgres_source_details(&sql).unwrap_err();
1451 assert!(
1452 matches!(err, EvalError::InvalidCatalogJson(msg) if msg.contains("valid hex")),
1453 "wrong error variant/message"
1454 );
1455 }
1456
1457 #[mz_ore::test]
1458 fn pg_malformed_proto_errors() {
1459 let sql = pg_source_sql("ffff");
1462 let err = super::parse_postgres_source_details(&sql).unwrap_err();
1463 assert!(
1464 matches!(err, EvalError::InvalidCatalogJson(msg) if msg.contains("publication-details proto")),
1465 "wrong error variant/message"
1466 );
1467 }
1468
1469 #[mz_ore::test]
1472 fn kafka_happy_path_with_prefix() {
1473 let sql = kafka_source_sql(true);
1474 let out = super::parse_kafka_source_details(&sql).expect("ok");
1475 assert_eq!(
1476 as_serde(out),
1477 json!({
1478 "topic": "test",
1479 "group_id_prefix": "my-prefix-",
1480 "connection_id": "u11",
1481 }),
1482 );
1483 }
1484
1485 #[mz_ore::test]
1486 fn kafka_happy_path_without_prefix() {
1487 let sql = kafka_source_sql(false);
1488 let out = super::parse_kafka_source_details(&sql).expect("ok");
1489 assert_eq!(
1490 as_serde(out),
1491 json!({
1492 "topic": "test",
1493 "group_id_prefix": null,
1494 "connection_id": "u11",
1495 }),
1496 );
1497 }
1498
1499 #[mz_ore::test]
1500 fn kafka_non_kafka_source_returns_null_jsonb() {
1501 let sql = "CREATE SOURCE \"materialize\".\"public\".\"lg\" \
1502 IN CLUSTER [u42] FROM LOAD GENERATOR COUNTER";
1503 let out = super::parse_kafka_source_details(sql).expect("ok");
1504 assert_eq!(as_serde(out), serde_json::Value::Null);
1505 }
1506
1507 #[mz_ore::test]
1508 fn kafka_missing_topic_errors() {
1509 let sql = "CREATE SOURCE \"materialize\".\"public\".\"k_src\" \
1510 IN CLUSTER [u42] \
1511 FROM KAFKA CONNECTION [u11 AS \"materialize\".\"public\".\"k_conn\"] \
1512 FORMAT TEXT";
1513 let err = super::parse_kafka_source_details(sql).unwrap_err();
1514 assert!(
1515 matches!(err, EvalError::InvalidCatalogJson(msg) if msg.contains("missing TOPIC")),
1516 "wrong error variant/message"
1517 );
1518 }
1519
1520 #[mz_ore::test]
1521 fn kafka_unquoted_topic_and_prefix() {
1522 let sql = "CREATE SOURCE \"materialize\".\"public\".\"k_src\" \
1526 IN CLUSTER [u42] \
1527 FROM KAFKA CONNECTION [u11 AS \"materialize\".\"public\".\"k_conn\"] \
1528 (TOPIC = my_topic, GROUP ID PREFIX = my_prefix) FORMAT TEXT";
1529 let out = super::parse_kafka_source_details(sql).expect("ok");
1530 let out = as_serde(out);
1531 assert_eq!(out["topic"], json!("my_topic"));
1532 assert_eq!(out["group_id_prefix"], json!("my_prefix"));
1533 }
1534
1535 #[mz_ore::test]
1536 fn kafka_unresolved_connection_errors() {
1537 let sql = "CREATE SOURCE \"materialize\".\"public\".\"k_src\" \
1541 IN CLUSTER [u42] \
1542 FROM KAFKA CONNECTION k_conn (TOPIC 'test') FORMAT TEXT";
1543 let err = super::parse_kafka_source_details(sql).unwrap_err();
1544 assert!(
1545 matches!(err, EvalError::InvalidCatalogJson(msg) if msg.contains("unresolved item name")),
1546 "wrong error variant/message"
1547 );
1548 }
1549
1550 fn table_from_source_sql(reference: &str, suffix: &str) -> String {
1553 format!(
1554 "CREATE TABLE \"materialize\".\"public\".\"tbl\" \
1555 FROM SOURCE [u1 AS \"materialize\".\"public\".\"src\"] \
1556 (REFERENCE = {reference}){suffix}"
1557 )
1558 }
1559
1560 #[mz_ore::test]
1561 fn export_table_postgres_style_no_format() {
1562 let sql = table_from_source_sql("\"db\".\"public\".\"t\"", "");
1565 let out = super::parse_source_export_details(&sql).expect("ok");
1566 assert_eq!(
1567 as_serde(out),
1568 json!({
1569 "source_id": "u1",
1570 "external_reference": ["db", "public", "t"],
1571 "envelope_type": null,
1572 "key_format": null,
1573 "value_format": null,
1574 }),
1575 );
1576 }
1577
1578 #[mz_ore::test]
1579 fn export_table_kafka_bare_value_only() {
1580 let sql = table_from_source_sql("\"topic\"", " FORMAT TEXT ENVELOPE NONE");
1582 let out = super::parse_source_export_details(&sql).expect("ok");
1583 assert_eq!(
1584 as_serde(out),
1585 json!({
1586 "source_id": "u1",
1587 "external_reference": ["topic"],
1588 "envelope_type": "none",
1589 "key_format": null,
1590 "value_format": "text",
1591 }),
1592 );
1593 }
1594
1595 #[mz_ore::test]
1596 fn export_table_kafka_omitted_envelope_is_null() {
1597 let sql = table_from_source_sql("\"topic\"", " FORMAT TEXT");
1601 let out = super::parse_source_export_details(&sql).expect("ok");
1602 assert_eq!(as_serde(out)["envelope_type"], serde_json::Value::Null);
1603 }
1604
1605 #[mz_ore::test]
1606 fn export_table_kafka_key_value_format() {
1607 let sql = table_from_source_sql(
1608 "\"topic\"",
1609 " KEY FORMAT TEXT VALUE FORMAT TEXT ENVELOPE NONE",
1610 );
1611 let out = super::parse_source_export_details(&sql).expect("ok");
1612 assert_eq!(
1613 as_serde(out),
1614 json!({
1615 "source_id": "u1",
1616 "external_reference": ["topic"],
1617 "envelope_type": "none",
1618 "key_format": "text",
1619 "value_format": "text",
1620 }),
1621 );
1622 }
1623
1624 #[mz_ore::test]
1625 #[cfg_attr(miri, ignore)] fn export_table_kafka_bare_avro_seed_with_key() {
1627 let sql = table_from_source_sql(
1631 "\"topic\"",
1632 " FORMAT AVRO USING CONFLUENT SCHEMA REGISTRY \
1633 CONNECTION [u5 AS \"materialize\".\"public\".\"csr\"] \
1634 SEED KEY SCHEMA 'k' VALUE SCHEMA 'v' ENVELOPE UPSERT",
1635 );
1636 let out = super::parse_source_export_details(&sql).expect("ok");
1637 assert_eq!(
1638 as_serde(out),
1639 json!({
1640 "source_id": "u1",
1641 "external_reference": ["topic"],
1642 "envelope_type": "upsert",
1643 "key_format": "avro",
1644 "value_format": "avro",
1645 }),
1646 );
1647 }
1648
1649 #[mz_ore::test]
1650 #[cfg_attr(miri, ignore)] fn export_table_kafka_bare_avro_seed_without_key() {
1652 let sql = table_from_source_sql(
1654 "\"topic\"",
1655 " FORMAT AVRO USING CONFLUENT SCHEMA REGISTRY \
1656 CONNECTION [u5 AS \"materialize\".\"public\".\"csr\"] \
1657 SEED VALUE SCHEMA 'v' ENVELOPE NONE",
1658 );
1659 let out = super::parse_source_export_details(&sql).expect("ok");
1660 assert_eq!(
1661 as_serde(out),
1662 json!({
1663 "source_id": "u1",
1664 "external_reference": ["topic"],
1665 "envelope_type": "none",
1666 "key_format": null,
1667 "value_format": "avro",
1668 }),
1669 );
1670 }
1671
1672 #[mz_ore::test]
1673 fn export_subsource_non_progress() {
1674 let sql = "CREATE SUBSOURCE \"materialize\".\"public\".\"sub\" (id int4) \
1677 OF SOURCE [u1 AS \"materialize\".\"public\".\"src\"] \
1678 WITH (EXTERNAL REFERENCE = \"db\".\"public\".\"t\")";
1679 let out = super::parse_source_export_details(sql).expect("ok");
1680 assert_eq!(
1681 as_serde(out),
1682 json!({
1683 "source_id": "u1",
1684 "external_reference": ["db", "public", "t"],
1685 "envelope_type": null,
1686 "key_format": null,
1687 "value_format": null,
1688 }),
1689 );
1690 }
1691
1692 #[mz_ore::test]
1693 fn export_progress_subsource_returns_null_jsonb() {
1694 let sql = "CREATE SUBSOURCE \"materialize\".\"public\".\"progress\" (id int4) \
1695 WITH (PROGRESS)";
1696 let out = super::parse_source_export_details(sql).expect("ok");
1697 assert_eq!(as_serde(out), serde_json::Value::Null);
1698 }
1699
1700 #[mz_ore::test]
1701 #[cfg_attr(miri, ignore)] fn export_non_source_export_returns_null_jsonb() {
1703 let sql = "CREATE VIEW v AS SELECT 1";
1704 let out = super::parse_source_export_details(sql).expect("ok");
1705 assert_eq!(as_serde(out), serde_json::Value::Null);
1706 }
1707
1708 #[mz_ore::test]
1709 fn export_unresolved_source_name_errors() {
1710 let sql = "CREATE TABLE \"materialize\".\"public\".\"tbl\" \
1711 FROM SOURCE src (REFERENCE = \"topic\")";
1712 let err = super::parse_source_export_details(sql).unwrap_err();
1713 assert!(
1714 matches!(err, EvalError::InvalidCatalogJson(msg) if msg.contains("unresolved item name")),
1715 "wrong error variant/message"
1716 );
1717 }
1718
1719 #[mz_ore::test]
1722 fn connection_kafka_single_broker_default_progress() {
1723 let sql = "CREATE CONNECTION \"materialize\".\"public\".\"c\" TO KAFKA \
1726 (BROKER = 'localhost:9092', SECURITY PROTOCOL = plaintext)";
1727 let out = super::parse_connection_details(sql).expect("ok");
1728 assert_eq!(
1729 as_serde(out),
1730 json!({
1731 "brokers": ["localhost:9092"],
1732 "progress_topic": null,
1733 }),
1734 );
1735 }
1736
1737 #[mz_ore::test]
1738 fn connection_kafka_explicit_progress_topic() {
1739 let sql = "CREATE CONNECTION \"materialize\".\"public\".\"c\" TO KAFKA \
1740 (BROKER = 'localhost:9092', PROGRESS TOPIC = 'override', \
1741 SECURITY PROTOCOL = plaintext)";
1742 let out = super::parse_connection_details(sql).expect("ok");
1743 assert_eq!(
1744 as_serde(out),
1745 json!({
1746 "brokers": ["localhost:9092"],
1747 "progress_topic": "override",
1748 }),
1749 );
1750 }
1751
1752 #[mz_ore::test]
1753 fn connection_kafka_broker_list() {
1754 let sql = "CREATE CONNECTION \"materialize\".\"public\".\"c\" TO KAFKA \
1755 (BROKERS ('b1:9092', 'b2:9092'), SECURITY PROTOCOL = plaintext)";
1756 let out = super::parse_connection_details(sql).expect("ok");
1757 assert_eq!(
1758 as_serde(out),
1759 json!({
1760 "brokers": ["b1:9092", "b2:9092"],
1761 "progress_topic": null,
1762 }),
1763 );
1764 }
1765
1766 #[mz_ore::test]
1767 fn connection_ssh_public_keys() {
1768 let sql = "CREATE CONNECTION \"materialize\".\"public\".\"c\" TO SSH TUNNEL \
1769 (HOST = 'ssh.example.com', PORT = 22, USER = 'mz', \
1770 PUBLIC KEY 1 = 'ssh-ed25519 AAAA', PUBLIC KEY 2 = 'ssh-ed25519 BBBB')";
1771 let out = super::parse_connection_details(sql).expect("ok");
1772 assert_eq!(
1773 as_serde(out),
1774 json!({
1775 "public_key_1": "ssh-ed25519 AAAA",
1776 "public_key_2": "ssh-ed25519 BBBB",
1777 }),
1778 );
1779 }
1780
1781 #[mz_ore::test]
1782 fn connection_aws_credentials_inline_key() {
1783 let sql = "CREATE CONNECTION \"materialize\".\"public\".\"c\" TO AWS \
1786 (ACCESS KEY ID = 'AKIAEXAMPLE', \
1787 SECRET ACCESS KEY = SECRET [u1 AS \"materialize\".\"public\".\"sk\"])";
1788 let out = super::parse_connection_details(sql).expect("ok");
1789 assert_eq!(
1790 as_serde(out),
1791 json!({
1792 "auth_kind": "credentials",
1793 "endpoint": null,
1794 "region": null,
1795 "access_key_id": "AKIAEXAMPLE",
1796 "access_key_id_secret_id": null,
1797 "secret_access_key_secret_id": "u1",
1798 "session_token": null,
1799 "session_token_secret_id": null,
1800 "assume_role_arn": null,
1801 "assume_role_session_name": null,
1802 }),
1803 );
1804 }
1805
1806 #[mz_ore::test]
1807 fn connection_aws_credentials_secret_key_and_session_token() {
1808 let sql = "CREATE CONNECTION \"materialize\".\"public\".\"c\" TO AWS \
1811 (ENDPOINT = 'http://localhost', REGION = 'us-east-1', \
1812 ACCESS KEY ID = SECRET [u1 AS \"materialize\".\"public\".\"ak\"], \
1813 SECRET ACCESS KEY = SECRET [u2 AS \"materialize\".\"public\".\"sk\"], \
1814 SESSION TOKEN = SECRET [u3 AS \"materialize\".\"public\".\"st\"])";
1815 let out = super::parse_connection_details(sql).expect("ok");
1816 assert_eq!(
1817 as_serde(out),
1818 json!({
1819 "auth_kind": "credentials",
1820 "endpoint": "http://localhost",
1821 "region": "us-east-1",
1822 "access_key_id": null,
1823 "access_key_id_secret_id": "u1",
1824 "secret_access_key_secret_id": "u2",
1825 "session_token": null,
1826 "session_token_secret_id": "u3",
1827 "assume_role_arn": null,
1828 "assume_role_session_name": null,
1829 }),
1830 );
1831 }
1832
1833 #[mz_ore::test]
1834 fn connection_aws_assume_role() {
1835 let sql = "CREATE CONNECTION \"materialize\".\"public\".\"c\" TO AWS \
1838 (ASSUME ROLE ARN 'arn:aws:iam::123:role/mz', \
1839 ASSUME ROLE SESSION NAME 'sess')";
1840 let out = super::parse_connection_details(sql).expect("ok");
1841 assert_eq!(
1842 as_serde(out),
1843 json!({
1844 "auth_kind": "assume-role",
1845 "endpoint": null,
1846 "region": null,
1847 "access_key_id": null,
1848 "access_key_id_secret_id": null,
1849 "secret_access_key_secret_id": null,
1850 "session_token": null,
1851 "session_token_secret_id": null,
1852 "assume_role_arn": "arn:aws:iam::123:role/mz",
1853 "assume_role_session_name": "sess",
1854 }),
1855 );
1856 }
1857
1858 #[mz_ore::test]
1859 fn connection_kafka_unquoted_progress_topic() {
1860 let sql = "CREATE CONNECTION \"materialize\".\"public\".\"c\" TO KAFKA \
1863 (BROKER = 'localhost:9092', PROGRESS TOPIC = my_topic, \
1864 SECURITY PROTOCOL = plaintext)";
1865 let out = super::parse_connection_details(sql).expect("ok");
1866 assert_eq!(as_serde(out)["progress_topic"], json!("my_topic"));
1867 }
1868
1869 #[mz_ore::test]
1870 fn connection_aws_unquoted_option_values() {
1871 let sql = "CREATE CONNECTION \"materialize\".\"public\".\"c\" TO AWS \
1874 (ENDPOINT = localhost, REGION = useast1, \
1875 ASSUME ROLE ARN 'arn:aws:iam::123:role/mz', \
1876 ASSUME ROLE SESSION NAME = mysession)";
1877 let out = super::parse_connection_details(sql).expect("ok");
1878 let out = as_serde(out);
1879 assert_eq!(out["endpoint"], json!("localhost"));
1880 assert_eq!(out["region"], json!("useast1"));
1881 assert_eq!(out["assume_role_session_name"], json!("mysession"));
1882 }
1883
1884 #[mz_ore::test]
1885 fn connection_aws_empty_endpoint_is_null() {
1886 let sql = "CREATE CONNECTION \"materialize\".\"public\".\"c\" TO AWS \
1889 (ENDPOINT = '', ASSUME ROLE ARN 'arn:aws:iam::123:role/mz')";
1890 let out = super::parse_connection_details(sql).expect("ok");
1891 assert_eq!(as_serde(out)["endpoint"], serde_json::Value::Null);
1892 }
1893
1894 #[mz_ore::test]
1895 fn connection_other_type_returns_null_jsonb() {
1896 let sql = "CREATE CONNECTION \"materialize\".\"public\".\"c\" TO POSTGRES \
1898 (HOST = 'db', DATABASE = 'postgres', USER = 'mz')";
1899 let out = super::parse_connection_details(sql).expect("ok");
1900 assert_eq!(as_serde(out), serde_json::Value::Null);
1901 }
1902
1903 #[mz_ore::test]
1904 #[cfg_attr(miri, ignore)] fn connection_non_connection_returns_null_jsonb() {
1906 let sql = "CREATE VIEW v AS SELECT 1";
1907 let out = super::parse_connection_details(sql).expect("ok");
1908 assert_eq!(as_serde(out), serde_json::Value::Null);
1909 }
1910
1911 #[mz_ore::test]
1914 fn catalog_kafka_old_syntax_omitted_envelope_defaults_none() {
1915 let sql = "CREATE SOURCE \"materialize\".\"public\".\"k\" \
1919 IN CLUSTER [u42] \
1920 FROM KAFKA CONNECTION [u11 AS \"materialize\".\"public\".\"k_conn\"] \
1921 (TOPIC 'test') FORMAT TEXT \
1922 EXPOSE PROGRESS AS [u12 AS \"materialize\".\"public\".\"k_progress\"]";
1923 let out = super::parse_catalog_create_sql(sql).expect("ok");
1924 assert_eq!(as_serde(out)["envelope_type"], json!("none"));
1925 }
1926
1927 #[mz_ore::test]
1928 fn catalog_kafka_old_syntax_explicit_envelope() {
1929 let sql = "CREATE SOURCE \"materialize\".\"public\".\"k\" \
1930 IN CLUSTER [u42] \
1931 FROM KAFKA CONNECTION [u11 AS \"materialize\".\"public\".\"k_conn\"] \
1932 (TOPIC 'test') FORMAT BYTES ENVELOPE UPSERT \
1933 EXPOSE PROGRESS AS [u12 AS \"materialize\".\"public\".\"k_progress\"]";
1934 let out = super::parse_catalog_create_sql(sql).expect("ok");
1935 assert_eq!(as_serde(out)["envelope_type"], json!("upsert"));
1936 }
1937
1938 #[mz_ore::test]
1939 fn catalog_kafka_new_syntax_source_omits_envelope_type() {
1940 let sql = "CREATE SOURCE \"materialize\".\"public\".\"k\" \
1944 IN CLUSTER [u42] \
1945 FROM KAFKA CONNECTION [u11 AS \"materialize\".\"public\".\"k_conn\"]";
1946 let out = super::parse_catalog_create_sql(sql).expect("ok");
1947 assert_eq!(as_serde(out).get("envelope_type"), None);
1948 }
1949
1950 #[mz_ore::test]
1951 fn catalog_non_kafka_source_omits_envelope_type() {
1952 let sql = "CREATE SOURCE \"materialize\".\"public\".\"lg\" \
1955 IN CLUSTER [u42] FROM LOAD GENERATOR COUNTER";
1956 let out = super::parse_catalog_create_sql(sql).expect("ok");
1957 assert_eq!(as_serde(out).get("envelope_type"), None);
1958 }
1959
1960 const AVRO_FORMAT: &str = "FORMAT AVRO USING CONFLUENT SCHEMA REGISTRY CONNECTION \
1963 [u12 AS \"materialize\".\"public\".\"csr_conn\"]";
1964
1965 fn kafka_sink_sql(key: Option<&str>, format: &str, envelope: &str) -> String {
1968 let key_clause = key.map(|k| format!(" KEY ({k})")).unwrap_or_default();
1969 format!(
1970 "CREATE SINK \"materialize\".\"public\".\"snk\" \
1971 IN CLUSTER [u42] \
1972 FROM [u1 AS \"materialize\".\"public\".\"t\"] \
1973 INTO KAFKA CONNECTION [u10 AS \"materialize\".\"public\".\"k_conn\"] \
1974 (TOPIC 'sink-topic'){key_clause} {format} ENVELOPE {envelope}"
1975 )
1976 }
1977
1978 fn iceberg_sink_sql(mode: &str) -> String {
1979 format!(
1980 "CREATE SINK \"materialize\".\"public\".\"ice\" \
1981 IN CLUSTER [u42] \
1982 FROM [u1 AS \"materialize\".\"public\".\"t\"] \
1983 INTO ICEBERG CATALOG CONNECTION [u20 AS \"materialize\".\"public\".\"cat_conn\"] \
1984 (NAMESPACE 'ns', TABLE 'tbl') \
1985 USING AWS CONNECTION [u21 AS \"materialize\".\"public\".\"aws_conn\"] \
1986 MODE {mode}"
1987 )
1988 }
1989
1990 #[mz_ore::test]
1991 fn sink_kafka_bare_format_without_key() {
1992 let sql = kafka_sink_sql(None, "FORMAT JSON", "DEBEZIUM");
1993 let out = super::parse_catalog_create_sql(&sql).expect("ok");
1994 assert_eq!(
1995 as_serde(out),
1996 json!({
1997 "type": "sink",
1998 "sink_type": "kafka",
1999 "cluster_id": "u42",
2000 "connection_id": "u10",
2001 "topic": "sink-topic",
2002 "envelope_type": "debezium",
2003 "format": "json",
2004 "value_format": "json",
2005 }),
2006 );
2007 }
2008
2009 #[mz_ore::test]
2010 fn sink_kafka_bare_format_with_key_derives_key_format() {
2011 let sql = kafka_sink_sql(Some("a"), AVRO_FORMAT, "UPSERT");
2014 let out = super::parse_catalog_create_sql(&sql).expect("ok");
2015 assert_eq!(
2016 as_serde(out),
2017 json!({
2018 "type": "sink",
2019 "sink_type": "kafka",
2020 "cluster_id": "u42",
2021 "connection_id": "u10",
2022 "topic": "sink-topic",
2023 "envelope_type": "upsert",
2024 "format": "avro",
2025 "key_format": "avro",
2026 "value_format": "avro",
2027 }),
2028 );
2029 }
2030
2031 #[mz_ore::test]
2032 fn sink_kafka_bare_text_format_with_key_does_not_collapse() {
2033 let sql = kafka_sink_sql(Some("a"), "FORMAT TEXT", "UPSERT");
2036 let out = super::parse_catalog_create_sql(&sql).expect("ok");
2037 let out = as_serde(out);
2038 assert_eq!(out["format"], json!("key-text-value-text"));
2039 assert_eq!(out["key_format"], json!("text"));
2040 assert_eq!(out["value_format"], json!("text"));
2041 }
2042
2043 #[mz_ore::test]
2044 fn sink_kafka_key_value_json_collapses() {
2045 let sql = kafka_sink_sql(Some("a"), "KEY FORMAT JSON VALUE FORMAT JSON", "UPSERT");
2046 let out = as_serde(super::parse_catalog_create_sql(&sql).expect("ok"));
2047 assert_eq!(out["format"], json!("json"));
2048 assert_eq!(out["key_format"], json!("json"));
2049 assert_eq!(out["value_format"], json!("json"));
2050 }
2051
2052 #[mz_ore::test]
2053 fn sink_kafka_key_value_mixed_is_composite() {
2054 let sql = kafka_sink_sql(Some("a"), "KEY FORMAT TEXT VALUE FORMAT BYTES", "UPSERT");
2055 let out = as_serde(super::parse_catalog_create_sql(&sql).expect("ok"));
2056 assert_eq!(out["format"], json!("key-text-value-bytes"));
2057 assert_eq!(out["key_format"], json!("text"));
2058 assert_eq!(out["value_format"], json!("bytes"));
2059 }
2060
2061 #[mz_ore::test]
2062 fn sink_kafka_key_format_without_key_is_dropped() {
2063 let sql = kafka_sink_sql(None, "KEY FORMAT JSON VALUE FORMAT TEXT", "DEBEZIUM");
2067 let out = as_serde(super::parse_catalog_create_sql(&sql).expect("ok"));
2068 assert_eq!(out["format"], json!("text"));
2069 assert_eq!(out["key_format"], serde_json::Value::Null);
2070 assert_eq!(out["value_format"], json!("text"));
2071 }
2072
2073 #[mz_ore::test]
2074 fn sink_kafka_missing_topic_errors() {
2075 let sql = "CREATE SINK \"materialize\".\"public\".\"snk\" \
2076 IN CLUSTER [u42] \
2077 FROM [u1 AS \"materialize\".\"public\".\"t\"] \
2078 INTO KAFKA CONNECTION [u10 AS \"materialize\".\"public\".\"k_conn\"] \
2079 FORMAT JSON ENVELOPE DEBEZIUM";
2080 let err = super::parse_catalog_create_sql(sql).unwrap_err();
2081 assert!(
2082 matches!(err, EvalError::InvalidCatalogJson(msg) if msg.contains("missing TOPIC")),
2083 "wrong error variant/message"
2084 );
2085 }
2086
2087 #[mz_ore::test]
2088 fn sink_iceberg_upsert_mode() {
2089 let sql = iceberg_sink_sql("UPSERT");
2090 let out = super::parse_catalog_create_sql(&sql).expect("ok");
2091 assert_eq!(
2092 as_serde(out),
2093 json!({
2094 "type": "sink",
2095 "sink_type": "iceberg",
2096 "cluster_id": "u42",
2097 "connection_id": "u20",
2099 "namespace": "ns",
2100 "table": "tbl",
2101 "envelope_type": "upsert",
2102 }),
2103 );
2104 }
2105
2106 #[mz_ore::test]
2107 fn sink_iceberg_append_mode() {
2108 let sql = iceberg_sink_sql("APPEND");
2109 let out = as_serde(super::parse_catalog_create_sql(&sql).expect("ok"));
2110 assert_eq!(out["envelope_type"], json!("append"));
2112 }
2113
2114 #[mz_ore::test]
2115 fn sink_iceberg_missing_table_errors() {
2116 let sql = "CREATE SINK \"materialize\".\"public\".\"ice\" \
2117 IN CLUSTER [u42] \
2118 FROM [u1 AS \"materialize\".\"public\".\"t\"] \
2119 INTO ICEBERG CATALOG CONNECTION [u20 AS \"materialize\".\"public\".\"cat_conn\"] \
2120 (NAMESPACE 'ns') MODE UPSERT";
2121 let err = super::parse_catalog_create_sql(sql).unwrap_err();
2122 assert!(
2123 matches!(err, EvalError::InvalidCatalogJson(msg) if msg.contains("missing TABLE")),
2124 "wrong error variant/message"
2125 );
2126 }
2127
2128 #[mz_ore::test]
2129 #[cfg_attr(miri, ignore)] fn sink_arm_leaves_other_item_types_alone() {
2131 let sql = "CREATE VIEW \"materialize\".\"public\".\"v\" AS SELECT 1";
2132 let out = super::parse_catalog_create_sql(sql).expect("ok");
2133 assert_eq!(
2134 as_serde(out),
2135 json!({ "type": "view", "definition": "SELECT 1;" })
2136 );
2137 }
2138
2139 fn item_type(sql: &str) -> Result<String, String> {
2143 match super::parse_catalog_create_sql(sql) {
2144 Ok(out) => match as_serde(out) {
2145 serde_json::Value::Object(mut m) => match m.remove("type") {
2146 Some(serde_json::Value::String(s)) => Ok(s),
2147 other => panic!("no string `type` key: {other:?}"),
2148 },
2149 other => panic!("not a JSON object: {other:?}"),
2150 },
2151 Err(EvalError::InvalidCatalogJson(msg)) => Err(msg.to_string()),
2152 Err(e) => panic!("unexpected error variant: {e:?}"),
2153 }
2154 }
2155
2156 fn view_sql(query: &str) -> String {
2157 format!("CREATE VIEW \"materialize\".\"public\".\"v\" AS {query}")
2158 }
2159
2160 fn view_definition(query: &str) -> String {
2162 match as_serde(super::parse_catalog_create_sql(&view_sql(query)).expect("ok")) {
2163 serde_json::Value::Object(mut m) => match m.remove("definition") {
2164 Some(serde_json::Value::String(s)) => s,
2165 other => panic!("no string `definition` key: {other:?}"),
2166 },
2167 other => panic!("not a JSON object: {other:?}"),
2168 }
2169 }
2170
2171 #[mz_ore::test]
2177 #[cfg_attr(miri, ignore)] fn catalog_item_type_per_statement_kind() {
2179 let cases = [
2180 (
2181 "CREATE TABLE \"materialize\".\"public\".\"t\" (a int4)",
2182 "table",
2183 ),
2184 (
2187 "CREATE TABLE \"materialize\".\"public\".\"tbl\" \
2188 FROM SOURCE [u1 AS \"materialize\".\"public\".\"src\"] \
2189 (REFERENCE = \"topic\") FORMAT TEXT",
2190 "table",
2191 ),
2192 (
2193 "CREATE TABLE \"materialize\".\"public\".\"wht\" FROM WEBHOOK BODY FORMAT JSON",
2194 "table",
2195 ),
2196 (
2197 "CREATE VIEW \"materialize\".\"public\".\"v\" AS SELECT 1",
2198 "view",
2199 ),
2200 (
2201 "CREATE MATERIALIZED VIEW \"materialize\".\"public\".\"mv\" \
2202 IN CLUSTER [u1] AS SELECT 1",
2203 "materialized-view",
2204 ),
2205 (
2206 "CREATE SOURCE \"materialize\".\"public\".\"lg\" \
2207 IN CLUSTER [u1] FROM LOAD GENERATOR COUNTER",
2208 "source",
2209 ),
2210 (
2211 "CREATE SOURCE \"materialize\".\"public\".\"wh\" \
2212 IN CLUSTER [u1] FROM WEBHOOK BODY FORMAT JSON",
2213 "source",
2214 ),
2215 (
2216 "CREATE SUBSOURCE \"materialize\".\"public\".\"sub\" (id int4) \
2217 OF SOURCE [u1 AS \"materialize\".\"public\".\"src\"]",
2218 "subsource",
2219 ),
2220 (
2221 "CREATE SUBSOURCE \"materialize\".\"public\".\"progress\" (id int4) \
2222 WITH (PROGRESS)",
2223 "subsource",
2224 ),
2225 (
2226 "CREATE SINK \"materialize\".\"public\".\"snk\" IN CLUSTER [u1] \
2227 FROM [u1 AS \"materialize\".\"public\".\"t\"] \
2228 INTO KAFKA CONNECTION [u2 AS \"materialize\".\"public\".\"c\"] \
2229 (TOPIC 'tp') FORMAT JSON ENVELOPE DEBEZIUM",
2230 "sink",
2231 ),
2232 (
2233 "CREATE INDEX \"i\" IN CLUSTER [u1] \
2234 ON [u1 AS \"materialize\".\"public\".\"t\"] (\"a\")",
2235 "index",
2236 ),
2237 (
2238 "CREATE TYPE \"materialize\".\"public\".\"ty\" AS LIST (ELEMENT TYPE = int4)",
2239 "type",
2240 ),
2241 (
2242 "CREATE SECRET \"materialize\".\"public\".\"s\" AS 'x'",
2243 "secret",
2244 ),
2245 (
2246 "CREATE CONNECTION \"materialize\".\"public\".\"c\" \
2247 TO KAFKA (BROKER 'b', SECURITY PROTOCOL PLAINTEXT)",
2248 "connection",
2249 ),
2250 ];
2251 for (sql, expected) in cases {
2252 assert_eq!(item_type(sql).as_deref(), Ok(expected), "for {sql}");
2253 }
2254 }
2255
2256 #[mz_ore::test]
2260 #[cfg_attr(miri, ignore)] fn catalog_view_definition() {
2262 assert_eq!(view_definition("SELECT 1"), "SELECT 1;");
2265 assert_eq!(
2266 view_definition("WITH c AS (SELECT 1 AS a) SELECT a FROM c"),
2267 "WITH \"c\" AS (SELECT 1 AS \"a\") SELECT \"a\" FROM \"c\";"
2268 );
2269 assert_eq!(
2270 view_definition("SELECT 1 UNION ALL SELECT 2"),
2271 "SELECT 1 UNION ALL SELECT 2;"
2272 );
2273 assert_eq!(
2274 view_definition("SELECT (SELECT max(a) FROM [u1 AS \"materialize\".\"public\".\"t\"])"),
2275 "SELECT (SELECT \"max\"(\"a\") FROM [u1 AS \"materialize\".\"public\".\"t\"]);"
2276 );
2277 assert_eq!(
2280 view_definition(
2281 "SELECT \"a b\", \"héllo\", \"q\"\"x\" \
2282 FROM [u1 AS \"materialize\".\"public\".\"t\"] \
2283 WHERE s = 'lit''eral' AND n = 42 ORDER BY 1"
2284 ),
2285 "SELECT \"a b\", \"héllo\", \"q\"\"x\" \
2286 FROM [u1 AS \"materialize\".\"public\".\"t\"] \
2287 WHERE \"s\" = 'lit''eral' AND \"n\" = 42 ORDER BY 1;"
2288 );
2289 }
2290
2291 #[mz_ore::test]
2296 #[cfg_attr(miri, ignore)] fn catalog_view_definition_is_idempotent() {
2298 for query in [
2299 "SELECT 1",
2300 "WITH c AS (SELECT 1 AS a) SELECT a FROM c",
2301 "SELECT 1 UNION ALL SELECT 2",
2302 "SELECT \"a b\", \"q\"\"x\" FROM [u1 AS \"materialize\".\"public\".\"t\"] \
2303 WHERE s = 'lit''eral' ORDER BY 1",
2304 ] {
2305 let once = view_definition(query);
2306 assert_eq!(
2307 view_definition(&once),
2308 once,
2309 "not a fixed point for {query}"
2310 );
2311 }
2312 }
2313
2314 #[mz_ore::test]
2317 #[cfg_attr(miri, ignore)] fn catalog_table_source_id() {
2319 let from_source = as_serde(
2320 super::parse_catalog_create_sql(
2321 "CREATE TABLE \"materialize\".\"public\".\"tbl\" \
2322 FROM SOURCE [u1 AS \"materialize\".\"public\".\"src\"] \
2323 (REFERENCE = \"topic\") FORMAT TEXT",
2324 )
2325 .expect("ok"),
2326 );
2327 assert_eq!(from_source, json!({ "type": "table", "source_id": "u1" }));
2328
2329 for sql in [
2330 "CREATE TABLE \"materialize\".\"public\".\"t\" (a int4)",
2331 "CREATE TABLE \"materialize\".\"public\".\"wht\" FROM WEBHOOK BODY FORMAT JSON",
2332 ] {
2333 assert_eq!(
2334 as_serde(super::parse_catalog_create_sql(sql).expect("ok")),
2335 json!({ "type": "table" }),
2336 "for {sql}"
2337 );
2338 }
2339 }
2340
2341 #[mz_ore::test]
2345 #[cfg_attr(miri, ignore)] fn catalog_create_sql_errors() {
2347 assert_eq!(
2348 item_type("this is not sql"),
2349 Err(
2350 "failed to parse create_sql: Expected a keyword at the beginning of a statement, \
2351 found identifier \"this\""
2352 .to_string()
2353 )
2354 );
2355 assert_eq!(
2356 item_type("CREATE TABLE t (a int4); CREATE TABLE u (b int4)"),
2357 Err("expected a single statement, found 2".to_string())
2358 );
2359 assert_eq!(
2362 item_type("SELECT 1"),
2363 Err("not a CREATE item statement".to_string())
2364 );
2365 assert_eq!(
2368 item_type(
2369 "CREATE TABLE \"materialize\".\"public\".\"tbl\" \
2370 FROM SOURCE src (REFERENCE = \"topic\")"
2371 ),
2372 Err("unresolved item name".to_string())
2373 );
2374 }
2375
2376 #[mz_ore::test]
2379 #[cfg_attr(miri, ignore)] fn item_references_view() {
2381 let sql = "CREATE VIEW \"materialize\".\"public\".\"v\" AS \
2382 SELECT \"pg_catalog\".\"abs\"(\"a\"::[s20 AS \"pg_catalog\".\"int4\"]) \
2383 FROM [u1 AS \"materialize\".\"public\".\"t\"]";
2384 let out = as_serde(super::parse_catalog_item_references(sql).expect("ok"));
2385 assert_eq!(
2386 out,
2387 json!({
2388 "ids": ["s20", "u1"],
2389 "named_funcs": [{"schema": "pg_catalog", "name": "abs"}],
2390 "named_types": [],
2391 "named_relations": [],
2392 })
2393 );
2394 }
2395
2396 #[mz_ore::test]
2397 #[cfg_attr(miri, ignore)] fn item_references_connection() {
2399 let sql = "CREATE CONNECTION \"materialize\".\"public\".\"kc\" TO KAFKA (\
2400 BROKER 'kafka:9092', \
2401 SSH TUNNEL [u5 AS \"materialize\".\"public\".\"ssh\"], \
2402 SASL PASSWORD = SECRET [u7 AS \"materialize\".\"public\".\"pw\"])";
2403 let out = as_serde(super::parse_catalog_item_references(sql).expect("ok"));
2404 assert_eq!(out["ids"], json!(["u5", "u7"]));
2405 assert_eq!(out["named_funcs"], json!([]));
2406 }
2407
2408 #[mz_ore::test]
2409 #[cfg_attr(miri, ignore)] fn item_references_cte_shadowing() {
2411 let sql = "CREATE VIEW \"materialize\".\"public\".\"v\" AS \
2412 WITH \"c\" AS (SELECT * FROM [u1 AS \"materialize\".\"public\".\"t\"]) \
2413 SELECT * FROM \"c\"";
2414 let out = as_serde(super::parse_catalog_item_references(sql).expect("ok"));
2415 assert_eq!(out["ids"], json!(["u1"]));
2416 assert_eq!(out["named_relations"], json!([]));
2417 }
2418
2419 #[mz_ore::test]
2420 #[cfg_attr(miri, ignore)] fn item_references_name_based_relation_surfaces() {
2422 let sql = "CREATE VIEW \"materialize\".\"public\".\"v\" AS \
2425 SELECT * FROM \"mz_catalog\".\"mz_tables\"";
2426 let out = as_serde(super::parse_catalog_item_references(sql).expect("ok"));
2427 assert_eq!(
2428 out["named_relations"],
2429 json!([{"schema": "mz_catalog", "name": "mz_tables"}])
2430 );
2431 }
2432
2433 #[mz_ore::test]
2434 fn item_references_parse_error() {
2435 let err = super::parse_catalog_item_references("NOT SQL").unwrap_err();
2436 assert!(
2437 matches!(err, EvalError::InvalidCatalogJson(msg) if msg.contains("failed to parse")),
2438 "wrong error variant/message"
2439 );
2440 }
2441}