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, Format, FormatSpecifier, KafkaSourceConfigOptionName, PgConfigOptionName,
22 RawClusterName, RawItemName, Value, WithOptionValue,
23};
24use prost::Message as _;
25use serde::{Deserialize, Serialize};
26use serde_json::json;
27
28use crate::EvalError;
29use crate::scalar::func::EagerUnaryFunc;
30use crate::scalar::func::impls::numeric::*;
31
32#[sqlfunc(
33 sqlname = "jsonb_to_text",
34 preserves_uniqueness = false,
35 inverse = to_unary!(super::CastStringToJsonb)
36)]
37pub fn cast_jsonb_to_string<'a>(a: JsonbRef<'a>) -> String {
38 let mut buf = String::new();
39 strconv::format_jsonb(&mut buf, a);
40 buf
41}
42
43#[sqlfunc(sqlname = "jsonb_to_smallint", is_monotone = true)]
44fn cast_jsonb_to_int16<'a>(a: JsonbRef<'a>) -> Result<i16, EvalError> {
45 match a.into_datum() {
46 Datum::Numeric(a) => cast_numeric_to_int16(a.into_inner()),
47 datum => Err(EvalError::InvalidJsonbCast {
48 from: jsonb_typeof(JsonbRef::from_datum(datum)).into(),
49 to: "smallint".into(),
50 }),
51 }
52}
53
54#[sqlfunc(sqlname = "jsonb_to_integer", is_monotone = true)]
55fn cast_jsonb_to_int32<'a>(a: JsonbRef<'a>) -> Result<i32, EvalError> {
56 match a.into_datum() {
57 Datum::Numeric(a) => cast_numeric_to_int32(a.into_inner()),
58 datum => Err(EvalError::InvalidJsonbCast {
59 from: jsonb_typeof(JsonbRef::from_datum(datum)).into(),
60 to: "integer".into(),
61 }),
62 }
63}
64
65#[sqlfunc(sqlname = "jsonb_to_bigint", is_monotone = true)]
66fn cast_jsonb_to_int64<'a>(a: JsonbRef<'a>) -> Result<i64, EvalError> {
67 match a.into_datum() {
68 Datum::Numeric(a) => cast_numeric_to_int64(a.into_inner()),
69 datum => Err(EvalError::InvalidJsonbCast {
70 from: jsonb_typeof(JsonbRef::from_datum(datum)).into(),
71 to: "bigint".into(),
72 }),
73 }
74}
75
76#[sqlfunc(sqlname = "jsonb_to_real", is_monotone = true)]
77fn cast_jsonb_to_float32<'a>(a: JsonbRef<'a>) -> Result<f32, EvalError> {
78 match a.into_datum() {
79 Datum::Numeric(a) => cast_numeric_to_float32(a.into_inner()),
80 datum => Err(EvalError::InvalidJsonbCast {
81 from: jsonb_typeof(JsonbRef::from_datum(datum)).into(),
82 to: "real".into(),
83 }),
84 }
85}
86
87#[sqlfunc(sqlname = "jsonb_to_double", is_monotone = true)]
88fn cast_jsonb_to_float64<'a>(a: JsonbRef<'a>) -> Result<f64, EvalError> {
89 match a.into_datum() {
90 Datum::Numeric(a) => cast_numeric_to_float64(a.into_inner()),
91 datum => Err(EvalError::InvalidJsonbCast {
92 from: jsonb_typeof(JsonbRef::from_datum(datum)).into(),
93 to: "double precision".into(),
94 }),
95 }
96}
97
98#[derive(
99 Ord,
100 PartialOrd,
101 Clone,
102 Debug,
103 Eq,
104 PartialEq,
105 Serialize,
106 Deserialize,
107 Hash
108)]
109pub struct CastJsonbToNumeric(pub Option<NumericMaxScale>);
110
111impl EagerUnaryFunc for CastJsonbToNumeric {
112 type Input<'a> = JsonbRef<'a>;
113 type Output<'a> = Result<Numeric, EvalError>;
114
115 fn call<'a>(&self, a: Self::Input<'a>) -> Self::Output<'a> {
116 match a.into_datum() {
117 Datum::Numeric(mut num) => match self.0 {
118 None => Ok(num.into_inner()),
119 Some(scale) => {
120 if numeric::rescale(&mut num.0, scale.into_u8()).is_err() {
121 return Err(EvalError::NumericFieldOverflow);
122 };
123 Ok(num.into_inner())
124 }
125 },
126 datum => Err(EvalError::InvalidJsonbCast {
127 from: jsonb_typeof(JsonbRef::from_datum(datum)).into(),
128 to: "numeric".into(),
129 }),
130 }
131 }
132
133 fn output_sql_type(&self, input: SqlColumnType) -> SqlColumnType {
134 SqlScalarType::Numeric { max_scale: self.0 }.nullable(input.nullable)
135 }
136
137 fn is_monotone(&self) -> bool {
138 true
139 }
140}
141
142impl fmt::Display for CastJsonbToNumeric {
143 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
144 f.write_str("jsonb_to_numeric")
145 }
146}
147
148#[sqlfunc(sqlname = "jsonb_to_boolean", is_monotone = true)]
149fn cast_jsonb_to_bool<'a>(a: JsonbRef<'a>) -> Result<bool, EvalError> {
150 match a.into_datum() {
151 Datum::True => Ok(true),
152 Datum::False => Ok(false),
153 datum => Err(EvalError::InvalidJsonbCast {
154 from: jsonb_typeof(JsonbRef::from_datum(datum)).into(),
155 to: "boolean".into(),
156 }),
157 }
158}
159
160#[sqlfunc(sqlname = "jsonbable_to_jsonb")]
161fn cast_jsonbable_to_jsonb<'a>(a: JsonbRef<'a>) -> JsonbRef<'a> {
162 match a.into_datum() {
163 Datum::Numeric(n) => {
164 let n = n.into_inner();
165 let datum = if n.is_finite() {
166 Datum::from(n)
167 } else if n.is_nan() {
168 Datum::String("NaN")
169 } else if n.is_negative() {
170 Datum::String("-Infinity")
171 } else {
172 Datum::String("Infinity")
173 };
174 JsonbRef::from_datum(datum)
175 }
176 datum => JsonbRef::from_datum(datum),
177 }
178}
179
180#[sqlfunc]
181fn jsonb_array_length<'a>(a: JsonbRef<'a>) -> Result<Option<i32>, EvalError> {
182 match a.into_datum() {
183 Datum::List(list) => {
184 let count = list.iter().count();
185 match i32::try_from(count) {
186 Ok(len) => Ok(Some(len)),
187 Err(_) => Err(EvalError::Int32OutOfRange(count.to_string().into())),
188 }
189 }
190 _ => Ok(None),
191 }
192}
193
194#[sqlfunc]
195fn jsonb_typeof<'a>(a: JsonbRef<'a>) -> &'a str {
196 match a.into_datum() {
197 Datum::Map(_) => "object",
198 Datum::List(_) => "array",
199 Datum::String(_) => "string",
200 Datum::Numeric(_) => "number",
201 Datum::True | Datum::False => "boolean",
202 Datum::JsonNull => "null",
203 d => panic!("Not jsonb: {:?}", d),
204 }
205}
206
207#[sqlfunc]
208fn jsonb_strip_nulls<'a>(a: JsonbRef<'a>) -> Jsonb {
209 fn strip_nulls(a: Datum, row: &mut RowPacker) {
210 match a {
211 Datum::Map(dict) => row.push_dict_with(|row| {
212 for (k, v) in dict.iter() {
213 match v {
214 Datum::JsonNull => (),
215 _ => {
216 row.push(Datum::String(k));
217 strip_nulls(v, row);
218 }
219 }
220 }
221 }),
222 Datum::List(list) => row.push_list_with(|row| {
223 for elem in list.iter() {
224 strip_nulls(elem, row);
225 }
226 }),
227 _ => row.push(a),
228 }
229 }
230 let mut row = Row::default();
231 strip_nulls(a.into_datum(), &mut row.packer());
232 Jsonb::from_row(row)
233}
234
235#[sqlfunc]
236fn jsonb_pretty<'a>(a: JsonbRef<'a>) -> String {
237 let mut buf = String::new();
238 strconv::format_jsonb_pretty(&mut buf, a);
239 buf
240}
241
242fn jsonb_datum_to_u64<'a>(d: Datum<'a>) -> Result<u64, String> {
244 let Datum::Numeric(n) = d else {
245 return Err("expected numeric value".into());
246 };
247
248 let mut cx = numeric::cx_datum();
249 cx.try_into_u64(n.0)
250 .map_err(|_| format!("number out of u64 range: {n}"))
251}
252
253fn jsonb_datum_to_acl_mode(d: Datum) -> Result<AclMode, String> {
259 let Datum::Map(dict) = d else {
260 return Err(format!("unexpected acl_mode: {d}"));
261 };
262 let mut bits = None;
263 for (key, val) in dict.iter() {
264 match key {
265 "bitflags" => bits = Some(jsonb_datum_to_u64(val)?),
266 other => return Err(format!("unexpected acl_mode field: {other}")),
267 }
268 }
269 let bits = bits.ok_or_else(|| "missing acl_mode bitflags".to_string())?;
270 AclMode::from_bits(bits).ok_or_else(|| format!("invalid acl_mode bitflags: {bits}"))
271}
272
273fn jsonb_datum_to_role_id(d: Datum) -> Result<RoleId, String> {
275 match d {
276 Datum::String("Public") => Ok(RoleId::Public),
277 Datum::String(other) => Err(format!("unexpected role ID variant: {other}")),
278 Datum::Map(dict) => {
279 let (key, val) = dict.iter().next().ok_or_else(|| "empty".to_string())?;
280 let n = jsonb_datum_to_u64(val)?;
281 match key {
282 "User" => Ok(RoleId::User(n)),
283 "System" => Ok(RoleId::System(n)),
284 "Predefined" => Ok(RoleId::Predefined(n)),
285 other => Err(format!("unexpected role ID variant: {other}")),
286 }
287 }
288 _ => Err("expected string or object".into()),
289 }
290}
291
292#[sqlfunc]
296fn parse_catalog_id<'a>(a: JsonbRef<'a>) -> Result<String, EvalError> {
297 let parse = || match a.into_datum() {
298 Datum::String(variant) => match variant {
300 "Explain" => Ok("e".to_string()),
301 "Public" => Ok("p".to_string()),
302 other => Err(format!("unexpected ID variant: {other}")),
303 },
304 Datum::Map(dict) => {
306 let (key, val) = dict.iter().next().ok_or_else(|| "empty".to_string())?;
307 let prefix = match key {
308 "IntrospectionSourceIndex" => "si",
309 "Predefined" => "g",
310 "System" => "s",
311 "Transient" => "t",
312 "User" => "u",
313 other => return Err(format!("unexpected ID variant: {other}")),
314 };
315 let n = jsonb_datum_to_u64(val)?;
316 Ok(format!("{prefix}{n}"))
317 }
318 _ => Err("expected string or object".into()),
319 };
320
321 parse().map_err(|e| EvalError::InvalidCatalogJson(e.into()))
322}
323
324#[sqlfunc]
326fn parse_catalog_privileges<'a>(a: JsonbRef<'a>) -> Result<ArrayRustType<MzAclItem>, EvalError> {
327 let parse_one = |datum| match datum {
328 Datum::Map(dict) => {
329 let mut grantee = None;
330 let mut grantor = None;
331 let mut acl_mode = None;
332 for (key, val) in dict.iter() {
333 match key {
334 "grantee" => {
335 let id = jsonb_datum_to_role_id(val)?;
336 grantee = Some(id);
337 }
338 "grantor" => {
339 let id = jsonb_datum_to_role_id(val)?;
340 grantor = Some(id);
341 }
342 "acl_mode" => {
343 acl_mode = Some(jsonb_datum_to_acl_mode(val)?);
344 }
345 other => return Err(format!("unexpected privilege field: {other}")),
346 }
347 }
348 Ok(MzAclItem {
349 grantee: grantee.ok_or_else(|| format!("missing grantee: {dict:?}"))?,
350 grantor: grantor.ok_or_else(|| "missing grantor in privilege".to_string())?,
351 acl_mode: acl_mode.ok_or_else(|| "missing acl_mode in privilege".to_string())?,
352 })
353 }
354 other => Err(format!("expected object in array, found: {other}")),
355 };
356
357 let parse = || match a.into_datum() {
358 Datum::List(list) => {
359 let mut result = Vec::new();
360 for item in list.iter() {
361 result.push(parse_one(item)?);
362 }
363 Ok(result)
364 }
365 _ => Err("expected array".to_string()),
366 };
367
368 parse()
369 .map(ArrayRustType)
370 .map_err(|e| EvalError::InvalidCatalogJson(e.into()))
371}
372
373#[sqlfunc]
376fn parse_catalog_acl_mode<'a>(a: JsonbRef<'a>) -> Result<String, EvalError> {
377 jsonb_datum_to_acl_mode(a.into_datum())
378 .map(|mode| mode.to_string())
379 .map_err(|e| EvalError::InvalidCatalogJson(e.into()))
380}
381
382#[sqlfunc]
390fn parse_catalog_create_sql<'a>(a: &'a str) -> Result<Jsonb, EvalError> {
391 fn get_cluster_id(in_cluster: RawClusterName) -> Result<String, &'static str> {
392 match in_cluster {
393 RawClusterName::Resolved(s) => Ok(s),
394 RawClusterName::Unresolved(_) => Err("unresolved cluster name"),
395 }
396 }
397
398 fn get_item_id(item: RawItemName) -> Result<String, &'static str> {
399 match item {
400 RawItemName::Id(id, _, _) => Ok(id),
401 RawItemName::Name(_) => Err("unresolved item name"),
402 }
403 }
404
405 fn format_name<T: AstInfo>(fmt: &Format<T>) -> &'static str {
406 match fmt {
407 Format::Bytes => "bytes",
408 Format::Avro(_) => "avro",
409 Format::Protobuf(_) => "protobuf",
410 Format::Regex(_) => "regex",
411 Format::Csv { .. } => "csv",
412 Format::Json { .. } => "json",
413 Format::Text => "text",
414 }
415 }
416
417 let parse = || -> Result<serde_json::Value, String> {
418 let mut stmts = mz_sql_parser::parser::parse_statements(a)
419 .map_err(|e| format!("failed to parse create_sql: {e}"))?;
420 let stmt = match stmts.len() {
421 1 => stmts.remove(0).ast,
422 n => return Err(format!("expected a single statement, found {n}")),
423 };
424
425 let mut info = BTreeMap::<&str, serde_json::Value>::new();
426
427 use mz_sql_parser::ast::Statement::*;
428 let item_type = match stmt {
429 CreateSecret(_) => "secret",
430 CreateConnection(stmt) => {
431 let connection_type = stmt.connection_type.as_str();
432 info.insert("connection_type", json!(connection_type));
433
434 "connection"
435 }
436 CreateView(_) => "view",
437 CreateMaterializedView(stmt) => {
438 let Some(in_cluster) = stmt.in_cluster else {
439 return Err("missing IN CLUSTER".into());
440 };
441 let cluster_id = match in_cluster {
442 RawClusterName::Unresolved(ident) => ident.into_string(),
443 RawClusterName::Resolved(s) => s,
444 };
445 info.insert("cluster_id", json!(cluster_id));
446
447 let mut definition = stmt.query.to_ast_string_stable();
448 definition.push(';');
449 info.insert("definition", json!(definition));
450
451 "materialized-view"
452 }
453 CreateTable(_) | CreateTableFromSource(_) => "table",
454 CreateSource(stmt) => {
455 let Some(in_cluster) = stmt.in_cluster else {
456 return Err("missing IN CLUSTER".into());
457 };
458 let cluster_id = get_cluster_id(in_cluster)?;
459 info.insert("cluster_id", json!(cluster_id));
460
461 use mz_sql_parser::ast::CreateSourceConnection::*;
462 let (source_type, connection) = match stmt.connection {
463 Kafka { connection, .. } => ("kafka", Some(connection)),
464 Postgres { connection, .. } => ("postgres", Some(connection)),
465 MySql { connection, .. } => ("mysql", Some(connection)),
466 SqlServer { connection, .. } => ("sql-server", Some(connection)),
467 LoadGenerator { .. } => ("load-generator", None),
468 };
469 info.insert("source_type", json!(source_type));
470 if let Some(conn) = connection {
471 let conn_id = get_item_id(conn)?;
472 info.insert("connection_id", json!(conn_id));
473 }
474
475 let is_debezium = matches!(
476 stmt.envelope,
477 Some(mz_sql_parser::ast::SourceEnvelope::Debezium)
478 );
479
480 if let Some(envelope) = stmt.envelope {
481 use mz_sql_parser::ast::SourceEnvelope::*;
482 let envelope_type = match envelope {
483 None => "none",
484 Debezium => "debezium",
485 Upsert { .. } => "upsert",
486 CdcV2 => "materialize",
487 };
488 info.insert("envelope_type", json!(envelope_type));
489 }
490
491 if let Some(format_spec) = stmt.format {
492 match &format_spec {
493 FormatSpecifier::Bare(fmt) => {
494 if is_debezium {
497 info.insert("key_format", json!(format_name(fmt)));
498 }
499 info.insert("value_format", json!(format_name(fmt)));
500 }
501 FormatSpecifier::KeyValue { key, value } => {
502 info.insert("key_format", json!(format_name(key)));
503 info.insert("value_format", json!(format_name(value)));
504 }
505 }
506 }
507
508 "source"
509 }
510 CreateWebhookSource(stmt) => {
511 if stmt.is_table {
512 "table"
513 } else {
514 info.insert("source_type", json!("webhook"));
515 if let Some(in_cluster) = stmt.in_cluster {
516 let cluster_id = get_cluster_id(in_cluster)?;
517 info.insert("cluster_id", json!(cluster_id));
518 }
519 "source"
520 }
521 }
522 CreateSubsource(stmt) => {
523 use mz_sql_parser::ast::CreateSubsourceOptionName;
524 let is_progress = stmt
525 .with_options
526 .iter()
527 .any(|o| matches!(o.name, CreateSubsourceOptionName::Progress));
528 let source_type = if is_progress { "progress" } else { "subsource" };
529 info.insert("source_type", json!(source_type));
530
531 if let Some(of_source) = stmt.of_source {
532 let of_source_id = get_item_id(of_source)?;
533 info.insert("of_source_id", json!(of_source_id));
534 }
535
536 "subsource"
537 }
538 CreateSink(_) => "sink",
539 CreateIndex(stmt) => {
540 let Some(in_cluster) = stmt.in_cluster else {
541 return Err("missing IN CLUSTER".into());
542 };
543 let cluster_id = get_cluster_id(in_cluster)?;
544 info.insert("cluster_id", json!(cluster_id));
545 let on_id = get_item_id(stmt.on_name)?;
546 info.insert("on_id", json!(on_id));
547 "index"
548 }
549 CreateType(_) => "type",
550 _ => return Err("not a CREATE item statement".into()),
551 };
552 info.insert("type", json!(item_type));
553
554 let info = info.into_iter().map(|(k, v)| (k.to_string(), v)).collect();
555 Ok(info)
556 };
557
558 let val = parse().map_err(|e| EvalError::InvalidCatalogJson(e.into()))?;
559 let jsonb = Jsonb::from_serde_json(val).expect("valid JSONB");
560 Ok(jsonb)
561}
562
563#[derive(Clone, PartialEq, ::prost::Message)]
569struct PostgresPublicationDetailsSubset {
570 #[prost(string, tag = "2")]
571 slot: String,
572 #[prost(uint64, optional, tag = "3")]
573 timeline_id: Option<u64>,
574}
575
576#[sqlfunc]
587fn parse_postgres_source_details<'a>(a: &'a str) -> Result<Jsonb, EvalError> {
588 let parse = || -> Result<serde_json::Value, String> {
589 let mut stmts = mz_sql_parser::parser::parse_statements(a)
590 .map_err(|e| format!("failed to parse create_sql: {e}"))?;
591 let stmt = match stmts.len() {
592 1 => stmts.remove(0).ast,
593 n => return Err(format!("expected a single statement, found {n}")),
594 };
595
596 use mz_sql_parser::ast::CreateSourceConnection;
597 use mz_sql_parser::ast::Statement::CreateSource;
598 let options = match stmt {
599 CreateSource(stmt) => match stmt.connection {
600 CreateSourceConnection::Postgres { options, .. } => options,
601 _ => return Ok(serde_json::Value::Null),
602 },
603 _ => return Ok(serde_json::Value::Null),
604 };
605
606 let details_hex = options
607 .into_iter()
608 .find(|opt| opt.name == PgConfigOptionName::Details)
609 .and_then(|opt| match opt.value {
610 Some(WithOptionValue::Value(Value::String(s))) => Some(s),
611 _ => None,
612 })
613 .ok_or("missing DETAILS option on postgres source")?;
614
615 let details_bytes =
616 hex::decode(&details_hex).map_err(|e| format!("DETAILS is not valid hex: {e}"))?;
617
618 let details = PostgresPublicationDetailsSubset::decode(&*details_bytes)
619 .map_err(|e| format!("DETAILS is not a valid publication-details proto: {e}"))?;
620
621 Ok(json!({
622 "slot": details.slot,
623 "timeline_id": details.timeline_id,
624 }))
625 };
626
627 let val = parse().map_err(|e| EvalError::InvalidCatalogJson(e.into()))?;
628 let jsonb = Jsonb::from_serde_json(val).expect("valid JSONB");
629 Ok(jsonb)
630}
631
632#[sqlfunc]
644fn parse_kafka_source_details<'a>(a: &'a str) -> Result<Jsonb, EvalError> {
645 fn get_item_id(item: RawItemName) -> Result<String, &'static str> {
646 match item {
647 RawItemName::Id(id, _, _) => Ok(id),
648 RawItemName::Name(_) => Err("unresolved item name"),
649 }
650 }
651
652 let parse = || -> Result<serde_json::Value, String> {
653 let mut stmts = mz_sql_parser::parser::parse_statements(a)
654 .map_err(|e| format!("failed to parse create_sql: {e}"))?;
655 let stmt = match stmts.len() {
656 1 => stmts.remove(0).ast,
657 n => return Err(format!("expected a single statement, found {n}")),
658 };
659
660 use mz_sql_parser::ast::CreateSourceConnection;
661 use mz_sql_parser::ast::Statement::CreateSource;
662 let (connection, options) = match stmt {
663 CreateSource(stmt) => match stmt.connection {
664 CreateSourceConnection::Kafka {
665 connection,
666 options,
667 } => (connection, options),
668 _ => return Ok(serde_json::Value::Null),
669 },
670 _ => return Ok(serde_json::Value::Null),
671 };
672
673 let connection_id = get_item_id(connection)?;
674
675 let mut topic: Option<String> = None;
676 let mut group_id_prefix: Option<String> = None;
677 for opt in options {
678 let string_value = match opt.value {
679 Some(WithOptionValue::Value(Value::String(s))) => Some(s),
680 _ => None,
681 };
682 match opt.name {
683 KafkaSourceConfigOptionName::Topic => topic = string_value,
684 KafkaSourceConfigOptionName::GroupIdPrefix => group_id_prefix = string_value,
685 _ => {}
686 }
687 }
688
689 let topic = topic.ok_or("missing TOPIC option on kafka source")?;
690
691 Ok(json!({
692 "topic": topic,
693 "group_id_prefix": group_id_prefix,
694 "connection_id": connection_id,
695 }))
696 };
697
698 let val = parse().map_err(|e| EvalError::InvalidCatalogJson(e.into()))?;
699 let jsonb = Jsonb::from_serde_json(val).expect("valid JSONB");
700 Ok(jsonb)
701}
702
703#[cfg(test)]
704mod tests {
705 use mz_repr::adt::jsonb::Jsonb;
706 use prost::Message as _;
707 use serde_json::json;
708
709 use crate::EvalError;
710
711 fn encode_pg_details(slot: &str, timeline_id: Option<u64>) -> String {
714 let details = super::PostgresPublicationDetailsSubset {
715 slot: slot.to_string(),
716 timeline_id,
717 };
718 hex::encode(details.encode_to_vec())
719 }
720
721 fn pg_source_sql(details_hex: &str) -> String {
722 format!(
723 "CREATE SOURCE \"materialize\".\"public\".\"pg_src\" \
724 IN CLUSTER [u42] \
725 FROM POSTGRES CONNECTION [u10 AS \"materialize\".\"public\".\"pg_conn\"] \
726 (DETAILS = '{details_hex}', PUBLICATION = 'mz_source') \
727 FOR ALL TABLES"
728 )
729 }
730
731 fn kafka_source_sql(with_prefix: bool) -> String {
732 let prefix_opt = if with_prefix {
733 ", GROUP ID PREFIX 'my-prefix-'"
734 } else {
735 ""
736 };
737 format!(
738 "CREATE SOURCE \"materialize\".\"public\".\"k_src\" \
739 IN CLUSTER [u42] \
740 FROM KAFKA CONNECTION [u11 AS \"materialize\".\"public\".\"k_conn\"] \
741 (TOPIC 'test'{prefix_opt}) FORMAT TEXT"
742 )
743 }
744
745 fn as_serde(jsonb: Jsonb) -> serde_json::Value {
746 jsonb.as_ref().to_serde_json()
747 }
748
749 #[mz_ore::test]
752 #[cfg_attr(miri, ignore)] fn pg_happy_path_with_timeline() {
754 let hex = encode_pg_details("materialize_abc", Some(42));
755 let sql = pg_source_sql(&hex);
756 let out = super::parse_postgres_source_details(&sql).expect("ok");
757 assert_eq!(
758 as_serde(out),
759 json!({ "slot": "materialize_abc", "timeline_id": 42 }),
760 );
761 }
762
763 #[mz_ore::test]
764 fn pg_happy_path_null_timeline() {
765 let hex = encode_pg_details("materialize_legacy", None);
768 let sql = pg_source_sql(&hex);
769 let out = super::parse_postgres_source_details(&sql).expect("ok");
770 assert_eq!(
771 as_serde(out),
772 json!({ "slot": "materialize_legacy", "timeline_id": null }),
773 );
774 }
775
776 #[mz_ore::test]
777 fn pg_non_postgres_source_returns_null_jsonb() {
778 let sql = "CREATE SOURCE \"materialize\".\"public\".\"lg\" \
779 IN CLUSTER [u42] FROM LOAD GENERATOR COUNTER";
780 let out = super::parse_postgres_source_details(sql).expect("ok");
781 assert_eq!(as_serde(out), serde_json::Value::Null);
782 }
783
784 #[mz_ore::test]
785 #[cfg_attr(miri, ignore)] fn pg_non_create_source_returns_null_jsonb() {
787 let sql = "CREATE VIEW v AS SELECT 1";
788 let out = super::parse_postgres_source_details(sql).expect("ok");
789 assert_eq!(as_serde(out), serde_json::Value::Null);
790 }
791
792 #[mz_ore::test]
793 fn pg_missing_details_option_errors() {
794 let sql = "CREATE SOURCE \"materialize\".\"public\".\"pg_src\" \
795 IN CLUSTER [u42] \
796 FROM POSTGRES CONNECTION [u10 AS \"materialize\".\"public\".\"pg_conn\"] \
797 (PUBLICATION = 'mz_source') FOR ALL TABLES";
798 let err = super::parse_postgres_source_details(sql).unwrap_err();
799 assert!(
800 matches!(err, EvalError::InvalidCatalogJson(msg) if msg.contains("missing DETAILS")),
801 "wrong error variant/message"
802 );
803 }
804
805 #[mz_ore::test]
806 fn pg_malformed_hex_errors() {
807 let sql = pg_source_sql("not-hex!!");
808 let err = super::parse_postgres_source_details(&sql).unwrap_err();
809 assert!(
810 matches!(err, EvalError::InvalidCatalogJson(msg) if msg.contains("valid hex")),
811 "wrong error variant/message"
812 );
813 }
814
815 #[mz_ore::test]
816 fn pg_malformed_proto_errors() {
817 let sql = pg_source_sql("ffff");
820 let err = super::parse_postgres_source_details(&sql).unwrap_err();
821 assert!(
822 matches!(err, EvalError::InvalidCatalogJson(msg) if msg.contains("publication-details proto")),
823 "wrong error variant/message"
824 );
825 }
826
827 #[mz_ore::test]
830 fn kafka_happy_path_with_prefix() {
831 let sql = kafka_source_sql(true);
832 let out = super::parse_kafka_source_details(&sql).expect("ok");
833 assert_eq!(
834 as_serde(out),
835 json!({
836 "topic": "test",
837 "group_id_prefix": "my-prefix-",
838 "connection_id": "u11",
839 }),
840 );
841 }
842
843 #[mz_ore::test]
844 fn kafka_happy_path_without_prefix() {
845 let sql = kafka_source_sql(false);
846 let out = super::parse_kafka_source_details(&sql).expect("ok");
847 assert_eq!(
848 as_serde(out),
849 json!({
850 "topic": "test",
851 "group_id_prefix": null,
852 "connection_id": "u11",
853 }),
854 );
855 }
856
857 #[mz_ore::test]
858 fn kafka_non_kafka_source_returns_null_jsonb() {
859 let sql = "CREATE SOURCE \"materialize\".\"public\".\"lg\" \
860 IN CLUSTER [u42] FROM LOAD GENERATOR COUNTER";
861 let out = super::parse_kafka_source_details(sql).expect("ok");
862 assert_eq!(as_serde(out), serde_json::Value::Null);
863 }
864
865 #[mz_ore::test]
866 fn kafka_missing_topic_errors() {
867 let sql = "CREATE SOURCE \"materialize\".\"public\".\"k_src\" \
868 IN CLUSTER [u42] \
869 FROM KAFKA CONNECTION [u11 AS \"materialize\".\"public\".\"k_conn\"] \
870 FORMAT TEXT";
871 let err = super::parse_kafka_source_details(sql).unwrap_err();
872 assert!(
873 matches!(err, EvalError::InvalidCatalogJson(msg) if msg.contains("missing TOPIC")),
874 "wrong error variant/message"
875 );
876 }
877
878 #[mz_ore::test]
879 fn kafka_unresolved_connection_errors() {
880 let sql = "CREATE SOURCE \"materialize\".\"public\".\"k_src\" \
884 IN CLUSTER [u42] \
885 FROM KAFKA CONNECTION k_conn (TOPIC 'test') FORMAT TEXT";
886 let err = super::parse_kafka_source_details(sql).unwrap_err();
887 assert!(
888 matches!(err, EvalError::InvalidCatalogJson(msg) if msg.contains("unresolved item name")),
889 "wrong error variant/message"
890 );
891 }
892}