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, CreateSubsourceOptionName, Format, FormatSpecifier,
22 KafkaSourceConfigOptionName, PgConfigOptionName, ProtobufSchema, RawClusterName, RawItemName,
23 SourceEnvelope, SourceErrorPolicy, Value, WithOptionValue,
24};
25use prost::Message as _;
26use serde::{Deserialize, Serialize};
27use serde_json::json;
28
29use crate::EvalError;
30use crate::scalar::func::EagerUnaryFunc;
31use crate::scalar::func::impls::numeric::*;
32
33#[sqlfunc(
34 sqlname = "jsonb_to_text",
35 preserves_uniqueness = false,
36 inverse = to_unary!(super::CastStringToJsonb)
37)]
38pub fn cast_jsonb_to_string<'a>(a: JsonbRef<'a>) -> String {
39 let mut buf = String::new();
40 strconv::format_jsonb(&mut buf, a);
41 buf
42}
43
44#[sqlfunc(sqlname = "jsonb_to_smallint", is_monotone = true)]
45fn cast_jsonb_to_int16<'a>(a: JsonbRef<'a>) -> Result<i16, EvalError> {
46 match a.into_datum() {
47 Datum::Numeric(a) => cast_numeric_to_int16(a.into_inner()),
48 datum => Err(EvalError::InvalidJsonbCast {
49 from: jsonb_typeof(JsonbRef::from_datum(datum)).into(),
50 to: "smallint".into(),
51 }),
52 }
53}
54
55#[sqlfunc(sqlname = "jsonb_to_integer", is_monotone = true)]
56fn cast_jsonb_to_int32<'a>(a: JsonbRef<'a>) -> Result<i32, EvalError> {
57 match a.into_datum() {
58 Datum::Numeric(a) => cast_numeric_to_int32(a.into_inner()),
59 datum => Err(EvalError::InvalidJsonbCast {
60 from: jsonb_typeof(JsonbRef::from_datum(datum)).into(),
61 to: "integer".into(),
62 }),
63 }
64}
65
66#[sqlfunc(sqlname = "jsonb_to_bigint", is_monotone = true)]
67fn cast_jsonb_to_int64<'a>(a: JsonbRef<'a>) -> Result<i64, EvalError> {
68 match a.into_datum() {
69 Datum::Numeric(a) => cast_numeric_to_int64(a.into_inner()),
70 datum => Err(EvalError::InvalidJsonbCast {
71 from: jsonb_typeof(JsonbRef::from_datum(datum)).into(),
72 to: "bigint".into(),
73 }),
74 }
75}
76
77#[sqlfunc(sqlname = "jsonb_to_real", is_monotone = true)]
78fn cast_jsonb_to_float32<'a>(a: JsonbRef<'a>) -> Result<f32, EvalError> {
79 match a.into_datum() {
80 Datum::Numeric(a) => cast_numeric_to_float32(a.into_inner()),
81 datum => Err(EvalError::InvalidJsonbCast {
82 from: jsonb_typeof(JsonbRef::from_datum(datum)).into(),
83 to: "real".into(),
84 }),
85 }
86}
87
88#[sqlfunc(sqlname = "jsonb_to_double", is_monotone = true)]
89fn cast_jsonb_to_float64<'a>(a: JsonbRef<'a>) -> Result<f64, EvalError> {
90 match a.into_datum() {
91 Datum::Numeric(a) => cast_numeric_to_float64(a.into_inner()),
92 datum => Err(EvalError::InvalidJsonbCast {
93 from: jsonb_typeof(JsonbRef::from_datum(datum)).into(),
94 to: "double precision".into(),
95 }),
96 }
97}
98
99#[derive(
100 Ord,
101 PartialOrd,
102 Clone,
103 Debug,
104 Eq,
105 PartialEq,
106 Serialize,
107 Deserialize,
108 Hash
109)]
110pub struct CastJsonbToNumeric(pub Option<NumericMaxScale>);
111
112impl EagerUnaryFunc for CastJsonbToNumeric {
113 type Input<'a> = JsonbRef<'a>;
114 type Output<'a> = Result<Numeric, EvalError>;
115
116 fn call<'a>(&self, a: Self::Input<'a>) -> Self::Output<'a> {
117 match a.into_datum() {
118 Datum::Numeric(mut num) => match self.0 {
119 None => Ok(num.into_inner()),
120 Some(scale) => {
121 if numeric::rescale(&mut num.0, scale.into_u8()).is_err() {
122 return Err(EvalError::NumericFieldOverflow);
123 };
124 Ok(num.into_inner())
125 }
126 },
127 datum => Err(EvalError::InvalidJsonbCast {
128 from: jsonb_typeof(JsonbRef::from_datum(datum)).into(),
129 to: "numeric".into(),
130 }),
131 }
132 }
133
134 fn output_sql_type(&self, input: SqlColumnType) -> SqlColumnType {
135 SqlScalarType::Numeric { max_scale: self.0 }.nullable(input.nullable)
136 }
137
138 fn is_monotone(&self) -> bool {
139 true
140 }
141}
142
143impl fmt::Display for CastJsonbToNumeric {
144 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
145 f.write_str("jsonb_to_numeric")
146 }
147}
148
149#[sqlfunc(sqlname = "jsonb_to_boolean", is_monotone = true)]
150fn cast_jsonb_to_bool<'a>(a: JsonbRef<'a>) -> Result<bool, EvalError> {
151 match a.into_datum() {
152 Datum::True => Ok(true),
153 Datum::False => Ok(false),
154 datum => Err(EvalError::InvalidJsonbCast {
155 from: jsonb_typeof(JsonbRef::from_datum(datum)).into(),
156 to: "boolean".into(),
157 }),
158 }
159}
160
161#[sqlfunc(sqlname = "jsonbable_to_jsonb")]
162fn cast_jsonbable_to_jsonb<'a>(a: JsonbRef<'a>) -> JsonbRef<'a> {
163 match a.into_datum() {
164 Datum::Numeric(n) => {
165 let n = n.into_inner();
166 let datum = if n.is_finite() {
167 Datum::from(n)
168 } else if n.is_nan() {
169 Datum::String("NaN")
170 } else if n.is_negative() {
171 Datum::String("-Infinity")
172 } else {
173 Datum::String("Infinity")
174 };
175 JsonbRef::from_datum(datum)
176 }
177 datum => JsonbRef::from_datum(datum),
178 }
179}
180
181#[sqlfunc]
182fn jsonb_array_length<'a>(a: JsonbRef<'a>) -> Result<Option<i32>, EvalError> {
183 match a.into_datum() {
184 Datum::List(list) => {
185 let count = list.iter().count();
186 match i32::try_from(count) {
187 Ok(len) => Ok(Some(len)),
188 Err(_) => Err(EvalError::Int32OutOfRange(count.to_string().into())),
189 }
190 }
191 _ => Ok(None),
192 }
193}
194
195#[sqlfunc]
196fn jsonb_typeof<'a>(a: JsonbRef<'a>) -> &'a str {
197 match a.into_datum() {
198 Datum::Map(_) => "object",
199 Datum::List(_) => "array",
200 Datum::String(_) => "string",
201 Datum::Numeric(_) => "number",
202 Datum::True | Datum::False => "boolean",
203 Datum::JsonNull => "null",
204 d => panic!("Not jsonb: {:?}", d),
205 }
206}
207
208#[sqlfunc]
209fn jsonb_strip_nulls<'a>(a: JsonbRef<'a>) -> Jsonb {
210 fn strip_nulls(a: Datum, row: &mut RowPacker) {
211 match a {
212 Datum::Map(dict) => row.push_dict_with(|row| {
213 for (k, v) in dict.iter() {
214 match v {
215 Datum::JsonNull => (),
216 _ => {
217 row.push(Datum::String(k));
218 strip_nulls(v, row);
219 }
220 }
221 }
222 }),
223 Datum::List(list) => row.push_list_with(|row| {
224 for elem in list.iter() {
225 strip_nulls(elem, row);
226 }
227 }),
228 _ => row.push(a),
229 }
230 }
231 let mut row = Row::default();
232 strip_nulls(a.into_datum(), &mut row.packer());
233 Jsonb::from_row(row)
234}
235
236#[sqlfunc]
237fn jsonb_pretty<'a>(a: JsonbRef<'a>) -> String {
238 let mut buf = String::new();
239 strconv::format_jsonb_pretty(&mut buf, a);
240 buf
241}
242
243fn jsonb_datum_to_u64<'a>(d: Datum<'a>) -> Result<u64, String> {
245 let Datum::Numeric(n) = d else {
246 return Err("expected numeric value".into());
247 };
248
249 let mut cx = numeric::cx_datum();
250 cx.try_into_u64(n.0)
251 .map_err(|_| format!("number out of u64 range: {n}"))
252}
253
254fn jsonb_datum_to_acl_mode(d: Datum) -> Result<AclMode, String> {
260 let Datum::Map(dict) = d else {
261 return Err(format!("unexpected acl_mode: {d}"));
262 };
263 let mut bits = None;
264 for (key, val) in dict.iter() {
265 match key {
266 "bitflags" => bits = Some(jsonb_datum_to_u64(val)?),
267 other => return Err(format!("unexpected acl_mode field: {other}")),
268 }
269 }
270 let bits = bits.ok_or_else(|| "missing acl_mode bitflags".to_string())?;
271 AclMode::from_bits(bits).ok_or_else(|| format!("invalid acl_mode bitflags: {bits}"))
272}
273
274fn jsonb_datum_to_role_id(d: Datum) -> Result<RoleId, String> {
276 match d {
277 Datum::String("Public") => Ok(RoleId::Public),
278 Datum::String(other) => Err(format!("unexpected role ID variant: {other}")),
279 Datum::Map(dict) => {
280 let (key, val) = dict.iter().next().ok_or_else(|| "empty".to_string())?;
281 let n = jsonb_datum_to_u64(val)?;
282 match key {
283 "User" => Ok(RoleId::User(n)),
284 "System" => Ok(RoleId::System(n)),
285 "Predefined" => Ok(RoleId::Predefined(n)),
286 other => Err(format!("unexpected role ID variant: {other}")),
287 }
288 }
289 _ => Err("expected string or object".into()),
290 }
291}
292
293#[sqlfunc]
297fn parse_catalog_id<'a>(a: JsonbRef<'a>) -> Result<String, EvalError> {
298 let parse = || match a.into_datum() {
299 Datum::String(variant) => match variant {
301 "Explain" => Ok("e".to_string()),
302 "Public" => Ok("p".to_string()),
303 other => Err(format!("unexpected ID variant: {other}")),
304 },
305 Datum::Map(dict) => {
307 let (key, val) = dict.iter().next().ok_or_else(|| "empty".to_string())?;
308 let prefix = match key {
309 "IntrospectionSourceIndex" => "si",
310 "Predefined" => "g",
311 "System" => "s",
312 "Transient" => "t",
313 "User" => "u",
314 other => return Err(format!("unexpected ID variant: {other}")),
315 };
316 let n = jsonb_datum_to_u64(val)?;
317 Ok(format!("{prefix}{n}"))
318 }
319 _ => Err("expected string or object".into()),
320 };
321
322 parse().map_err(|e| EvalError::InvalidCatalogJson(e.into()))
323}
324
325#[sqlfunc]
327fn parse_catalog_privileges<'a>(a: JsonbRef<'a>) -> Result<ArrayRustType<MzAclItem>, EvalError> {
328 let parse_one = |datum| match datum {
329 Datum::Map(dict) => {
330 let mut grantee = None;
331 let mut grantor = None;
332 let mut acl_mode = None;
333 for (key, val) in dict.iter() {
334 match key {
335 "grantee" => {
336 let id = jsonb_datum_to_role_id(val)?;
337 grantee = Some(id);
338 }
339 "grantor" => {
340 let id = jsonb_datum_to_role_id(val)?;
341 grantor = Some(id);
342 }
343 "acl_mode" => {
344 acl_mode = Some(jsonb_datum_to_acl_mode(val)?);
345 }
346 other => return Err(format!("unexpected privilege field: {other}")),
347 }
348 }
349 Ok(MzAclItem {
350 grantee: grantee.ok_or_else(|| format!("missing grantee: {dict:?}"))?,
351 grantor: grantor.ok_or_else(|| "missing grantor in privilege".to_string())?,
352 acl_mode: acl_mode.ok_or_else(|| "missing acl_mode in privilege".to_string())?,
353 })
354 }
355 other => Err(format!("expected object in array, found: {other}")),
356 };
357
358 let parse = || match a.into_datum() {
359 Datum::List(list) => {
360 let mut result = Vec::new();
361 for item in list.iter() {
362 result.push(parse_one(item)?);
363 }
364 Ok(result)
365 }
366 _ => Err("expected array".to_string()),
367 };
368
369 parse()
370 .map(ArrayRustType)
371 .map_err(|e| EvalError::InvalidCatalogJson(e.into()))
372}
373
374#[sqlfunc]
377fn parse_catalog_acl_mode<'a>(a: JsonbRef<'a>) -> Result<String, EvalError> {
378 jsonb_datum_to_acl_mode(a.into_datum())
379 .map(|mode| mode.to_string())
380 .map_err(|e| EvalError::InvalidCatalogJson(e.into()))
381}
382
383#[sqlfunc]
391fn parse_catalog_create_sql<'a>(a: &'a str) -> Result<Jsonb, EvalError> {
392 fn get_cluster_id(in_cluster: RawClusterName) -> Result<String, &'static str> {
393 match in_cluster {
394 RawClusterName::Resolved(s) => Ok(s),
395 RawClusterName::Unresolved(_) => Err("unresolved cluster name"),
396 }
397 }
398
399 fn get_item_id(item: RawItemName) -> Result<String, &'static str> {
400 match item {
401 RawItemName::Id(id, _, _) => Ok(id),
402 RawItemName::Name(_) => Err("unresolved item name"),
403 }
404 }
405
406 fn format_name<T: AstInfo>(fmt: &Format<T>) -> &'static str {
407 match fmt {
408 Format::Bytes => "bytes",
409 Format::Avro(_) => "avro",
410 Format::Protobuf(_) => "protobuf",
411 Format::Regex(_) => "regex",
412 Format::Csv { .. } => "csv",
413 Format::Json { .. } => "json",
414 Format::Text => "text",
415 }
416 }
417
418 let parse = || -> Result<serde_json::Value, String> {
419 let mut stmts = mz_sql_parser::parser::parse_statements(a)
420 .map_err(|e| format!("failed to parse create_sql: {e}"))?;
421 let stmt = match stmts.len() {
422 1 => stmts.remove(0).ast,
423 n => return Err(format!("expected a single statement, found {n}")),
424 };
425
426 let mut info = BTreeMap::<&str, serde_json::Value>::new();
427
428 use mz_sql_parser::ast::Statement::*;
429 let item_type = match stmt {
430 CreateSecret(_) => "secret",
431 CreateConnection(stmt) => {
432 let connection_type = stmt.connection_type.as_str();
433 info.insert("connection_type", json!(connection_type));
434
435 "connection"
436 }
437 CreateView(_) => "view",
438 CreateMaterializedView(stmt) => {
439 let Some(in_cluster) = stmt.in_cluster else {
440 return Err("missing IN CLUSTER".into());
441 };
442 let cluster_id = match in_cluster {
443 RawClusterName::Unresolved(ident) => ident.into_string(),
444 RawClusterName::Resolved(s) => s,
445 };
446 info.insert("cluster_id", json!(cluster_id));
447
448 let mut definition = stmt.query.to_ast_string_stable();
449 definition.push(';');
450 info.insert("definition", json!(definition));
451
452 "materialized-view"
453 }
454 CreateTable(_) | CreateTableFromSource(_) => "table",
455 CreateSource(stmt) => {
456 let Some(in_cluster) = stmt.in_cluster else {
457 return Err("missing IN CLUSTER".into());
458 };
459 let cluster_id = get_cluster_id(in_cluster)?;
460 info.insert("cluster_id", json!(cluster_id));
461
462 use mz_sql_parser::ast::CreateSourceConnection::*;
463 let (source_type, connection) = match stmt.connection {
464 Kafka { connection, .. } => ("kafka", Some(connection)),
465 Postgres { connection, .. } => ("postgres", Some(connection)),
466 MySql { connection, .. } => ("mysql", Some(connection)),
467 SqlServer { connection, .. } => ("sql-server", Some(connection)),
468 LoadGenerator { .. } => ("load-generator", None),
469 };
470 info.insert("source_type", json!(source_type));
471 if let Some(conn) = connection {
472 let conn_id = get_item_id(conn)?;
473 info.insert("connection_id", json!(conn_id));
474 }
475
476 let is_debezium = matches!(
477 stmt.envelope,
478 Some(mz_sql_parser::ast::SourceEnvelope::Debezium)
479 );
480
481 if let Some(envelope) = stmt.envelope {
482 use mz_sql_parser::ast::SourceEnvelope::*;
483 let envelope_type = match envelope {
484 None => "none",
485 Debezium => "debezium",
486 Upsert { .. } => "upsert",
487 CdcV2 => "materialize",
488 };
489 info.insert("envelope_type", json!(envelope_type));
490 }
491
492 if let Some(format_spec) = stmt.format {
493 match &format_spec {
494 FormatSpecifier::Bare(fmt) => {
495 if is_debezium {
498 info.insert("key_format", json!(format_name(fmt)));
499 }
500 info.insert("value_format", json!(format_name(fmt)));
501 }
502 FormatSpecifier::KeyValue { key, value } => {
503 info.insert("key_format", json!(format_name(key)));
504 info.insert("value_format", json!(format_name(value)));
505 }
506 }
507 }
508
509 "source"
510 }
511 CreateWebhookSource(stmt) => {
512 if stmt.is_table {
513 "table"
514 } else {
515 info.insert("source_type", json!("webhook"));
516 if let Some(in_cluster) = stmt.in_cluster {
517 let cluster_id = get_cluster_id(in_cluster)?;
518 info.insert("cluster_id", json!(cluster_id));
519 }
520 "source"
521 }
522 }
523 CreateSubsource(stmt) => {
524 use mz_sql_parser::ast::CreateSubsourceOptionName;
525 let is_progress = stmt
526 .with_options
527 .iter()
528 .any(|o| matches!(o.name, CreateSubsourceOptionName::Progress));
529 let source_type = if is_progress { "progress" } else { "subsource" };
530 info.insert("source_type", json!(source_type));
531
532 if let Some(of_source) = stmt.of_source {
533 let of_source_id = get_item_id(of_source)?;
534 info.insert("of_source_id", json!(of_source_id));
535 }
536
537 "subsource"
538 }
539 CreateSink(_) => "sink",
540 CreateIndex(stmt) => {
541 let Some(in_cluster) = stmt.in_cluster else {
542 return Err("missing IN CLUSTER".into());
543 };
544 let cluster_id = get_cluster_id(in_cluster)?;
545 info.insert("cluster_id", json!(cluster_id));
546 let on_id = get_item_id(stmt.on_name)?;
547 info.insert("on_id", json!(on_id));
548 "index"
549 }
550 CreateType(_) => "type",
551 _ => return Err("not a CREATE item statement".into()),
552 };
553 info.insert("type", json!(item_type));
554
555 let info = info.into_iter().map(|(k, v)| (k.to_string(), v)).collect();
556 Ok(info)
557 };
558
559 let val = parse().map_err(|e| EvalError::InvalidCatalogJson(e.into()))?;
560 let jsonb = Jsonb::from_serde_json(val).expect("valid JSONB");
561 Ok(jsonb)
562}
563
564#[derive(Clone, PartialEq, ::prost::Message)]
570struct PostgresPublicationDetailsSubset {
571 #[prost(string, tag = "2")]
572 slot: String,
573 #[prost(uint64, optional, tag = "3")]
574 timeline_id: Option<u64>,
575}
576
577#[sqlfunc]
588fn parse_postgres_source_details<'a>(a: &'a str) -> Result<Jsonb, EvalError> {
589 let parse = || -> Result<serde_json::Value, String> {
590 let mut stmts = mz_sql_parser::parser::parse_statements(a)
591 .map_err(|e| format!("failed to parse create_sql: {e}"))?;
592 let stmt = match stmts.len() {
593 1 => stmts.remove(0).ast,
594 n => return Err(format!("expected a single statement, found {n}")),
595 };
596
597 use mz_sql_parser::ast::CreateSourceConnection;
598 use mz_sql_parser::ast::Statement::CreateSource;
599 let options = match stmt {
600 CreateSource(stmt) => match stmt.connection {
601 CreateSourceConnection::Postgres { options, .. } => options,
602 _ => return Ok(serde_json::Value::Null),
603 },
604 _ => return Ok(serde_json::Value::Null),
605 };
606
607 let details_hex = options
608 .into_iter()
609 .find(|opt| opt.name == PgConfigOptionName::Details)
610 .and_then(|opt| match opt.value {
611 Some(WithOptionValue::Value(Value::String(s))) => Some(s),
612 _ => None,
613 })
614 .ok_or("missing DETAILS option on postgres source")?;
615
616 let details_bytes =
617 hex::decode(&details_hex).map_err(|e| format!("DETAILS is not valid hex: {e}"))?;
618
619 let details = PostgresPublicationDetailsSubset::decode(&*details_bytes)
620 .map_err(|e| format!("DETAILS is not a valid publication-details proto: {e}"))?;
621
622 Ok(json!({
623 "slot": details.slot,
624 "timeline_id": details.timeline_id,
625 }))
626 };
627
628 let val = parse().map_err(|e| EvalError::InvalidCatalogJson(e.into()))?;
629 let jsonb = Jsonb::from_serde_json(val).expect("valid JSONB");
630 Ok(jsonb)
631}
632
633#[sqlfunc]
645fn parse_kafka_source_details<'a>(a: &'a str) -> Result<Jsonb, EvalError> {
646 fn get_item_id(item: RawItemName) -> Result<String, &'static str> {
647 match item {
648 RawItemName::Id(id, _, _) => Ok(id),
649 RawItemName::Name(_) => Err("unresolved item name"),
650 }
651 }
652
653 let parse = || -> Result<serde_json::Value, String> {
654 let mut stmts = mz_sql_parser::parser::parse_statements(a)
655 .map_err(|e| format!("failed to parse create_sql: {e}"))?;
656 let stmt = match stmts.len() {
657 1 => stmts.remove(0).ast,
658 n => return Err(format!("expected a single statement, found {n}")),
659 };
660
661 use mz_sql_parser::ast::CreateSourceConnection;
662 use mz_sql_parser::ast::Statement::CreateSource;
663 let (connection, options) = match stmt {
664 CreateSource(stmt) => match stmt.connection {
665 CreateSourceConnection::Kafka {
666 connection,
667 options,
668 } => (connection, options),
669 _ => return Ok(serde_json::Value::Null),
670 },
671 _ => return Ok(serde_json::Value::Null),
672 };
673
674 let connection_id = get_item_id(connection)?;
675
676 let mut topic: Option<String> = None;
677 let mut group_id_prefix: Option<String> = None;
678 for opt in options {
679 let string_value = match opt.value {
680 Some(WithOptionValue::Value(Value::String(s))) => Some(s),
681 _ => None,
682 };
683 match opt.name {
684 KafkaSourceConfigOptionName::Topic => topic = string_value,
685 KafkaSourceConfigOptionName::GroupIdPrefix => group_id_prefix = string_value,
686 _ => {}
687 }
688 }
689
690 let topic = topic.ok_or("missing TOPIC option on kafka source")?;
691
692 Ok(json!({
693 "topic": topic,
694 "group_id_prefix": group_id_prefix,
695 "connection_id": connection_id,
696 }))
697 };
698
699 let val = parse().map_err(|e| EvalError::InvalidCatalogJson(e.into()))?;
700 let jsonb = Jsonb::from_serde_json(val).expect("valid JSONB");
701 Ok(jsonb)
702}
703
704#[sqlfunc]
739fn parse_source_export_details<'a>(a: &'a str) -> Result<Jsonb, EvalError> {
740 fn get_item_id(item: RawItemName) -> Result<String, &'static str> {
741 match item {
742 RawItemName::Id(id, _, _) => Ok(id),
743 RawItemName::Name(_) => Err("unresolved item name"),
744 }
745 }
746
747 fn format_name<T: AstInfo>(fmt: &Format<T>) -> &'static str {
748 match fmt {
749 Format::Bytes => "bytes",
750 Format::Avro(_) => "avro",
751 Format::Protobuf(_) => "protobuf",
752 Format::Regex(_) => "regex",
753 Format::Csv { .. } => "csv",
754 Format::Json { .. } => "json",
755 Format::Text => "text",
756 }
757 }
758
759 fn bare_format_has_key<T: AstInfo>(fmt: &Format<T>) -> bool {
763 match fmt {
764 Format::Avro(AvroSchema::Csr { csr_connection }) => csr_connection
765 .seed
766 .as_ref()
767 .is_some_and(|seed| seed.key_schema.is_some()),
768 Format::Protobuf(ProtobufSchema::Csr { csr_connection }) => csr_connection
769 .seed
770 .as_ref()
771 .is_some_and(|seed| seed.key.is_some()),
772 _ => false,
773 }
774 }
775
776 fn key_value_formats<T: AstInfo>(
777 spec: &FormatSpecifier<T>,
778 ) -> (Option<&'static str>, Option<&'static str>) {
779 match spec {
780 FormatSpecifier::KeyValue { key, value } => {
781 (Some(format_name(key)), Some(format_name(value)))
782 }
783 FormatSpecifier::Bare(fmt) => {
784 let value = Some(format_name(fmt));
785 let key = bare_format_has_key(fmt).then(|| format_name(fmt));
786 (key, value)
787 }
788 }
789 }
790
791 fn envelope_name(envelope: &SourceEnvelope) -> &'static str {
792 match envelope {
793 SourceEnvelope::None => "none",
794 SourceEnvelope::Debezium => "debezium",
795 SourceEnvelope::Upsert {
796 value_decode_err_policy,
797 } => {
798 if value_decode_err_policy
799 .iter()
800 .any(|p| matches!(p, SourceErrorPolicy::Inline { .. }))
801 {
802 "upsert-value-err-inline"
803 } else {
804 "upsert"
805 }
806 }
807 SourceEnvelope::CdcV2 => "materialize",
808 }
809 }
810
811 let parse = || -> Result<serde_json::Value, String> {
812 let mut stmts = mz_sql_parser::parser::parse_statements(a)
813 .map_err(|e| format!("failed to parse create_sql: {e}"))?;
814 let stmt = match stmts.len() {
815 1 => stmts.remove(0).ast,
816 n => return Err(format!("expected a single statement, found {n}")),
817 };
818
819 use mz_sql_parser::ast::Statement::{CreateSubsource, CreateTableFromSource};
820 match stmt {
821 CreateTableFromSource(stmt) => {
822 let source_id = get_item_id(stmt.source)?;
823 let external_reference = stmt
824 .external_reference
825 .ok_or("missing external reference on CREATE TABLE FROM SOURCE")?
826 .0
827 .into_iter()
828 .map(|ident| ident.into_string())
829 .collect::<Vec<_>>();
830
831 let envelope_type = stmt.envelope.as_ref().map(envelope_name);
832 let (key_format, value_format) = match &stmt.format {
833 Some(spec) => key_value_formats(spec),
834 None => (None, None),
835 };
836
837 Ok(json!({
838 "source_id": source_id,
839 "external_reference": external_reference,
840 "envelope_type": envelope_type,
841 "key_format": key_format,
842 "value_format": value_format,
843 }))
844 }
845 CreateSubsource(stmt) => {
846 let is_progress = stmt
849 .with_options
850 .iter()
851 .any(|o| matches!(o.name, CreateSubsourceOptionName::Progress));
852 if is_progress {
853 return Ok(serde_json::Value::Null);
854 }
855
856 let source_id = stmt
857 .of_source
858 .ok_or("non-progress CREATE SUBSOURCE without OF SOURCE")
859 .and_then(get_item_id)?;
860
861 let external_reference = stmt
862 .with_options
863 .into_iter()
864 .find(|o| matches!(o.name, CreateSubsourceOptionName::ExternalReference))
865 .and_then(|o| match o.value {
866 Some(WithOptionValue::UnresolvedItemName(name)) => Some(name),
867 _ => None,
868 })
869 .ok_or("CREATE SUBSOURCE missing EXTERNAL REFERENCE option")?
870 .0
871 .into_iter()
872 .map(|ident| ident.into_string())
873 .collect::<Vec<_>>();
874
875 Ok(json!({
876 "source_id": source_id,
877 "external_reference": external_reference,
878 "envelope_type": serde_json::Value::Null,
879 "key_format": serde_json::Value::Null,
880 "value_format": serde_json::Value::Null,
881 }))
882 }
883 _ => Ok(serde_json::Value::Null),
884 }
885 };
886
887 let val = parse().map_err(|e| EvalError::InvalidCatalogJson(e.into()))?;
888 let jsonb = Jsonb::from_serde_json(val).expect("valid JSONB");
889 Ok(jsonb)
890}
891
892#[cfg(test)]
893mod tests {
894 use mz_repr::adt::jsonb::Jsonb;
895 use prost::Message as _;
896 use serde_json::json;
897
898 use crate::EvalError;
899
900 fn encode_pg_details(slot: &str, timeline_id: Option<u64>) -> String {
903 let details = super::PostgresPublicationDetailsSubset {
904 slot: slot.to_string(),
905 timeline_id,
906 };
907 hex::encode(details.encode_to_vec())
908 }
909
910 fn pg_source_sql(details_hex: &str) -> String {
911 format!(
912 "CREATE SOURCE \"materialize\".\"public\".\"pg_src\" \
913 IN CLUSTER [u42] \
914 FROM POSTGRES CONNECTION [u10 AS \"materialize\".\"public\".\"pg_conn\"] \
915 (DETAILS = '{details_hex}', PUBLICATION = 'mz_source') \
916 FOR ALL TABLES"
917 )
918 }
919
920 fn kafka_source_sql(with_prefix: bool) -> String {
921 let prefix_opt = if with_prefix {
922 ", GROUP ID PREFIX 'my-prefix-'"
923 } else {
924 ""
925 };
926 format!(
927 "CREATE SOURCE \"materialize\".\"public\".\"k_src\" \
928 IN CLUSTER [u42] \
929 FROM KAFKA CONNECTION [u11 AS \"materialize\".\"public\".\"k_conn\"] \
930 (TOPIC 'test'{prefix_opt}) FORMAT TEXT"
931 )
932 }
933
934 fn as_serde(jsonb: Jsonb) -> serde_json::Value {
935 jsonb.as_ref().to_serde_json()
936 }
937
938 #[mz_ore::test]
941 #[cfg_attr(miri, ignore)] fn pg_happy_path_with_timeline() {
943 let hex = encode_pg_details("materialize_abc", Some(42));
944 let sql = pg_source_sql(&hex);
945 let out = super::parse_postgres_source_details(&sql).expect("ok");
946 assert_eq!(
947 as_serde(out),
948 json!({ "slot": "materialize_abc", "timeline_id": 42 }),
949 );
950 }
951
952 #[mz_ore::test]
953 fn pg_happy_path_null_timeline() {
954 let hex = encode_pg_details("materialize_legacy", None);
957 let sql = pg_source_sql(&hex);
958 let out = super::parse_postgres_source_details(&sql).expect("ok");
959 assert_eq!(
960 as_serde(out),
961 json!({ "slot": "materialize_legacy", "timeline_id": null }),
962 );
963 }
964
965 #[mz_ore::test]
966 fn pg_non_postgres_source_returns_null_jsonb() {
967 let sql = "CREATE SOURCE \"materialize\".\"public\".\"lg\" \
968 IN CLUSTER [u42] FROM LOAD GENERATOR COUNTER";
969 let out = super::parse_postgres_source_details(sql).expect("ok");
970 assert_eq!(as_serde(out), serde_json::Value::Null);
971 }
972
973 #[mz_ore::test]
974 #[cfg_attr(miri, ignore)] fn pg_non_create_source_returns_null_jsonb() {
976 let sql = "CREATE VIEW v AS SELECT 1";
977 let out = super::parse_postgres_source_details(sql).expect("ok");
978 assert_eq!(as_serde(out), serde_json::Value::Null);
979 }
980
981 #[mz_ore::test]
982 fn pg_missing_details_option_errors() {
983 let sql = "CREATE SOURCE \"materialize\".\"public\".\"pg_src\" \
984 IN CLUSTER [u42] \
985 FROM POSTGRES CONNECTION [u10 AS \"materialize\".\"public\".\"pg_conn\"] \
986 (PUBLICATION = 'mz_source') FOR ALL TABLES";
987 let err = super::parse_postgres_source_details(sql).unwrap_err();
988 assert!(
989 matches!(err, EvalError::InvalidCatalogJson(msg) if msg.contains("missing DETAILS")),
990 "wrong error variant/message"
991 );
992 }
993
994 #[mz_ore::test]
995 fn pg_malformed_hex_errors() {
996 let sql = pg_source_sql("not-hex!!");
997 let err = super::parse_postgres_source_details(&sql).unwrap_err();
998 assert!(
999 matches!(err, EvalError::InvalidCatalogJson(msg) if msg.contains("valid hex")),
1000 "wrong error variant/message"
1001 );
1002 }
1003
1004 #[mz_ore::test]
1005 fn pg_malformed_proto_errors() {
1006 let sql = pg_source_sql("ffff");
1009 let err = super::parse_postgres_source_details(&sql).unwrap_err();
1010 assert!(
1011 matches!(err, EvalError::InvalidCatalogJson(msg) if msg.contains("publication-details proto")),
1012 "wrong error variant/message"
1013 );
1014 }
1015
1016 #[mz_ore::test]
1019 fn kafka_happy_path_with_prefix() {
1020 let sql = kafka_source_sql(true);
1021 let out = super::parse_kafka_source_details(&sql).expect("ok");
1022 assert_eq!(
1023 as_serde(out),
1024 json!({
1025 "topic": "test",
1026 "group_id_prefix": "my-prefix-",
1027 "connection_id": "u11",
1028 }),
1029 );
1030 }
1031
1032 #[mz_ore::test]
1033 fn kafka_happy_path_without_prefix() {
1034 let sql = kafka_source_sql(false);
1035 let out = super::parse_kafka_source_details(&sql).expect("ok");
1036 assert_eq!(
1037 as_serde(out),
1038 json!({
1039 "topic": "test",
1040 "group_id_prefix": null,
1041 "connection_id": "u11",
1042 }),
1043 );
1044 }
1045
1046 #[mz_ore::test]
1047 fn kafka_non_kafka_source_returns_null_jsonb() {
1048 let sql = "CREATE SOURCE \"materialize\".\"public\".\"lg\" \
1049 IN CLUSTER [u42] FROM LOAD GENERATOR COUNTER";
1050 let out = super::parse_kafka_source_details(sql).expect("ok");
1051 assert_eq!(as_serde(out), serde_json::Value::Null);
1052 }
1053
1054 #[mz_ore::test]
1055 fn kafka_missing_topic_errors() {
1056 let sql = "CREATE SOURCE \"materialize\".\"public\".\"k_src\" \
1057 IN CLUSTER [u42] \
1058 FROM KAFKA CONNECTION [u11 AS \"materialize\".\"public\".\"k_conn\"] \
1059 FORMAT TEXT";
1060 let err = super::parse_kafka_source_details(sql).unwrap_err();
1061 assert!(
1062 matches!(err, EvalError::InvalidCatalogJson(msg) if msg.contains("missing TOPIC")),
1063 "wrong error variant/message"
1064 );
1065 }
1066
1067 #[mz_ore::test]
1068 fn kafka_unresolved_connection_errors() {
1069 let sql = "CREATE SOURCE \"materialize\".\"public\".\"k_src\" \
1073 IN CLUSTER [u42] \
1074 FROM KAFKA CONNECTION k_conn (TOPIC 'test') FORMAT TEXT";
1075 let err = super::parse_kafka_source_details(sql).unwrap_err();
1076 assert!(
1077 matches!(err, EvalError::InvalidCatalogJson(msg) if msg.contains("unresolved item name")),
1078 "wrong error variant/message"
1079 );
1080 }
1081
1082 fn table_from_source_sql(reference: &str, suffix: &str) -> String {
1085 format!(
1086 "CREATE TABLE \"materialize\".\"public\".\"tbl\" \
1087 FROM SOURCE [u1 AS \"materialize\".\"public\".\"src\"] \
1088 (REFERENCE = {reference}){suffix}"
1089 )
1090 }
1091
1092 #[mz_ore::test]
1093 fn export_table_postgres_style_no_format() {
1094 let sql = table_from_source_sql("\"db\".\"public\".\"t\"", "");
1097 let out = super::parse_source_export_details(&sql).expect("ok");
1098 assert_eq!(
1099 as_serde(out),
1100 json!({
1101 "source_id": "u1",
1102 "external_reference": ["db", "public", "t"],
1103 "envelope_type": null,
1104 "key_format": null,
1105 "value_format": null,
1106 }),
1107 );
1108 }
1109
1110 #[mz_ore::test]
1111 fn export_table_kafka_bare_value_only() {
1112 let sql = table_from_source_sql("\"topic\"", " FORMAT TEXT ENVELOPE NONE");
1114 let out = super::parse_source_export_details(&sql).expect("ok");
1115 assert_eq!(
1116 as_serde(out),
1117 json!({
1118 "source_id": "u1",
1119 "external_reference": ["topic"],
1120 "envelope_type": "none",
1121 "key_format": null,
1122 "value_format": "text",
1123 }),
1124 );
1125 }
1126
1127 #[mz_ore::test]
1128 fn export_table_kafka_key_value_format() {
1129 let sql = table_from_source_sql(
1130 "\"topic\"",
1131 " KEY FORMAT TEXT VALUE FORMAT TEXT ENVELOPE NONE",
1132 );
1133 let out = super::parse_source_export_details(&sql).expect("ok");
1134 assert_eq!(
1135 as_serde(out),
1136 json!({
1137 "source_id": "u1",
1138 "external_reference": ["topic"],
1139 "envelope_type": "none",
1140 "key_format": "text",
1141 "value_format": "text",
1142 }),
1143 );
1144 }
1145
1146 #[mz_ore::test]
1147 #[cfg_attr(miri, ignore)] fn export_table_kafka_bare_avro_seed_with_key() {
1149 let sql = table_from_source_sql(
1153 "\"topic\"",
1154 " FORMAT AVRO USING CONFLUENT SCHEMA REGISTRY \
1155 CONNECTION [u5 AS \"materialize\".\"public\".\"csr\"] \
1156 SEED KEY SCHEMA 'k' VALUE SCHEMA 'v' ENVELOPE UPSERT",
1157 );
1158 let out = super::parse_source_export_details(&sql).expect("ok");
1159 assert_eq!(
1160 as_serde(out),
1161 json!({
1162 "source_id": "u1",
1163 "external_reference": ["topic"],
1164 "envelope_type": "upsert",
1165 "key_format": "avro",
1166 "value_format": "avro",
1167 }),
1168 );
1169 }
1170
1171 #[mz_ore::test]
1172 #[cfg_attr(miri, ignore)] fn export_table_kafka_bare_avro_seed_without_key() {
1174 let sql = table_from_source_sql(
1176 "\"topic\"",
1177 " FORMAT AVRO USING CONFLUENT SCHEMA REGISTRY \
1178 CONNECTION [u5 AS \"materialize\".\"public\".\"csr\"] \
1179 SEED VALUE SCHEMA 'v' ENVELOPE NONE",
1180 );
1181 let out = super::parse_source_export_details(&sql).expect("ok");
1182 assert_eq!(
1183 as_serde(out),
1184 json!({
1185 "source_id": "u1",
1186 "external_reference": ["topic"],
1187 "envelope_type": "none",
1188 "key_format": null,
1189 "value_format": "avro",
1190 }),
1191 );
1192 }
1193
1194 #[mz_ore::test]
1195 fn export_subsource_non_progress() {
1196 let sql = "CREATE SUBSOURCE \"materialize\".\"public\".\"sub\" (id int4) \
1199 OF SOURCE [u1 AS \"materialize\".\"public\".\"src\"] \
1200 WITH (EXTERNAL REFERENCE = \"db\".\"public\".\"t\")";
1201 let out = super::parse_source_export_details(sql).expect("ok");
1202 assert_eq!(
1203 as_serde(out),
1204 json!({
1205 "source_id": "u1",
1206 "external_reference": ["db", "public", "t"],
1207 "envelope_type": null,
1208 "key_format": null,
1209 "value_format": null,
1210 }),
1211 );
1212 }
1213
1214 #[mz_ore::test]
1215 fn export_progress_subsource_returns_null_jsonb() {
1216 let sql = "CREATE SUBSOURCE \"materialize\".\"public\".\"progress\" (id int4) \
1217 WITH (PROGRESS)";
1218 let out = super::parse_source_export_details(sql).expect("ok");
1219 assert_eq!(as_serde(out), serde_json::Value::Null);
1220 }
1221
1222 #[mz_ore::test]
1223 #[cfg_attr(miri, ignore)] fn export_non_source_export_returns_null_jsonb() {
1225 let sql = "CREATE VIEW v AS SELECT 1";
1226 let out = super::parse_source_export_details(sql).expect("ok");
1227 assert_eq!(as_serde(out), serde_json::Value::Null);
1228 }
1229
1230 #[mz_ore::test]
1231 fn export_unresolved_source_name_errors() {
1232 let sql = "CREATE TABLE \"materialize\".\"public\".\"tbl\" \
1233 FROM SOURCE src (REFERENCE = \"topic\")";
1234 let err = super::parse_source_export_details(sql).unwrap_err();
1235 assert!(
1236 matches!(err, EvalError::InvalidCatalogJson(msg) if msg.contains("unresolved item name")),
1237 "wrong error variant/message"
1238 );
1239 }
1240}