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::{
21 AstInfo, AvroSchema, ConnectionOption, ConnectionOptionName, CreateConnectionType,
22 CreateSinkConnection, CreateSubsourceOptionName, Format, FormatSpecifier,
23 IcebergSinkConfigOptionName, IcebergSinkMode, KafkaSinkConfigOptionName,
24 KafkaSourceConfigOptionName, PgConfigOptionName, ProtobufSchema, RawClusterName, RawItemName,
25 SinkEnvelope, SourceEnvelope, SourceErrorPolicy, UnresolvedItemName, Value, WithOptionValue,
26};
27use prost::Message as _;
28use serde::{Deserialize, Serialize};
29use serde_json::json;
30
31use crate::EvalError;
32use crate::scalar::func::EagerUnaryFunc;
33use crate::scalar::func::impls::numeric::*;
34
35#[sqlfunc(
36 sqlname = "jsonb_to_text",
37 preserves_uniqueness = false,
38 inverse = to_unary!(super::CastStringToJsonb)
39)]
40pub fn cast_jsonb_to_string<'a>(a: JsonbRef<'a>) -> String {
41 let mut buf = String::new();
42 strconv::format_jsonb(&mut buf, a);
43 buf
44}
45
46#[sqlfunc(sqlname = "jsonb_to_smallint", is_monotone = true)]
47fn cast_jsonb_to_int16<'a>(a: JsonbRef<'a>) -> Result<i16, EvalError> {
48 match a.into_datum() {
49 Datum::Numeric(a) => cast_numeric_to_int16(a.into_inner()),
50 datum => Err(EvalError::InvalidJsonbCast {
51 from: jsonb_typeof(JsonbRef::from_datum(datum)).into(),
52 to: "smallint".into(),
53 }),
54 }
55}
56
57#[sqlfunc(sqlname = "jsonb_to_integer", is_monotone = true)]
58fn cast_jsonb_to_int32<'a>(a: JsonbRef<'a>) -> Result<i32, EvalError> {
59 match a.into_datum() {
60 Datum::Numeric(a) => cast_numeric_to_int32(a.into_inner()),
61 datum => Err(EvalError::InvalidJsonbCast {
62 from: jsonb_typeof(JsonbRef::from_datum(datum)).into(),
63 to: "integer".into(),
64 }),
65 }
66}
67
68#[sqlfunc(sqlname = "jsonb_to_bigint", is_monotone = true)]
69fn cast_jsonb_to_int64<'a>(a: JsonbRef<'a>) -> Result<i64, EvalError> {
70 match a.into_datum() {
71 Datum::Numeric(a) => cast_numeric_to_int64(a.into_inner()),
72 datum => Err(EvalError::InvalidJsonbCast {
73 from: jsonb_typeof(JsonbRef::from_datum(datum)).into(),
74 to: "bigint".into(),
75 }),
76 }
77}
78
79#[sqlfunc(sqlname = "jsonb_to_real", is_monotone = true)]
80fn cast_jsonb_to_float32<'a>(a: JsonbRef<'a>) -> Result<f32, EvalError> {
81 match a.into_datum() {
82 Datum::Numeric(a) => cast_numeric_to_float32(a.into_inner()),
83 datum => Err(EvalError::InvalidJsonbCast {
84 from: jsonb_typeof(JsonbRef::from_datum(datum)).into(),
85 to: "real".into(),
86 }),
87 }
88}
89
90#[sqlfunc(sqlname = "jsonb_to_double", is_monotone = true)]
91fn cast_jsonb_to_float64<'a>(a: JsonbRef<'a>) -> Result<f64, EvalError> {
92 match a.into_datum() {
93 Datum::Numeric(a) => cast_numeric_to_float64(a.into_inner()),
94 datum => Err(EvalError::InvalidJsonbCast {
95 from: jsonb_typeof(JsonbRef::from_datum(datum)).into(),
96 to: "double precision".into(),
97 }),
98 }
99}
100
101#[derive(
102 Ord,
103 PartialOrd,
104 Clone,
105 Debug,
106 Eq,
107 PartialEq,
108 Serialize,
109 Deserialize,
110 Hash
111)]
112pub struct CastJsonbToNumeric(pub Option<NumericMaxScale>);
113
114impl EagerUnaryFunc for CastJsonbToNumeric {
115 type Input<'a> = JsonbRef<'a>;
116 type Output<'a> = Result<Numeric, EvalError>;
117
118 fn call<'a>(&self, a: Self::Input<'a>) -> Self::Output<'a> {
119 match a.into_datum() {
120 Datum::Numeric(mut num) => match self.0 {
121 None => Ok(num.into_inner()),
122 Some(scale) => {
123 if numeric::rescale(&mut num.0, scale.into_u8()).is_err() {
124 return Err(EvalError::NumericFieldOverflow);
125 };
126 Ok(num.into_inner())
127 }
128 },
129 datum => Err(EvalError::InvalidJsonbCast {
130 from: jsonb_typeof(JsonbRef::from_datum(datum)).into(),
131 to: "numeric".into(),
132 }),
133 }
134 }
135
136 fn output_sql_type(&self, input: SqlColumnType) -> SqlColumnType {
137 SqlScalarType::Numeric { max_scale: self.0 }.nullable(input.nullable)
138 }
139
140 fn is_monotone(&self) -> bool {
141 true
142 }
143}
144
145impl fmt::Display for CastJsonbToNumeric {
146 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
147 f.write_str("jsonb_to_numeric")
148 }
149}
150
151#[sqlfunc(sqlname = "jsonb_to_boolean", is_monotone = true)]
152fn cast_jsonb_to_bool<'a>(a: JsonbRef<'a>) -> Result<bool, EvalError> {
153 match a.into_datum() {
154 Datum::True => Ok(true),
155 Datum::False => Ok(false),
156 datum => Err(EvalError::InvalidJsonbCast {
157 from: jsonb_typeof(JsonbRef::from_datum(datum)).into(),
158 to: "boolean".into(),
159 }),
160 }
161}
162
163#[sqlfunc(sqlname = "jsonbable_to_jsonb")]
164fn cast_jsonbable_to_jsonb<'a>(a: JsonbRef<'a>) -> JsonbRef<'a> {
165 match a.into_datum() {
166 Datum::Numeric(n) => {
167 let n = n.into_inner();
168 let datum = if n.is_finite() {
169 Datum::from(n)
170 } else if n.is_nan() {
171 Datum::String("NaN")
172 } else if n.is_negative() {
173 Datum::String("-Infinity")
174 } else {
175 Datum::String("Infinity")
176 };
177 JsonbRef::from_datum(datum)
178 }
179 datum => JsonbRef::from_datum(datum),
180 }
181}
182
183#[sqlfunc]
184fn jsonb_array_length<'a>(a: JsonbRef<'a>) -> Result<Option<i32>, EvalError> {
185 match a.into_datum() {
186 Datum::List(list) => {
187 let count = list.iter().count();
188 match i32::try_from(count) {
189 Ok(len) => Ok(Some(len)),
190 Err(_) => Err(EvalError::Int32OutOfRange(count.to_string().into())),
191 }
192 }
193 _ => Ok(None),
194 }
195}
196
197#[sqlfunc]
198fn jsonb_typeof<'a>(a: JsonbRef<'a>) -> &'a str {
199 match a.into_datum() {
200 Datum::Map(_) => "object",
201 Datum::List(_) => "array",
202 Datum::String(_) => "string",
203 Datum::Numeric(_) => "number",
204 Datum::True | Datum::False => "boolean",
205 Datum::JsonNull => "null",
206 d => panic!("Not jsonb: {:?}", d),
207 }
208}
209
210#[sqlfunc]
211fn jsonb_strip_nulls<'a>(a: JsonbRef<'a>) -> Jsonb {
212 fn strip_nulls(a: Datum, row: &mut RowPacker) {
213 match a {
214 Datum::Map(dict) => row.push_dict_with(|row| {
215 for (k, v) in dict.iter() {
216 match v {
217 Datum::JsonNull => (),
218 _ => {
219 row.push(Datum::String(k));
220 strip_nulls(v, row);
221 }
222 }
223 }
224 }),
225 Datum::List(list) => row.push_list_with(|row| {
226 for elem in list.iter() {
227 strip_nulls(elem, row);
228 }
229 }),
230 _ => row.push(a),
231 }
232 }
233 let mut row = Row::default();
234 strip_nulls(a.into_datum(), &mut row.packer());
235 Jsonb::from_row(row)
236}
237
238#[sqlfunc]
240fn jsonb_pretty<'a>(a: JsonbRef<'a>) -> String {
241 let mut buf = String::new();
242 strconv::format_jsonb_pretty(&mut buf, a);
243 buf
244}
245
246fn jsonb_datum_to_u64<'a>(d: Datum<'a>) -> Result<u64, String> {
248 let Datum::Numeric(n) = d else {
249 return Err("expected numeric value".into());
250 };
251
252 let mut cx = numeric::cx_datum();
253 cx.try_into_u64(n.0)
254 .map_err(|_| format!("number out of u64 range: {n}"))
255}
256
257fn jsonb_datum_to_acl_mode(d: Datum) -> Result<AclMode, String> {
263 let Datum::Map(dict) = d else {
264 return Err(format!("unexpected acl_mode: {d}"));
265 };
266 let mut bits = None;
267 for (key, val) in dict.iter() {
268 match key {
269 "bitflags" => bits = Some(jsonb_datum_to_u64(val)?),
270 other => return Err(format!("unexpected acl_mode field: {other}")),
271 }
272 }
273 let bits = bits.ok_or_else(|| "missing acl_mode bitflags".to_string())?;
274 AclMode::from_bits(bits).ok_or_else(|| format!("invalid acl_mode bitflags: {bits}"))
275}
276
277fn jsonb_datum_to_role_id(d: Datum) -> Result<RoleId, String> {
279 match d {
280 Datum::String("Public") => Ok(RoleId::Public),
281 Datum::String(other) => Err(format!("unexpected role ID variant: {other}")),
282 Datum::Map(dict) => {
283 let (key, val) = dict.iter().next().ok_or_else(|| "empty".to_string())?;
284 let n = jsonb_datum_to_u64(val)?;
285 match key {
286 "User" => Ok(RoleId::User(n)),
287 "System" => Ok(RoleId::System(n)),
288 "Predefined" => Ok(RoleId::Predefined(n)),
289 other => Err(format!("unexpected role ID variant: {other}")),
290 }
291 }
292 _ => Err("expected string or object".into()),
293 }
294}
295
296#[sqlfunc]
300fn parse_catalog_id<'a>(a: JsonbRef<'a>) -> Result<String, EvalError> {
301 let parse = || match a.into_datum() {
302 Datum::String(variant) => match variant {
304 "Explain" => Ok("e".to_string()),
305 "Public" => Ok("p".to_string()),
306 other => Err(format!("unexpected ID variant: {other}")),
307 },
308 Datum::Map(dict) => {
310 let (key, val) = dict.iter().next().ok_or_else(|| "empty".to_string())?;
311 let prefix = match key {
312 "IntrospectionSourceIndex" => "si",
313 "Predefined" => "g",
314 "System" => "s",
315 "Transient" => "t",
316 "User" => "u",
317 other => return Err(format!("unexpected ID variant: {other}")),
318 };
319 let n = jsonb_datum_to_u64(val)?;
320 Ok(format!("{prefix}{n}"))
321 }
322 _ => Err("expected string or object".into()),
323 };
324
325 parse().map_err(|e| EvalError::InvalidCatalogJson(e.into()))
326}
327
328#[sqlfunc]
330fn parse_catalog_privileges<'a>(a: JsonbRef<'a>) -> Result<ArrayRustType<MzAclItem>, EvalError> {
331 let parse_one = |datum| match datum {
332 Datum::Map(dict) => {
333 let mut grantee = None;
334 let mut grantor = None;
335 let mut acl_mode = None;
336 for (key, val) in dict.iter() {
337 match key {
338 "grantee" => {
339 let id = jsonb_datum_to_role_id(val)?;
340 grantee = Some(id);
341 }
342 "grantor" => {
343 let id = jsonb_datum_to_role_id(val)?;
344 grantor = Some(id);
345 }
346 "acl_mode" => {
347 acl_mode = Some(jsonb_datum_to_acl_mode(val)?);
348 }
349 other => return Err(format!("unexpected privilege field: {other}")),
350 }
351 }
352 Ok(MzAclItem {
353 grantee: grantee.ok_or_else(|| format!("missing grantee: {dict:?}"))?,
354 grantor: grantor.ok_or_else(|| "missing grantor in privilege".to_string())?,
355 acl_mode: acl_mode.ok_or_else(|| "missing acl_mode in privilege".to_string())?,
356 })
357 }
358 other => Err(format!("expected object in array, found: {other}")),
359 };
360
361 let parse = || match a.into_datum() {
362 Datum::List(list) => {
363 let mut result = Vec::new();
364 for item in list.iter() {
365 result.push(parse_one(item)?);
366 }
367 Ok(result)
368 }
369 _ => Err("expected array".to_string()),
370 };
371
372 parse()
373 .map(ArrayRustType)
374 .map_err(|e| EvalError::InvalidCatalogJson(e.into()))
375}
376
377#[sqlfunc]
380fn parse_catalog_acl_mode<'a>(a: JsonbRef<'a>) -> Result<String, EvalError> {
381 jsonb_datum_to_acl_mode(a.into_datum())
382 .map(|mode| mode.to_string())
383 .map_err(|e| EvalError::InvalidCatalogJson(e.into()))
384}
385
386fn option_string<T: AstInfo>(value: &WithOptionValue<T>) -> Option<String> {
397 match value {
398 WithOptionValue::Value(Value::String(s)) => Some(s.clone()),
399 WithOptionValue::Ident(ident) => Some(ident.clone().into_string()),
400 WithOptionValue::UnresolvedItemName(UnresolvedItemName(parts)) if parts.len() == 1 => {
401 Some(parts[0].clone().into_string())
402 }
403 _ => None,
404 }
405}
406
407#[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#[derive(Clone, PartialEq, ::prost::Message)]
824struct PostgresPublicationDetailsSubset {
825 #[prost(string, tag = "2")]
826 slot: String,
827 #[prost(uint64, optional, tag = "3")]
828 timeline_id: Option<u64>,
829}
830
831#[sqlfunc]
842fn parse_postgres_source_details<'a>(a: &'a str) -> Result<Jsonb, EvalError> {
843 let parse = || -> Result<serde_json::Value, String> {
844 let mut stmts = mz_sql_parser::parser::parse_statements(a)
845 .map_err(|e| format!("failed to parse create_sql: {e}"))?;
846 let stmt = match stmts.len() {
847 1 => stmts.remove(0).ast,
848 n => return Err(format!("expected a single statement, found {n}")),
849 };
850
851 use mz_sql_parser::ast::CreateSourceConnection;
852 use mz_sql_parser::ast::Statement::CreateSource;
853 let options = match stmt {
854 CreateSource(stmt) => match stmt.connection {
855 CreateSourceConnection::Postgres { options, .. } => options,
856 _ => return Ok(serde_json::Value::Null),
857 },
858 _ => return Ok(serde_json::Value::Null),
859 };
860
861 let details_hex = options
862 .into_iter()
863 .find(|opt| opt.name == PgConfigOptionName::Details)
864 .and_then(|opt| match opt.value {
865 Some(WithOptionValue::Value(Value::String(s))) => Some(s),
866 _ => None,
867 })
868 .ok_or("missing DETAILS option on postgres source")?;
869
870 let details_bytes =
871 hex::decode(&details_hex).map_err(|e| format!("DETAILS is not valid hex: {e}"))?;
872
873 let details = PostgresPublicationDetailsSubset::decode(&*details_bytes)
874 .map_err(|e| format!("DETAILS is not a valid publication-details proto: {e}"))?;
875
876 Ok(json!({
877 "slot": details.slot,
878 "timeline_id": details.timeline_id,
879 }))
880 };
881
882 let val = parse().map_err(|e| EvalError::InvalidCatalogJson(e.into()))?;
883 let jsonb = Jsonb::from_serde_json(val).expect("valid JSONB");
884 Ok(jsonb)
885}
886
887#[sqlfunc]
899fn parse_kafka_source_details<'a>(a: &'a str) -> Result<Jsonb, EvalError> {
900 fn get_item_id(item: RawItemName) -> Result<String, &'static str> {
901 match item {
902 RawItemName::Id(id, _, _) => Ok(id),
903 RawItemName::Name(_) => Err("unresolved item name"),
904 }
905 }
906
907 let parse = || -> Result<serde_json::Value, String> {
908 let mut stmts = mz_sql_parser::parser::parse_statements(a)
909 .map_err(|e| format!("failed to parse create_sql: {e}"))?;
910 let stmt = match stmts.len() {
911 1 => stmts.remove(0).ast,
912 n => return Err(format!("expected a single statement, found {n}")),
913 };
914
915 use mz_sql_parser::ast::CreateSourceConnection;
916 use mz_sql_parser::ast::Statement::CreateSource;
917 let (connection, options) = match stmt {
918 CreateSource(stmt) => match stmt.connection {
919 CreateSourceConnection::Kafka {
920 connection,
921 options,
922 } => (connection, options),
923 _ => return Ok(serde_json::Value::Null),
924 },
925 _ => return Ok(serde_json::Value::Null),
926 };
927
928 let connection_id = get_item_id(connection)?;
929
930 let mut topic: Option<String> = None;
931 let mut group_id_prefix: Option<String> = None;
932 for opt in options {
933 let string_value = opt.value.as_ref().and_then(option_string);
934 match opt.name {
935 KafkaSourceConfigOptionName::Topic => topic = string_value,
936 KafkaSourceConfigOptionName::GroupIdPrefix => group_id_prefix = string_value,
937 _ => {}
938 }
939 }
940
941 let topic = topic.ok_or("missing TOPIC option on kafka source")?;
942
943 Ok(json!({
944 "topic": topic,
945 "group_id_prefix": group_id_prefix,
946 "connection_id": connection_id,
947 }))
948 };
949
950 let val = parse().map_err(|e| EvalError::InvalidCatalogJson(e.into()))?;
951 let jsonb = Jsonb::from_serde_json(val).expect("valid JSONB");
952 Ok(jsonb)
953}
954
955#[sqlfunc]
990fn parse_source_export_details<'a>(a: &'a str) -> Result<Jsonb, EvalError> {
991 fn get_item_id(item: RawItemName) -> Result<String, &'static str> {
992 match item {
993 RawItemName::Id(id, _, _) => Ok(id),
994 RawItemName::Name(_) => Err("unresolved item name"),
995 }
996 }
997
998 fn format_name<T: AstInfo>(fmt: &Format<T>) -> &'static str {
999 match fmt {
1000 Format::Bytes => "bytes",
1001 Format::Avro(_) => "avro",
1002 Format::Protobuf(_) => "protobuf",
1003 Format::Regex(_) => "regex",
1004 Format::Csv { .. } => "csv",
1005 Format::Json { .. } => "json",
1006 Format::Text => "text",
1007 }
1008 }
1009
1010 fn bare_format_has_key<T: AstInfo>(fmt: &Format<T>) -> bool {
1014 match fmt {
1015 Format::Avro(AvroSchema::Csr { csr_connection }) => csr_connection
1016 .seed
1017 .as_ref()
1018 .is_some_and(|seed| seed.key_schema.is_some()),
1019 Format::Protobuf(ProtobufSchema::Csr { csr_connection }) => csr_connection
1020 .seed
1021 .as_ref()
1022 .is_some_and(|seed| seed.key.is_some()),
1023 _ => false,
1024 }
1025 }
1026
1027 fn key_value_formats<T: AstInfo>(
1028 spec: &FormatSpecifier<T>,
1029 ) -> (Option<&'static str>, Option<&'static str>) {
1030 match spec {
1031 FormatSpecifier::KeyValue { key, value } => {
1032 (Some(format_name(key)), Some(format_name(value)))
1033 }
1034 FormatSpecifier::Bare(fmt) => {
1035 let value = Some(format_name(fmt));
1036 let key = bare_format_has_key(fmt).then(|| format_name(fmt));
1037 (key, value)
1038 }
1039 }
1040 }
1041
1042 fn envelope_name(envelope: &SourceEnvelope) -> &'static str {
1043 match envelope {
1044 SourceEnvelope::None => "none",
1045 SourceEnvelope::Debezium => "debezium",
1046 SourceEnvelope::Upsert {
1047 value_decode_err_policy,
1048 } => {
1049 if value_decode_err_policy
1050 .iter()
1051 .any(|p| matches!(p, SourceErrorPolicy::Inline { .. }))
1052 {
1053 "upsert-value-err-inline"
1054 } else {
1055 "upsert"
1056 }
1057 }
1058 SourceEnvelope::CdcV2 => "materialize",
1059 }
1060 }
1061
1062 let parse = || -> Result<serde_json::Value, String> {
1063 let mut stmts = mz_sql_parser::parser::parse_statements(a)
1064 .map_err(|e| format!("failed to parse create_sql: {e}"))?;
1065 let stmt = match stmts.len() {
1066 1 => stmts.remove(0).ast,
1067 n => return Err(format!("expected a single statement, found {n}")),
1068 };
1069
1070 use mz_sql_parser::ast::Statement::{CreateSubsource, CreateTableFromSource};
1071 match stmt {
1072 CreateTableFromSource(stmt) => {
1073 let source_id = get_item_id(stmt.source)?;
1074 let external_reference = stmt
1075 .external_reference
1076 .ok_or("missing external reference on CREATE TABLE FROM SOURCE")?
1077 .0
1078 .into_iter()
1079 .map(|ident| ident.into_string())
1080 .collect::<Vec<_>>();
1081
1082 let envelope_type = stmt.envelope.as_ref().map(envelope_name);
1083 let (key_format, value_format) = match &stmt.format {
1084 Some(spec) => key_value_formats(spec),
1085 None => (None, None),
1086 };
1087
1088 Ok(json!({
1089 "source_id": source_id,
1090 "external_reference": external_reference,
1091 "envelope_type": envelope_type,
1092 "key_format": key_format,
1093 "value_format": value_format,
1094 }))
1095 }
1096 CreateSubsource(stmt) => {
1097 let is_progress = stmt
1100 .with_options
1101 .iter()
1102 .any(|o| matches!(o.name, CreateSubsourceOptionName::Progress));
1103 if is_progress {
1104 return Ok(serde_json::Value::Null);
1105 }
1106
1107 let source_id = stmt
1108 .of_source
1109 .ok_or("non-progress CREATE SUBSOURCE without OF SOURCE")
1110 .and_then(get_item_id)?;
1111
1112 let external_reference = stmt
1113 .with_options
1114 .into_iter()
1115 .find(|o| matches!(o.name, CreateSubsourceOptionName::ExternalReference))
1116 .and_then(|o| match o.value {
1117 Some(WithOptionValue::UnresolvedItemName(name)) => Some(name),
1118 _ => None,
1119 })
1120 .ok_or("CREATE SUBSOURCE missing EXTERNAL REFERENCE option")?
1121 .0
1122 .into_iter()
1123 .map(|ident| ident.into_string())
1124 .collect::<Vec<_>>();
1125
1126 Ok(json!({
1127 "source_id": source_id,
1128 "external_reference": external_reference,
1129 "envelope_type": serde_json::Value::Null,
1130 "key_format": serde_json::Value::Null,
1131 "value_format": serde_json::Value::Null,
1132 }))
1133 }
1134 _ => Ok(serde_json::Value::Null),
1135 }
1136 };
1137
1138 let val = parse().map_err(|e| EvalError::InvalidCatalogJson(e.into()))?;
1139 let jsonb = Jsonb::from_serde_json(val).expect("valid JSONB");
1140 Ok(jsonb)
1141}
1142
1143#[sqlfunc]
1188fn parse_connection_details<'a>(a: &'a str) -> Result<Jsonb, EvalError> {
1189 fn broker_addresses<T: AstInfo>(values: &[ConnectionOption<T>]) -> Vec<String> {
1194 let mut brokers = Vec::new();
1195 for opt in values {
1196 match (&opt.name, &opt.value) {
1197 (ConnectionOptionName::Broker, Some(WithOptionValue::ConnectionKafkaBroker(b))) => {
1198 brokers.push(b.address.clone());
1199 }
1200 (ConnectionOptionName::Brokers, Some(WithOptionValue::Sequence(seq))) => {
1201 for v in seq {
1202 if let WithOptionValue::ConnectionKafkaBroker(b) = v {
1203 brokers.push(b.address.clone());
1204 }
1205 }
1206 }
1207 _ => {}
1208 }
1209 }
1210 brokers
1211 }
1212
1213 fn string_option<T: AstInfo>(
1214 values: &[ConnectionOption<T>],
1215 name: ConnectionOptionName,
1216 ) -> Option<String> {
1217 values
1218 .iter()
1219 .find(|o| o.name == name)
1220 .and_then(|o| o.value.as_ref())
1221 .and_then(option_string)
1222 }
1223
1224 fn secret_id_option<T: AstInfo<ItemName = RawItemName>>(
1228 values: &[ConnectionOption<T>],
1229 name: ConnectionOptionName,
1230 ) -> Option<String> {
1231 values.iter().find_map(|o| match &o.value {
1232 Some(WithOptionValue::Secret(RawItemName::Id(id, _, _))) if o.name == name => {
1233 Some(id.clone())
1234 }
1235 _ => None,
1236 })
1237 }
1238
1239 let parse = || -> Result<serde_json::Value, String> {
1240 let mut stmts = mz_sql_parser::parser::parse_statements(a)
1241 .map_err(|e| format!("failed to parse create_sql: {e}"))?;
1242 let stmt = match stmts.len() {
1243 1 => stmts.remove(0).ast,
1244 n => return Err(format!("expected a single statement, found {n}")),
1245 };
1246
1247 use mz_sql_parser::ast::Statement::CreateConnection;
1248 let stmt = match stmt {
1249 CreateConnection(stmt) => stmt,
1250 _ => return Ok(serde_json::Value::Null),
1251 };
1252
1253 match stmt.connection_type {
1254 CreateConnectionType::Kafka => Ok(json!({
1255 "brokers": broker_addresses(&stmt.values),
1256 "progress_topic": string_option(&stmt.values, ConnectionOptionName::ProgressTopic),
1257 })),
1258 CreateConnectionType::Ssh => Ok(json!({
1259 "public_key_1": string_option(&stmt.values, ConnectionOptionName::PublicKey1),
1260 "public_key_2": string_option(&stmt.values, ConnectionOptionName::PublicKey2),
1261 })),
1262 CreateConnectionType::Aws => {
1263 let assume_role_arn =
1264 string_option(&stmt.values, ConnectionOptionName::AssumeRoleArn);
1265 let auth_kind = if assume_role_arn.is_some() {
1266 "assume-role"
1267 } else {
1268 "credentials"
1269 };
1270 Ok(json!({
1271 "auth_kind": auth_kind,
1272 "endpoint": string_option(&stmt.values, ConnectionOptionName::Endpoint)
1276 .filter(|s| !s.is_empty()),
1277 "region": string_option(&stmt.values, ConnectionOptionName::Region),
1278 "access_key_id": string_option(&stmt.values, ConnectionOptionName::AccessKeyId),
1279 "access_key_id_secret_id":
1280 secret_id_option(&stmt.values, ConnectionOptionName::AccessKeyId),
1281 "secret_access_key_secret_id":
1282 secret_id_option(&stmt.values, ConnectionOptionName::SecretAccessKey),
1283 "session_token": string_option(&stmt.values, ConnectionOptionName::SessionToken),
1284 "session_token_secret_id":
1285 secret_id_option(&stmt.values, ConnectionOptionName::SessionToken),
1286 "assume_role_arn": assume_role_arn,
1287 "assume_role_session_name":
1288 string_option(&stmt.values, ConnectionOptionName::AssumeRoleSessionName),
1289 }))
1290 }
1291 _ => Ok(serde_json::Value::Null),
1292 }
1293 };
1294
1295 let val = parse().map_err(|e| EvalError::InvalidCatalogJson(e.into()))?;
1296 let jsonb = Jsonb::from_serde_json(val).expect("valid JSONB");
1297 Ok(jsonb)
1298}
1299
1300#[cfg(test)]
1301mod tests {
1302 use mz_repr::adt::jsonb::Jsonb;
1303 use prost::Message as _;
1304 use serde_json::json;
1305
1306 use crate::EvalError;
1307
1308 fn encode_pg_details(slot: &str, timeline_id: Option<u64>) -> String {
1311 let details = super::PostgresPublicationDetailsSubset {
1312 slot: slot.to_string(),
1313 timeline_id,
1314 };
1315 hex::encode(details.encode_to_vec())
1316 }
1317
1318 fn pg_source_sql(details_hex: &str) -> String {
1319 format!(
1320 "CREATE SOURCE \"materialize\".\"public\".\"pg_src\" \
1321 IN CLUSTER [u42] \
1322 FROM POSTGRES CONNECTION [u10 AS \"materialize\".\"public\".\"pg_conn\"] \
1323 (DETAILS = '{details_hex}', PUBLICATION = 'mz_source') \
1324 FOR ALL TABLES"
1325 )
1326 }
1327
1328 fn kafka_source_sql(with_prefix: bool) -> String {
1329 let prefix_opt = if with_prefix {
1330 ", GROUP ID PREFIX 'my-prefix-'"
1331 } else {
1332 ""
1333 };
1334 format!(
1335 "CREATE SOURCE \"materialize\".\"public\".\"k_src\" \
1336 IN CLUSTER [u42] \
1337 FROM KAFKA CONNECTION [u11 AS \"materialize\".\"public\".\"k_conn\"] \
1338 (TOPIC 'test'{prefix_opt}) FORMAT TEXT"
1339 )
1340 }
1341
1342 fn as_serde(jsonb: Jsonb) -> serde_json::Value {
1343 jsonb.as_ref().to_serde_json()
1344 }
1345
1346 #[mz_ore::test]
1349 #[cfg_attr(miri, ignore)] fn pg_happy_path_with_timeline() {
1351 let hex = encode_pg_details("materialize_abc", Some(42));
1352 let sql = pg_source_sql(&hex);
1353 let out = super::parse_postgres_source_details(&sql).expect("ok");
1354 assert_eq!(
1355 as_serde(out),
1356 json!({ "slot": "materialize_abc", "timeline_id": 42 }),
1357 );
1358 }
1359
1360 #[mz_ore::test]
1361 fn pg_happy_path_null_timeline() {
1362 let hex = encode_pg_details("materialize_legacy", None);
1365 let sql = pg_source_sql(&hex);
1366 let out = super::parse_postgres_source_details(&sql).expect("ok");
1367 assert_eq!(
1368 as_serde(out),
1369 json!({ "slot": "materialize_legacy", "timeline_id": null }),
1370 );
1371 }
1372
1373 #[mz_ore::test]
1374 fn pg_non_postgres_source_returns_null_jsonb() {
1375 let sql = "CREATE SOURCE \"materialize\".\"public\".\"lg\" \
1376 IN CLUSTER [u42] FROM LOAD GENERATOR COUNTER";
1377 let out = super::parse_postgres_source_details(sql).expect("ok");
1378 assert_eq!(as_serde(out), serde_json::Value::Null);
1379 }
1380
1381 #[mz_ore::test]
1382 #[cfg_attr(miri, ignore)] fn pg_non_create_source_returns_null_jsonb() {
1384 let sql = "CREATE VIEW v AS SELECT 1";
1385 let out = super::parse_postgres_source_details(sql).expect("ok");
1386 assert_eq!(as_serde(out), serde_json::Value::Null);
1387 }
1388
1389 #[mz_ore::test]
1390 fn pg_missing_details_option_errors() {
1391 let sql = "CREATE SOURCE \"materialize\".\"public\".\"pg_src\" \
1392 IN CLUSTER [u42] \
1393 FROM POSTGRES CONNECTION [u10 AS \"materialize\".\"public\".\"pg_conn\"] \
1394 (PUBLICATION = 'mz_source') FOR ALL TABLES";
1395 let err = super::parse_postgres_source_details(sql).unwrap_err();
1396 assert!(
1397 matches!(err, EvalError::InvalidCatalogJson(msg) if msg.contains("missing DETAILS")),
1398 "wrong error variant/message"
1399 );
1400 }
1401
1402 #[mz_ore::test]
1403 fn pg_malformed_hex_errors() {
1404 let sql = pg_source_sql("not-hex!!");
1405 let err = super::parse_postgres_source_details(&sql).unwrap_err();
1406 assert!(
1407 matches!(err, EvalError::InvalidCatalogJson(msg) if msg.contains("valid hex")),
1408 "wrong error variant/message"
1409 );
1410 }
1411
1412 #[mz_ore::test]
1413 fn pg_malformed_proto_errors() {
1414 let sql = pg_source_sql("ffff");
1417 let err = super::parse_postgres_source_details(&sql).unwrap_err();
1418 assert!(
1419 matches!(err, EvalError::InvalidCatalogJson(msg) if msg.contains("publication-details proto")),
1420 "wrong error variant/message"
1421 );
1422 }
1423
1424 #[mz_ore::test]
1427 fn kafka_happy_path_with_prefix() {
1428 let sql = kafka_source_sql(true);
1429 let out = super::parse_kafka_source_details(&sql).expect("ok");
1430 assert_eq!(
1431 as_serde(out),
1432 json!({
1433 "topic": "test",
1434 "group_id_prefix": "my-prefix-",
1435 "connection_id": "u11",
1436 }),
1437 );
1438 }
1439
1440 #[mz_ore::test]
1441 fn kafka_happy_path_without_prefix() {
1442 let sql = kafka_source_sql(false);
1443 let out = super::parse_kafka_source_details(&sql).expect("ok");
1444 assert_eq!(
1445 as_serde(out),
1446 json!({
1447 "topic": "test",
1448 "group_id_prefix": null,
1449 "connection_id": "u11",
1450 }),
1451 );
1452 }
1453
1454 #[mz_ore::test]
1455 fn kafka_non_kafka_source_returns_null_jsonb() {
1456 let sql = "CREATE SOURCE \"materialize\".\"public\".\"lg\" \
1457 IN CLUSTER [u42] FROM LOAD GENERATOR COUNTER";
1458 let out = super::parse_kafka_source_details(sql).expect("ok");
1459 assert_eq!(as_serde(out), serde_json::Value::Null);
1460 }
1461
1462 #[mz_ore::test]
1463 fn kafka_missing_topic_errors() {
1464 let sql = "CREATE SOURCE \"materialize\".\"public\".\"k_src\" \
1465 IN CLUSTER [u42] \
1466 FROM KAFKA CONNECTION [u11 AS \"materialize\".\"public\".\"k_conn\"] \
1467 FORMAT TEXT";
1468 let err = super::parse_kafka_source_details(sql).unwrap_err();
1469 assert!(
1470 matches!(err, EvalError::InvalidCatalogJson(msg) if msg.contains("missing TOPIC")),
1471 "wrong error variant/message"
1472 );
1473 }
1474
1475 #[mz_ore::test]
1476 fn kafka_unquoted_topic_and_prefix() {
1477 let sql = "CREATE SOURCE \"materialize\".\"public\".\"k_src\" \
1481 IN CLUSTER [u42] \
1482 FROM KAFKA CONNECTION [u11 AS \"materialize\".\"public\".\"k_conn\"] \
1483 (TOPIC = my_topic, GROUP ID PREFIX = my_prefix) FORMAT TEXT";
1484 let out = super::parse_kafka_source_details(sql).expect("ok");
1485 let out = as_serde(out);
1486 assert_eq!(out["topic"], json!("my_topic"));
1487 assert_eq!(out["group_id_prefix"], json!("my_prefix"));
1488 }
1489
1490 #[mz_ore::test]
1491 fn kafka_unresolved_connection_errors() {
1492 let sql = "CREATE SOURCE \"materialize\".\"public\".\"k_src\" \
1496 IN CLUSTER [u42] \
1497 FROM KAFKA CONNECTION k_conn (TOPIC 'test') FORMAT TEXT";
1498 let err = super::parse_kafka_source_details(sql).unwrap_err();
1499 assert!(
1500 matches!(err, EvalError::InvalidCatalogJson(msg) if msg.contains("unresolved item name")),
1501 "wrong error variant/message"
1502 );
1503 }
1504
1505 fn table_from_source_sql(reference: &str, suffix: &str) -> String {
1508 format!(
1509 "CREATE TABLE \"materialize\".\"public\".\"tbl\" \
1510 FROM SOURCE [u1 AS \"materialize\".\"public\".\"src\"] \
1511 (REFERENCE = {reference}){suffix}"
1512 )
1513 }
1514
1515 #[mz_ore::test]
1516 fn export_table_postgres_style_no_format() {
1517 let sql = table_from_source_sql("\"db\".\"public\".\"t\"", "");
1520 let out = super::parse_source_export_details(&sql).expect("ok");
1521 assert_eq!(
1522 as_serde(out),
1523 json!({
1524 "source_id": "u1",
1525 "external_reference": ["db", "public", "t"],
1526 "envelope_type": null,
1527 "key_format": null,
1528 "value_format": null,
1529 }),
1530 );
1531 }
1532
1533 #[mz_ore::test]
1534 fn export_table_kafka_bare_value_only() {
1535 let sql = table_from_source_sql("\"topic\"", " FORMAT TEXT ENVELOPE NONE");
1537 let out = super::parse_source_export_details(&sql).expect("ok");
1538 assert_eq!(
1539 as_serde(out),
1540 json!({
1541 "source_id": "u1",
1542 "external_reference": ["topic"],
1543 "envelope_type": "none",
1544 "key_format": null,
1545 "value_format": "text",
1546 }),
1547 );
1548 }
1549
1550 #[mz_ore::test]
1551 fn export_table_kafka_omitted_envelope_is_null() {
1552 let sql = table_from_source_sql("\"topic\"", " FORMAT TEXT");
1556 let out = super::parse_source_export_details(&sql).expect("ok");
1557 assert_eq!(as_serde(out)["envelope_type"], serde_json::Value::Null);
1558 }
1559
1560 #[mz_ore::test]
1561 fn export_table_kafka_key_value_format() {
1562 let sql = table_from_source_sql(
1563 "\"topic\"",
1564 " KEY FORMAT TEXT VALUE FORMAT TEXT ENVELOPE NONE",
1565 );
1566 let out = super::parse_source_export_details(&sql).expect("ok");
1567 assert_eq!(
1568 as_serde(out),
1569 json!({
1570 "source_id": "u1",
1571 "external_reference": ["topic"],
1572 "envelope_type": "none",
1573 "key_format": "text",
1574 "value_format": "text",
1575 }),
1576 );
1577 }
1578
1579 #[mz_ore::test]
1580 #[cfg_attr(miri, ignore)] fn export_table_kafka_bare_avro_seed_with_key() {
1582 let sql = table_from_source_sql(
1586 "\"topic\"",
1587 " FORMAT AVRO USING CONFLUENT SCHEMA REGISTRY \
1588 CONNECTION [u5 AS \"materialize\".\"public\".\"csr\"] \
1589 SEED KEY SCHEMA 'k' VALUE SCHEMA 'v' ENVELOPE UPSERT",
1590 );
1591 let out = super::parse_source_export_details(&sql).expect("ok");
1592 assert_eq!(
1593 as_serde(out),
1594 json!({
1595 "source_id": "u1",
1596 "external_reference": ["topic"],
1597 "envelope_type": "upsert",
1598 "key_format": "avro",
1599 "value_format": "avro",
1600 }),
1601 );
1602 }
1603
1604 #[mz_ore::test]
1605 #[cfg_attr(miri, ignore)] fn export_table_kafka_bare_avro_seed_without_key() {
1607 let sql = table_from_source_sql(
1609 "\"topic\"",
1610 " FORMAT AVRO USING CONFLUENT SCHEMA REGISTRY \
1611 CONNECTION [u5 AS \"materialize\".\"public\".\"csr\"] \
1612 SEED VALUE SCHEMA 'v' ENVELOPE NONE",
1613 );
1614 let out = super::parse_source_export_details(&sql).expect("ok");
1615 assert_eq!(
1616 as_serde(out),
1617 json!({
1618 "source_id": "u1",
1619 "external_reference": ["topic"],
1620 "envelope_type": "none",
1621 "key_format": null,
1622 "value_format": "avro",
1623 }),
1624 );
1625 }
1626
1627 #[mz_ore::test]
1628 fn export_subsource_non_progress() {
1629 let sql = "CREATE SUBSOURCE \"materialize\".\"public\".\"sub\" (id int4) \
1632 OF SOURCE [u1 AS \"materialize\".\"public\".\"src\"] \
1633 WITH (EXTERNAL REFERENCE = \"db\".\"public\".\"t\")";
1634 let out = super::parse_source_export_details(sql).expect("ok");
1635 assert_eq!(
1636 as_serde(out),
1637 json!({
1638 "source_id": "u1",
1639 "external_reference": ["db", "public", "t"],
1640 "envelope_type": null,
1641 "key_format": null,
1642 "value_format": null,
1643 }),
1644 );
1645 }
1646
1647 #[mz_ore::test]
1648 fn export_progress_subsource_returns_null_jsonb() {
1649 let sql = "CREATE SUBSOURCE \"materialize\".\"public\".\"progress\" (id int4) \
1650 WITH (PROGRESS)";
1651 let out = super::parse_source_export_details(sql).expect("ok");
1652 assert_eq!(as_serde(out), serde_json::Value::Null);
1653 }
1654
1655 #[mz_ore::test]
1656 #[cfg_attr(miri, ignore)] fn export_non_source_export_returns_null_jsonb() {
1658 let sql = "CREATE VIEW v AS SELECT 1";
1659 let out = super::parse_source_export_details(sql).expect("ok");
1660 assert_eq!(as_serde(out), serde_json::Value::Null);
1661 }
1662
1663 #[mz_ore::test]
1664 fn export_unresolved_source_name_errors() {
1665 let sql = "CREATE TABLE \"materialize\".\"public\".\"tbl\" \
1666 FROM SOURCE src (REFERENCE = \"topic\")";
1667 let err = super::parse_source_export_details(sql).unwrap_err();
1668 assert!(
1669 matches!(err, EvalError::InvalidCatalogJson(msg) if msg.contains("unresolved item name")),
1670 "wrong error variant/message"
1671 );
1672 }
1673
1674 #[mz_ore::test]
1677 fn connection_kafka_single_broker_default_progress() {
1678 let sql = "CREATE CONNECTION \"materialize\".\"public\".\"c\" TO KAFKA \
1681 (BROKER = 'localhost:9092', SECURITY PROTOCOL = plaintext)";
1682 let out = super::parse_connection_details(sql).expect("ok");
1683 assert_eq!(
1684 as_serde(out),
1685 json!({
1686 "brokers": ["localhost:9092"],
1687 "progress_topic": null,
1688 }),
1689 );
1690 }
1691
1692 #[mz_ore::test]
1693 fn connection_kafka_explicit_progress_topic() {
1694 let sql = "CREATE CONNECTION \"materialize\".\"public\".\"c\" TO KAFKA \
1695 (BROKER = 'localhost:9092', PROGRESS TOPIC = 'override', \
1696 SECURITY PROTOCOL = plaintext)";
1697 let out = super::parse_connection_details(sql).expect("ok");
1698 assert_eq!(
1699 as_serde(out),
1700 json!({
1701 "brokers": ["localhost:9092"],
1702 "progress_topic": "override",
1703 }),
1704 );
1705 }
1706
1707 #[mz_ore::test]
1708 fn connection_kafka_broker_list() {
1709 let sql = "CREATE CONNECTION \"materialize\".\"public\".\"c\" TO KAFKA \
1710 (BROKERS ('b1:9092', 'b2:9092'), SECURITY PROTOCOL = plaintext)";
1711 let out = super::parse_connection_details(sql).expect("ok");
1712 assert_eq!(
1713 as_serde(out),
1714 json!({
1715 "brokers": ["b1:9092", "b2:9092"],
1716 "progress_topic": null,
1717 }),
1718 );
1719 }
1720
1721 #[mz_ore::test]
1722 fn connection_ssh_public_keys() {
1723 let sql = "CREATE CONNECTION \"materialize\".\"public\".\"c\" TO SSH TUNNEL \
1724 (HOST = 'ssh.example.com', PORT = 22, USER = 'mz', \
1725 PUBLIC KEY 1 = 'ssh-ed25519 AAAA', PUBLIC KEY 2 = 'ssh-ed25519 BBBB')";
1726 let out = super::parse_connection_details(sql).expect("ok");
1727 assert_eq!(
1728 as_serde(out),
1729 json!({
1730 "public_key_1": "ssh-ed25519 AAAA",
1731 "public_key_2": "ssh-ed25519 BBBB",
1732 }),
1733 );
1734 }
1735
1736 #[mz_ore::test]
1737 fn connection_aws_credentials_inline_key() {
1738 let sql = "CREATE CONNECTION \"materialize\".\"public\".\"c\" TO AWS \
1741 (ACCESS KEY ID = 'AKIAEXAMPLE', \
1742 SECRET ACCESS KEY = SECRET [u1 AS \"materialize\".\"public\".\"sk\"])";
1743 let out = super::parse_connection_details(sql).expect("ok");
1744 assert_eq!(
1745 as_serde(out),
1746 json!({
1747 "auth_kind": "credentials",
1748 "endpoint": null,
1749 "region": null,
1750 "access_key_id": "AKIAEXAMPLE",
1751 "access_key_id_secret_id": null,
1752 "secret_access_key_secret_id": "u1",
1753 "session_token": null,
1754 "session_token_secret_id": null,
1755 "assume_role_arn": null,
1756 "assume_role_session_name": null,
1757 }),
1758 );
1759 }
1760
1761 #[mz_ore::test]
1762 fn connection_aws_credentials_secret_key_and_session_token() {
1763 let sql = "CREATE CONNECTION \"materialize\".\"public\".\"c\" TO AWS \
1766 (ENDPOINT = 'http://localhost', REGION = 'us-east-1', \
1767 ACCESS KEY ID = SECRET [u1 AS \"materialize\".\"public\".\"ak\"], \
1768 SECRET ACCESS KEY = SECRET [u2 AS \"materialize\".\"public\".\"sk\"], \
1769 SESSION TOKEN = SECRET [u3 AS \"materialize\".\"public\".\"st\"])";
1770 let out = super::parse_connection_details(sql).expect("ok");
1771 assert_eq!(
1772 as_serde(out),
1773 json!({
1774 "auth_kind": "credentials",
1775 "endpoint": "http://localhost",
1776 "region": "us-east-1",
1777 "access_key_id": null,
1778 "access_key_id_secret_id": "u1",
1779 "secret_access_key_secret_id": "u2",
1780 "session_token": null,
1781 "session_token_secret_id": "u3",
1782 "assume_role_arn": null,
1783 "assume_role_session_name": null,
1784 }),
1785 );
1786 }
1787
1788 #[mz_ore::test]
1789 fn connection_aws_assume_role() {
1790 let sql = "CREATE CONNECTION \"materialize\".\"public\".\"c\" TO AWS \
1793 (ASSUME ROLE ARN 'arn:aws:iam::123:role/mz', \
1794 ASSUME ROLE SESSION NAME 'sess')";
1795 let out = super::parse_connection_details(sql).expect("ok");
1796 assert_eq!(
1797 as_serde(out),
1798 json!({
1799 "auth_kind": "assume-role",
1800 "endpoint": null,
1801 "region": null,
1802 "access_key_id": null,
1803 "access_key_id_secret_id": null,
1804 "secret_access_key_secret_id": null,
1805 "session_token": null,
1806 "session_token_secret_id": null,
1807 "assume_role_arn": "arn:aws:iam::123:role/mz",
1808 "assume_role_session_name": "sess",
1809 }),
1810 );
1811 }
1812
1813 #[mz_ore::test]
1814 fn connection_kafka_unquoted_progress_topic() {
1815 let sql = "CREATE CONNECTION \"materialize\".\"public\".\"c\" TO KAFKA \
1818 (BROKER = 'localhost:9092', PROGRESS TOPIC = my_topic, \
1819 SECURITY PROTOCOL = plaintext)";
1820 let out = super::parse_connection_details(sql).expect("ok");
1821 assert_eq!(as_serde(out)["progress_topic"], json!("my_topic"));
1822 }
1823
1824 #[mz_ore::test]
1825 fn connection_aws_unquoted_option_values() {
1826 let sql = "CREATE CONNECTION \"materialize\".\"public\".\"c\" TO AWS \
1829 (ENDPOINT = localhost, REGION = useast1, \
1830 ASSUME ROLE ARN 'arn:aws:iam::123:role/mz', \
1831 ASSUME ROLE SESSION NAME = mysession)";
1832 let out = super::parse_connection_details(sql).expect("ok");
1833 let out = as_serde(out);
1834 assert_eq!(out["endpoint"], json!("localhost"));
1835 assert_eq!(out["region"], json!("useast1"));
1836 assert_eq!(out["assume_role_session_name"], json!("mysession"));
1837 }
1838
1839 #[mz_ore::test]
1840 fn connection_aws_empty_endpoint_is_null() {
1841 let sql = "CREATE CONNECTION \"materialize\".\"public\".\"c\" TO AWS \
1844 (ENDPOINT = '', ASSUME ROLE ARN 'arn:aws:iam::123:role/mz')";
1845 let out = super::parse_connection_details(sql).expect("ok");
1846 assert_eq!(as_serde(out)["endpoint"], serde_json::Value::Null);
1847 }
1848
1849 #[mz_ore::test]
1850 fn connection_other_type_returns_null_jsonb() {
1851 let sql = "CREATE CONNECTION \"materialize\".\"public\".\"c\" TO POSTGRES \
1853 (HOST = 'db', DATABASE = 'postgres', USER = 'mz')";
1854 let out = super::parse_connection_details(sql).expect("ok");
1855 assert_eq!(as_serde(out), serde_json::Value::Null);
1856 }
1857
1858 #[mz_ore::test]
1859 #[cfg_attr(miri, ignore)] fn connection_non_connection_returns_null_jsonb() {
1861 let sql = "CREATE VIEW v AS SELECT 1";
1862 let out = super::parse_connection_details(sql).expect("ok");
1863 assert_eq!(as_serde(out), serde_json::Value::Null);
1864 }
1865
1866 #[mz_ore::test]
1869 fn catalog_kafka_old_syntax_omitted_envelope_defaults_none() {
1870 let sql = "CREATE SOURCE \"materialize\".\"public\".\"k\" \
1874 IN CLUSTER [u42] \
1875 FROM KAFKA CONNECTION [u11 AS \"materialize\".\"public\".\"k_conn\"] \
1876 (TOPIC 'test') FORMAT TEXT \
1877 EXPOSE PROGRESS AS [u12 AS \"materialize\".\"public\".\"k_progress\"]";
1878 let out = super::parse_catalog_create_sql(sql).expect("ok");
1879 assert_eq!(as_serde(out)["envelope_type"], json!("none"));
1880 }
1881
1882 #[mz_ore::test]
1883 fn catalog_kafka_old_syntax_explicit_envelope() {
1884 let sql = "CREATE SOURCE \"materialize\".\"public\".\"k\" \
1885 IN CLUSTER [u42] \
1886 FROM KAFKA CONNECTION [u11 AS \"materialize\".\"public\".\"k_conn\"] \
1887 (TOPIC 'test') FORMAT BYTES ENVELOPE UPSERT \
1888 EXPOSE PROGRESS AS [u12 AS \"materialize\".\"public\".\"k_progress\"]";
1889 let out = super::parse_catalog_create_sql(sql).expect("ok");
1890 assert_eq!(as_serde(out)["envelope_type"], json!("upsert"));
1891 }
1892
1893 #[mz_ore::test]
1894 fn catalog_kafka_new_syntax_source_omits_envelope_type() {
1895 let sql = "CREATE SOURCE \"materialize\".\"public\".\"k\" \
1899 IN CLUSTER [u42] \
1900 FROM KAFKA CONNECTION [u11 AS \"materialize\".\"public\".\"k_conn\"]";
1901 let out = super::parse_catalog_create_sql(sql).expect("ok");
1902 assert_eq!(as_serde(out).get("envelope_type"), None);
1903 }
1904
1905 #[mz_ore::test]
1906 fn catalog_non_kafka_source_omits_envelope_type() {
1907 let sql = "CREATE SOURCE \"materialize\".\"public\".\"lg\" \
1910 IN CLUSTER [u42] FROM LOAD GENERATOR COUNTER";
1911 let out = super::parse_catalog_create_sql(sql).expect("ok");
1912 assert_eq!(as_serde(out).get("envelope_type"), None);
1913 }
1914
1915 const AVRO_FORMAT: &str = "FORMAT AVRO USING CONFLUENT SCHEMA REGISTRY CONNECTION \
1918 [u12 AS \"materialize\".\"public\".\"csr_conn\"]";
1919
1920 fn kafka_sink_sql(key: Option<&str>, format: &str, envelope: &str) -> String {
1923 let key_clause = key.map(|k| format!(" KEY ({k})")).unwrap_or_default();
1924 format!(
1925 "CREATE SINK \"materialize\".\"public\".\"snk\" \
1926 IN CLUSTER [u42] \
1927 FROM [u1 AS \"materialize\".\"public\".\"t\"] \
1928 INTO KAFKA CONNECTION [u10 AS \"materialize\".\"public\".\"k_conn\"] \
1929 (TOPIC 'sink-topic'){key_clause} {format} ENVELOPE {envelope}"
1930 )
1931 }
1932
1933 fn iceberg_sink_sql(mode: &str) -> String {
1934 format!(
1935 "CREATE SINK \"materialize\".\"public\".\"ice\" \
1936 IN CLUSTER [u42] \
1937 FROM [u1 AS \"materialize\".\"public\".\"t\"] \
1938 INTO ICEBERG CATALOG CONNECTION [u20 AS \"materialize\".\"public\".\"cat_conn\"] \
1939 (NAMESPACE 'ns', TABLE 'tbl') \
1940 USING AWS CONNECTION [u21 AS \"materialize\".\"public\".\"aws_conn\"] \
1941 MODE {mode}"
1942 )
1943 }
1944
1945 #[mz_ore::test]
1946 fn sink_kafka_bare_format_without_key() {
1947 let sql = kafka_sink_sql(None, "FORMAT JSON", "DEBEZIUM");
1948 let out = super::parse_catalog_create_sql(&sql).expect("ok");
1949 assert_eq!(
1950 as_serde(out),
1951 json!({
1952 "type": "sink",
1953 "sink_type": "kafka",
1954 "cluster_id": "u42",
1955 "connection_id": "u10",
1956 "topic": "sink-topic",
1957 "envelope_type": "debezium",
1958 "format": "json",
1959 "value_format": "json",
1960 }),
1961 );
1962 }
1963
1964 #[mz_ore::test]
1965 fn sink_kafka_bare_format_with_key_derives_key_format() {
1966 let sql = kafka_sink_sql(Some("a"), AVRO_FORMAT, "UPSERT");
1969 let out = super::parse_catalog_create_sql(&sql).expect("ok");
1970 assert_eq!(
1971 as_serde(out),
1972 json!({
1973 "type": "sink",
1974 "sink_type": "kafka",
1975 "cluster_id": "u42",
1976 "connection_id": "u10",
1977 "topic": "sink-topic",
1978 "envelope_type": "upsert",
1979 "format": "avro",
1980 "key_format": "avro",
1981 "value_format": "avro",
1982 }),
1983 );
1984 }
1985
1986 #[mz_ore::test]
1987 fn sink_kafka_bare_text_format_with_key_does_not_collapse() {
1988 let sql = kafka_sink_sql(Some("a"), "FORMAT TEXT", "UPSERT");
1991 let out = super::parse_catalog_create_sql(&sql).expect("ok");
1992 let out = as_serde(out);
1993 assert_eq!(out["format"], json!("key-text-value-text"));
1994 assert_eq!(out["key_format"], json!("text"));
1995 assert_eq!(out["value_format"], json!("text"));
1996 }
1997
1998 #[mz_ore::test]
1999 fn sink_kafka_key_value_json_collapses() {
2000 let sql = kafka_sink_sql(Some("a"), "KEY FORMAT JSON VALUE FORMAT JSON", "UPSERT");
2001 let out = as_serde(super::parse_catalog_create_sql(&sql).expect("ok"));
2002 assert_eq!(out["format"], json!("json"));
2003 assert_eq!(out["key_format"], json!("json"));
2004 assert_eq!(out["value_format"], json!("json"));
2005 }
2006
2007 #[mz_ore::test]
2008 fn sink_kafka_key_value_mixed_is_composite() {
2009 let sql = kafka_sink_sql(Some("a"), "KEY FORMAT TEXT VALUE FORMAT BYTES", "UPSERT");
2010 let out = as_serde(super::parse_catalog_create_sql(&sql).expect("ok"));
2011 assert_eq!(out["format"], json!("key-text-value-bytes"));
2012 assert_eq!(out["key_format"], json!("text"));
2013 assert_eq!(out["value_format"], json!("bytes"));
2014 }
2015
2016 #[mz_ore::test]
2017 fn sink_kafka_key_format_without_key_is_dropped() {
2018 let sql = kafka_sink_sql(None, "KEY FORMAT JSON VALUE FORMAT TEXT", "DEBEZIUM");
2022 let out = as_serde(super::parse_catalog_create_sql(&sql).expect("ok"));
2023 assert_eq!(out["format"], json!("text"));
2024 assert_eq!(out["key_format"], serde_json::Value::Null);
2025 assert_eq!(out["value_format"], json!("text"));
2026 }
2027
2028 #[mz_ore::test]
2029 fn sink_kafka_missing_topic_errors() {
2030 let sql = "CREATE SINK \"materialize\".\"public\".\"snk\" \
2031 IN CLUSTER [u42] \
2032 FROM [u1 AS \"materialize\".\"public\".\"t\"] \
2033 INTO KAFKA CONNECTION [u10 AS \"materialize\".\"public\".\"k_conn\"] \
2034 FORMAT JSON ENVELOPE DEBEZIUM";
2035 let err = super::parse_catalog_create_sql(sql).unwrap_err();
2036 assert!(
2037 matches!(err, EvalError::InvalidCatalogJson(msg) if msg.contains("missing TOPIC")),
2038 "wrong error variant/message"
2039 );
2040 }
2041
2042 #[mz_ore::test]
2043 fn sink_iceberg_upsert_mode() {
2044 let sql = iceberg_sink_sql("UPSERT");
2045 let out = super::parse_catalog_create_sql(&sql).expect("ok");
2046 assert_eq!(
2047 as_serde(out),
2048 json!({
2049 "type": "sink",
2050 "sink_type": "iceberg",
2051 "cluster_id": "u42",
2052 "connection_id": "u20",
2054 "namespace": "ns",
2055 "table": "tbl",
2056 "envelope_type": "upsert",
2057 }),
2058 );
2059 }
2060
2061 #[mz_ore::test]
2062 fn sink_iceberg_append_mode() {
2063 let sql = iceberg_sink_sql("APPEND");
2064 let out = as_serde(super::parse_catalog_create_sql(&sql).expect("ok"));
2065 assert_eq!(out["envelope_type"], json!("append"));
2067 }
2068
2069 #[mz_ore::test]
2070 fn sink_iceberg_missing_table_errors() {
2071 let sql = "CREATE SINK \"materialize\".\"public\".\"ice\" \
2072 IN CLUSTER [u42] \
2073 FROM [u1 AS \"materialize\".\"public\".\"t\"] \
2074 INTO ICEBERG CATALOG CONNECTION [u20 AS \"materialize\".\"public\".\"cat_conn\"] \
2075 (NAMESPACE 'ns') MODE UPSERT";
2076 let err = super::parse_catalog_create_sql(sql).unwrap_err();
2077 assert!(
2078 matches!(err, EvalError::InvalidCatalogJson(msg) if msg.contains("missing TABLE")),
2079 "wrong error variant/message"
2080 );
2081 }
2082
2083 #[mz_ore::test]
2084 #[cfg_attr(miri, ignore)] fn sink_arm_leaves_other_item_types_alone() {
2086 let sql = "CREATE VIEW \"materialize\".\"public\".\"v\" AS SELECT 1";
2087 let out = super::parse_catalog_create_sql(sql).expect("ok");
2088 assert_eq!(
2089 as_serde(out),
2090 json!({ "type": "view", "definition": "SELECT 1;" })
2091 );
2092 }
2093
2094 fn item_type(sql: &str) -> Result<String, String> {
2098 match super::parse_catalog_create_sql(sql) {
2099 Ok(out) => match as_serde(out) {
2100 serde_json::Value::Object(mut m) => match m.remove("type") {
2101 Some(serde_json::Value::String(s)) => Ok(s),
2102 other => panic!("no string `type` key: {other:?}"),
2103 },
2104 other => panic!("not a JSON object: {other:?}"),
2105 },
2106 Err(EvalError::InvalidCatalogJson(msg)) => Err(msg.to_string()),
2107 Err(e) => panic!("unexpected error variant: {e:?}"),
2108 }
2109 }
2110
2111 fn view_sql(query: &str) -> String {
2112 format!("CREATE VIEW \"materialize\".\"public\".\"v\" AS {query}")
2113 }
2114
2115 fn view_definition(query: &str) -> String {
2117 match as_serde(super::parse_catalog_create_sql(&view_sql(query)).expect("ok")) {
2118 serde_json::Value::Object(mut m) => match m.remove("definition") {
2119 Some(serde_json::Value::String(s)) => s,
2120 other => panic!("no string `definition` key: {other:?}"),
2121 },
2122 other => panic!("not a JSON object: {other:?}"),
2123 }
2124 }
2125
2126 #[mz_ore::test]
2132 #[cfg_attr(miri, ignore)] fn catalog_item_type_per_statement_kind() {
2134 let cases = [
2135 (
2136 "CREATE TABLE \"materialize\".\"public\".\"t\" (a int4)",
2137 "table",
2138 ),
2139 (
2142 "CREATE TABLE \"materialize\".\"public\".\"tbl\" \
2143 FROM SOURCE [u1 AS \"materialize\".\"public\".\"src\"] \
2144 (REFERENCE = \"topic\") FORMAT TEXT",
2145 "table",
2146 ),
2147 (
2148 "CREATE TABLE \"materialize\".\"public\".\"wht\" FROM WEBHOOK BODY FORMAT JSON",
2149 "table",
2150 ),
2151 (
2152 "CREATE VIEW \"materialize\".\"public\".\"v\" AS SELECT 1",
2153 "view",
2154 ),
2155 (
2156 "CREATE MATERIALIZED VIEW \"materialize\".\"public\".\"mv\" \
2157 IN CLUSTER [u1] AS SELECT 1",
2158 "materialized-view",
2159 ),
2160 (
2161 "CREATE SOURCE \"materialize\".\"public\".\"lg\" \
2162 IN CLUSTER [u1] FROM LOAD GENERATOR COUNTER",
2163 "source",
2164 ),
2165 (
2166 "CREATE SOURCE \"materialize\".\"public\".\"wh\" \
2167 IN CLUSTER [u1] FROM WEBHOOK BODY FORMAT JSON",
2168 "source",
2169 ),
2170 (
2171 "CREATE SUBSOURCE \"materialize\".\"public\".\"sub\" (id int4) \
2172 OF SOURCE [u1 AS \"materialize\".\"public\".\"src\"]",
2173 "subsource",
2174 ),
2175 (
2176 "CREATE SUBSOURCE \"materialize\".\"public\".\"progress\" (id int4) \
2177 WITH (PROGRESS)",
2178 "subsource",
2179 ),
2180 (
2181 "CREATE SINK \"materialize\".\"public\".\"snk\" IN CLUSTER [u1] \
2182 FROM [u1 AS \"materialize\".\"public\".\"t\"] \
2183 INTO KAFKA CONNECTION [u2 AS \"materialize\".\"public\".\"c\"] \
2184 (TOPIC 'tp') FORMAT JSON ENVELOPE DEBEZIUM",
2185 "sink",
2186 ),
2187 (
2188 "CREATE INDEX \"i\" IN CLUSTER [u1] \
2189 ON [u1 AS \"materialize\".\"public\".\"t\"] (\"a\")",
2190 "index",
2191 ),
2192 (
2193 "CREATE TYPE \"materialize\".\"public\".\"ty\" AS LIST (ELEMENT TYPE = int4)",
2194 "type",
2195 ),
2196 (
2197 "CREATE SECRET \"materialize\".\"public\".\"s\" AS 'x'",
2198 "secret",
2199 ),
2200 (
2201 "CREATE CONNECTION \"materialize\".\"public\".\"c\" \
2202 TO KAFKA (BROKER 'b', SECURITY PROTOCOL PLAINTEXT)",
2203 "connection",
2204 ),
2205 ];
2206 for (sql, expected) in cases {
2207 assert_eq!(item_type(sql).as_deref(), Ok(expected), "for {sql}");
2208 }
2209 }
2210
2211 #[mz_ore::test]
2215 #[cfg_attr(miri, ignore)] fn catalog_view_definition() {
2217 assert_eq!(view_definition("SELECT 1"), "SELECT 1;");
2220 assert_eq!(
2221 view_definition("WITH c AS (SELECT 1 AS a) SELECT a FROM c"),
2222 "WITH \"c\" AS (SELECT 1 AS \"a\") SELECT \"a\" FROM \"c\";"
2223 );
2224 assert_eq!(
2225 view_definition("SELECT 1 UNION ALL SELECT 2"),
2226 "SELECT 1 UNION ALL SELECT 2;"
2227 );
2228 assert_eq!(
2229 view_definition("SELECT (SELECT max(a) FROM [u1 AS \"materialize\".\"public\".\"t\"])"),
2230 "SELECT (SELECT \"max\"(\"a\") FROM [u1 AS \"materialize\".\"public\".\"t\"]);"
2231 );
2232 assert_eq!(
2235 view_definition(
2236 "SELECT \"a b\", \"héllo\", \"q\"\"x\" \
2237 FROM [u1 AS \"materialize\".\"public\".\"t\"] \
2238 WHERE s = 'lit''eral' AND n = 42 ORDER BY 1"
2239 ),
2240 "SELECT \"a b\", \"héllo\", \"q\"\"x\" \
2241 FROM [u1 AS \"materialize\".\"public\".\"t\"] \
2242 WHERE \"s\" = 'lit''eral' AND \"n\" = 42 ORDER BY 1;"
2243 );
2244 }
2245
2246 #[mz_ore::test]
2251 #[cfg_attr(miri, ignore)] fn catalog_view_definition_is_idempotent() {
2253 for query in [
2254 "SELECT 1",
2255 "WITH c AS (SELECT 1 AS a) SELECT a FROM c",
2256 "SELECT 1 UNION ALL SELECT 2",
2257 "SELECT \"a b\", \"q\"\"x\" FROM [u1 AS \"materialize\".\"public\".\"t\"] \
2258 WHERE s = 'lit''eral' ORDER BY 1",
2259 ] {
2260 let once = view_definition(query);
2261 assert_eq!(
2262 view_definition(&once),
2263 once,
2264 "not a fixed point for {query}"
2265 );
2266 }
2267 }
2268
2269 #[mz_ore::test]
2272 #[cfg_attr(miri, ignore)] fn catalog_table_source_id() {
2274 let from_source = as_serde(
2275 super::parse_catalog_create_sql(
2276 "CREATE TABLE \"materialize\".\"public\".\"tbl\" \
2277 FROM SOURCE [u1 AS \"materialize\".\"public\".\"src\"] \
2278 (REFERENCE = \"topic\") FORMAT TEXT",
2279 )
2280 .expect("ok"),
2281 );
2282 assert_eq!(from_source, json!({ "type": "table", "source_id": "u1" }));
2283
2284 for sql in [
2285 "CREATE TABLE \"materialize\".\"public\".\"t\" (a int4)",
2286 "CREATE TABLE \"materialize\".\"public\".\"wht\" FROM WEBHOOK BODY FORMAT JSON",
2287 ] {
2288 assert_eq!(
2289 as_serde(super::parse_catalog_create_sql(sql).expect("ok")),
2290 json!({ "type": "table" }),
2291 "for {sql}"
2292 );
2293 }
2294 }
2295
2296 #[mz_ore::test]
2300 #[cfg_attr(miri, ignore)] fn catalog_create_sql_errors() {
2302 assert_eq!(
2303 item_type("this is not sql"),
2304 Err(
2305 "failed to parse create_sql: Expected a keyword at the beginning of a statement, \
2306 found identifier \"this\""
2307 .to_string()
2308 )
2309 );
2310 assert_eq!(
2311 item_type("CREATE TABLE t (a int4); CREATE TABLE u (b int4)"),
2312 Err("expected a single statement, found 2".to_string())
2313 );
2314 assert_eq!(
2317 item_type("SELECT 1"),
2318 Err("not a CREATE item statement".to_string())
2319 );
2320 assert_eq!(
2323 item_type(
2324 "CREATE TABLE \"materialize\".\"public\".\"tbl\" \
2325 FROM SOURCE src (REFERENCE = \"topic\")"
2326 ),
2327 Err("unresolved item name".to_string())
2328 );
2329 }
2330}