1use std::collections::{BTreeMap, HashMap};
16use std::fmt;
17
18use anyhow::{Context, anyhow};
19use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
20use postgres_openssl::MakeTlsConnector;
21use risingwave_common::bail;
22use risingwave_common::catalog::{ColumnDesc, ColumnId};
23use risingwave_common::types::{DataType, ScalarImpl, StructType};
24use sea_schema::postgres::def::{ColumnType as SeaType, TableDef, TableInfo};
25use sea_schema::postgres::discovery::SchemaDiscovery;
26use sea_schema::sea_query::{Alias, IntoIden};
27use serde::Deserialize;
28use serde_with::{DisplayFromStr, serde_as};
29use sqlx::postgres::{PgConnectOptions, PgSslMode};
30use sqlx::{PgPool, Row};
31use thiserror_ext::AsReport;
32use tokio_postgres::types::Kind as PgKind;
33use tokio_postgres::{Client as PgClient, NoTls};
34
35#[cfg(not(madsim))]
36use super::maybe_tls_connector::MaybeMakeTlsConnector;
37use crate::error::ConnectorResult;
38
39const DISCOVER_PRIMARY_KEY_QUERY: &str = r#"
45 SELECT a.attname as column_name
46 FROM pg_index i
47 JOIN pg_class c ON c.oid = i.indrelid
48 JOIN pg_namespace n ON n.oid = c.relnamespace
49 JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
50 WHERE n.nspname = $1
51 AND c.relname = $2
52 AND i.indisprimary = true
53 ORDER BY array_position(i.indkey, a.attnum)
54"#;
55
56const DISCOVER_PGVECTOR_COLUMNS_QUERY: &str = r#"
60 SELECT
61 a.attname as column_name,
62 a.atttypmod as atttypmod,
63 format_type(a.atttypid, a.atttypmod) as formatted_type
64 FROM pg_attribute a
65 JOIN pg_class c ON c.oid = a.attrelid
66 JOIN pg_namespace n ON n.oid = c.relnamespace
67 JOIN pg_type t ON t.oid = a.atttypid
68 WHERE n.nspname = $1
69 AND c.relname = $2
70 AND t.typname = 'vector'
71 AND a.attnum > 0
72 AND NOT a.attisdropped
73 ORDER BY a.attnum
74"#;
75
76const CHECK_TABLE_PRIVILEGE_QUERY: &str = r#"
77 SELECT
78 current_user::text AS user_name,
79 n.oid IS NOT NULL AS schema_exists,
80 c.oid IS NOT NULL AS table_exists,
81 COALESCE(has_schema_privilege(current_user, n.oid, 'USAGE'), false) AS has_schema_usage,
82 COALESCE(has_table_privilege(current_user, c.oid, $3), false) AS has_table_privilege,
83 COALESCE(has_any_column_privilege(current_user, c.oid, $3), false) AS has_any_column_privilege
84 FROM (SELECT 1) AS one
85 LEFT JOIN pg_namespace n ON n.nspname = $1
86 LEFT JOIN pg_class c ON c.relnamespace = n.oid
87 AND c.relname = $2
88 AND c.relkind IN ('r', 'p')
89 LIMIT 1
90"#;
91
92#[derive(Debug, Clone)]
97pub struct PgConnectionConfig {
98 pub host: String,
99 pub port: u16,
100 pub user: String,
101 pub password: String,
102 pub database: String,
103 pub ssl_mode: SslMode,
104 pub ssl_root_cert: Option<String>,
105}
106
107impl PgConnectionConfig {
108 fn to_sqlx_connect_options(&self) -> PgConnectOptions {
109 let mut options = PgConnectOptions::new()
110 .username(&self.user)
111 .password(&self.password)
112 .host(&self.host)
113 .port(self.port)
114 .database(&self.database)
115 .ssl_mode(match self.ssl_mode {
116 SslMode::Disabled => PgSslMode::Disable,
117 SslMode::Preferred => PgSslMode::Prefer,
118 SslMode::Required => PgSslMode::Require,
119 SslMode::VerifyCa => PgSslMode::VerifyCa,
120 SslMode::VerifyFull => PgSslMode::VerifyFull,
121 });
122
123 if matches!(self.ssl_mode, SslMode::VerifyCa | SslMode::VerifyFull)
124 && let Some(root_cert) = &self.ssl_root_cert
125 {
126 options = options.ssl_root_cert(root_cert.as_str());
127 }
128
129 options
130 }
131}
132
133#[serde_as]
137#[derive(Debug, Clone, Deserialize)]
138pub struct TcpKeepaliveConfig {
139 #[serde(rename = "tcp.keepalive.idle")]
140 #[serde_as(as = "DisplayFromStr")]
141 pub tcp_keepalive_idle: u32,
142 #[serde(rename = "tcp.keepalive.interval")]
143 #[serde_as(as = "DisplayFromStr")]
144 pub tcp_keepalive_interval: u32,
145 #[serde(rename = "tcp.keepalive.count")]
146 #[serde_as(as = "DisplayFromStr")]
147 pub tcp_keepalive_count: u32,
148}
149
150impl Default for TcpKeepaliveConfig {
151 fn default() -> Self {
152 Self {
153 tcp_keepalive_idle: 10 * 60,
154 tcp_keepalive_interval: 10,
155 tcp_keepalive_count: 3,
156 }
157 }
158}
159
160pub fn pg_connection_config_from_properties(
161 props: &BTreeMap<String, String>,
162) -> ConnectorResult<PgConnectionConfig> {
163 Ok(PgConnectionConfig {
164 host: props
165 .get("hostname")
166 .context("missing `hostname` in postgres-cdc properties")?
167 .clone(),
168 port: {
169 let raw = props
170 .get("port")
171 .context("missing `port` in postgres-cdc properties")?;
172 raw.parse::<u16>()
173 .with_context(|| format!("invalid postgres port `{}`", raw))?
174 },
175 user: props
176 .get("username")
177 .context("missing `username` in postgres-cdc properties")?
178 .clone(),
179 password: props.get("password").cloned().unwrap_or_default(),
180 database: props
181 .get("database.name")
182 .context("missing `database.name` in postgres-cdc properties")?
183 .clone(),
184 ssl_mode: props
185 .get("ssl.mode")
186 .and_then(|v| v.parse::<SslMode>().ok())
187 .unwrap_or_default(),
188 ssl_root_cert: props.get("ssl.root.cert").cloned(),
189 })
190}
191
192pub async fn create_pg_client_from_properties(
193 props: &BTreeMap<String, String>,
194 tcp_keepalive: Option<TcpKeepaliveConfig>,
195) -> ConnectorResult<PgClient> {
196 let config = pg_connection_config_from_properties(props)?;
197 create_pg_client(&config, tcp_keepalive)
198 .await
199 .map_err(Into::into)
200}
201
202pub async fn discover_pgvector_dimensions(
203 client: &PgClient,
204 schema: &str,
205 table: &str,
206) -> ConnectorResult<HashMap<String, usize>> {
207 let rows = client
208 .query(DISCOVER_PGVECTOR_COLUMNS_QUERY, &[&schema, &table])
209 .await?;
210
211 let mut dims = HashMap::new();
212 for row in rows {
213 let col_name: String = row.get("column_name");
214 let atttypmod: i32 = row.get("atttypmod");
215 if atttypmod > 0
216 && let Ok(dim) = usize::try_from(atttypmod)
217 {
218 dims.insert(col_name, dim);
219 }
220 }
221 Ok(dims)
222}
223
224#[derive(Debug, Clone, PartialEq, Deserialize, Default)]
225#[serde(rename_all = "lowercase")]
226pub enum SslMode {
227 #[serde(alias = "disable")]
228 Disabled,
229 #[serde(alias = "prefer")]
230 #[default]
231 Preferred,
232 #[serde(alias = "require")]
233 Required,
234 #[serde(alias = "verify-ca")]
237 VerifyCa,
238 #[serde(alias = "verify-full")]
241 VerifyFull,
242}
243
244pub struct PostgresExternalTable {
245 column_descs: Vec<ColumnDesc>,
246 pk_names: Vec<String>,
247}
248
249struct PostgresTablePrivilege {
250 user_name: String,
251 schema_exists: bool,
252 table_exists: bool,
253 has_schema_usage: bool,
254 has_table_privilege: bool,
255 has_any_column_privilege: bool,
256}
257
258impl PostgresExternalTable {
259 async fn discover_primary_key(
262 connection: &PgPool,
263 schema_name: &str,
264 table_name: &str,
265 ) -> ConnectorResult<Vec<String>> {
266 let rows = sqlx::query(DISCOVER_PRIMARY_KEY_QUERY)
267 .bind(schema_name)
268 .bind(table_name)
269 .fetch_all(connection)
270 .await
271 .context("Failed to discover primary key columns")?;
272
273 let pk_columns = rows
274 .into_iter()
275 .map(|row| row.get::<String, _>("column_name"))
276 .collect();
277
278 Ok(pk_columns)
279 }
280
281 async fn discover_pk_and_full_columns(
285 config: &PgConnectionConfig,
286 schema: &str,
287 table: &str,
288 required_table_privilege: Option<&str>,
289 ) -> ConnectorResult<(Vec<sea_schema::postgres::def::ColumnInfo>, Vec<String>)> {
290 let options = config.to_sqlx_connect_options();
291 let connection = PgPool::connect_with(options).await?;
292
293 let schema_discovery = SchemaDiscovery::new(connection.clone(), schema);
295 let empty_map: HashMap<String, Vec<String>> = HashMap::new();
296 let columns = schema_discovery
297 .discover_columns(
298 Alias::new(schema).into_iden(),
299 Alias::new(table).into_iden(),
300 &empty_map,
301 )
302 .await?;
303
304 let pgvector_columns = sqlx::query(DISCOVER_PGVECTOR_COLUMNS_QUERY)
305 .bind(schema)
306 .bind(table)
307 .fetch_all(&connection)
308 .await
309 .context("Failed to discover PostgreSQL pgvector columns")?;
310 let formatted_type_by_column: HashMap<String, String> = pgvector_columns
311 .into_iter()
312 .map(|row| {
313 (
314 row.get::<String, _>("column_name"),
315 row.get::<String, _>("formatted_type"),
316 )
317 })
318 .collect();
319
320 let mut columns = columns;
323 if let Some(privilege) = required_table_privilege {
324 Self::ensure_table_privilege(&connection, schema, table, privilege).await?;
325 }
326
327 for col in &mut columns {
328 if let SeaType::Unknown(name) = &col.col_type
329 && name.eq_ignore_ascii_case("vector")
330 && let Some(formatted_type) = formatted_type_by_column.get(&col.name)
331 {
332 col.col_type = SeaType::Unknown(formatted_type.clone());
333 }
334 }
335
336 let pk_columns = Self::discover_primary_key(&connection, schema, table).await?;
338
339 Ok((columns, pk_columns))
340 }
341
342 async fn ensure_table_privilege(
343 connection: &PgPool,
344 schema: &str,
345 table: &str,
346 required_privilege: &str,
347 ) -> ConnectorResult<()> {
348 let row = sqlx::query(CHECK_TABLE_PRIVILEGE_QUERY)
349 .bind(schema)
350 .bind(table)
351 .bind(required_privilege)
352 .fetch_one(connection)
353 .await
354 .context("Failed to check PostgreSQL table privileges")?;
355
356 let privilege_status = PostgresTablePrivilege {
357 user_name: row.get("user_name"),
358 schema_exists: row.get("schema_exists"),
359 table_exists: row.get("table_exists"),
360 has_schema_usage: row.get("has_schema_usage"),
361 has_table_privilege: row.get("has_table_privilege"),
362 has_any_column_privilege: row.get("has_any_column_privilege"),
363 };
364
365 if !privilege_status.schema_exists {
366 return Err(anyhow!("PostgreSQL schema `{schema}` does not exist").into());
367 }
368
369 if !privilege_status.table_exists {
370 return Err(anyhow!("PostgreSQL table `{schema}`.`{table}` does not exist").into());
371 }
372
373 if !privilege_status.has_schema_usage {
374 return Err(anyhow!(
375 "PostgreSQL table {} exists, but the connection user `{}` does not have USAGE privilege on schema `{}`. Grant privileges on the upstream PostgreSQL database: {}",
376 format_pg_table_name(schema, table),
377 privilege_status.user_name,
378 schema,
379 format_grant_usage(schema, &privilege_status.user_name),
380 )
381 .into());
382 }
383
384 if !privilege_status.has_table_privilege {
385 let column_privilege_msg = if privilege_status.has_any_column_privilege {
386 " The user has column-level privilege on at least one column, but RisingWave requires table-level privilege for CDC schema discovery and snapshot reads."
387 } else {
388 ""
389 };
390 return Err(anyhow!(
391 "PostgreSQL table {} exists, but the connection user `{}` does not have {} privilege on it.{} Grant privileges on the upstream PostgreSQL database: {}",
392 format_pg_table_name(schema, table),
393 privilege_status.user_name,
394 required_privilege,
395 column_privilege_msg,
396 format_required_table_grants(
397 schema,
398 table,
399 &privilege_status.user_name,
400 required_privilege
401 ),
402 )
403 .into());
404 }
405
406 Ok(())
407 }
408
409 async fn discover_schema(
410 config: &PgConnectionConfig,
411 schema: &str,
412 table: &str,
413 ) -> ConnectorResult<TableDef> {
414 let options = config.to_sqlx_connect_options();
415 let connection = PgPool::connect_with(options).await?;
416 let schema_discovery = SchemaDiscovery::new(connection, schema);
417 let empty_map = HashMap::new();
419 let table_schema = schema_discovery
420 .discover_table(
421 TableInfo {
422 name: table.to_owned(),
423 of_type: None,
424 },
425 &empty_map,
426 )
427 .await?;
428 Ok(table_schema)
429 }
430
431 pub async fn connect(
432 config: &PgConnectionConfig,
433 schema: &str,
434 table: &str,
435 is_append_only: bool,
436 required_table_privilege: Option<&str>,
437 ) -> ConnectorResult<Self> {
438 tracing::debug!("connect to postgres external table");
439
440 let (columns, pk_names) =
441 Self::discover_pk_and_full_columns(config, schema, table, required_table_privilege)
442 .await?;
443
444 let mut column_descs = vec![];
445 for col in &columns {
446 let rw_data_type = sea_type_to_rw_type(&col.col_type)?;
447 let column_desc = if let Some(ref default_expr) = col.default {
448 let val_text = default_expr
451 .0
452 .split("::")
453 .map(|s| s.trim_matches('\''))
454 .next()
455 .expect("default value expression");
456
457 match ScalarImpl::from_text(val_text, &rw_data_type) {
458 Ok(scalar) => ColumnDesc::named_with_default_value(
459 col.name.clone(),
460 ColumnId::placeholder(),
461 rw_data_type.clone(),
462 Some(scalar),
463 ),
464 Err(err) => {
465 tracing::warn!(error=%err.as_report(), "failed to parse postgres default value expression, only constant is supported");
466 ColumnDesc::named(col.name.clone(), ColumnId::placeholder(), rw_data_type)
467 }
468 }
469 } else {
470 ColumnDesc::named(col.name.clone(), ColumnId::placeholder(), rw_data_type)
471 };
472 column_descs.push(column_desc);
473 }
474
475 if !is_append_only && pk_names.is_empty() {
477 return Err(anyhow!(
478 "Postgres table should define the primary key for non-append-only tables"
479 )
480 .into());
481 }
482
483 Ok(Self {
484 column_descs,
485 pk_names,
486 })
487 }
488
489 pub async fn type_mapping(
491 config: &PgConnectionConfig,
492 schema: &str,
493 table: &str,
494 is_append_only: bool,
495 ) -> ConnectorResult<HashMap<String, tokio_postgres::types::Type>> {
496 tracing::debug!("connect to postgres external table to get type mapping");
497 let table_schema = Self::discover_schema(config, schema, table).await?;
498 let mut column_name_to_pg_type = HashMap::new();
499 for col in &table_schema.columns {
500 let pg_type = sea_type_to_pg_type(&col.col_type)?;
501 column_name_to_pg_type.insert(col.name.clone(), pg_type);
502 }
503 if !is_append_only && table_schema.primary_key_constraints.is_empty() {
504 return Err(anyhow!(
505 "Postgres table should define the primary key for non-append-only tables"
506 )
507 .into());
508 }
509 Ok(column_name_to_pg_type)
510 }
511
512 pub fn column_descs(&self) -> &Vec<ColumnDesc> {
513 &self.column_descs
514 }
515
516 pub fn pk_names(&self) -> &Vec<String> {
517 &self.pk_names
518 }
519}
520
521fn format_pg_table_name(schema: &str, table: &str) -> String {
522 format!(
523 "{}.{}",
524 quote_pg_identifier(schema),
525 quote_pg_identifier(table)
526 )
527}
528
529fn format_grant_usage(schema: &str, user_name: &str) -> String {
530 format!(
531 "GRANT USAGE ON SCHEMA {} TO {};",
532 quote_pg_identifier(schema),
533 quote_pg_identifier(user_name)
534 )
535}
536
537fn format_grant_table_privilege(
538 schema: &str,
539 table: &str,
540 user_name: &str,
541 privilege: &str,
542) -> String {
543 format!(
544 "GRANT {} ON TABLE {} TO {};",
545 privilege,
546 format_pg_table_name(schema, table),
547 quote_pg_identifier(user_name)
548 )
549}
550
551fn format_required_table_grants(
552 schema: &str,
553 table: &str,
554 user_name: &str,
555 privilege: &str,
556) -> String {
557 format!(
558 "{} {}",
559 format_grant_usage(schema, user_name),
560 format_grant_table_privilege(schema, table, user_name, privilege)
561 )
562}
563
564fn quote_pg_identifier(identifier: &str) -> String {
565 format!("\"{}\"", identifier.replace('"', "\"\""))
566}
567
568impl fmt::Display for SslMode {
569 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
570 f.write_str(match self {
571 SslMode::Disabled => "disabled",
572 SslMode::Preferred => "preferred",
573 SslMode::Required => "required",
574 SslMode::VerifyCa => "verify-ca",
575 SslMode::VerifyFull => "verify-full",
576 })
577 }
578}
579
580impl std::str::FromStr for SslMode {
581 type Err = serde_json::Error;
582
583 fn from_str(s: &str) -> Result<Self, Self::Err> {
584 serde_json::from_value(serde_json::Value::String(s.to_owned()))
585 }
586}
587
588pub async fn create_pg_client(
589 config: &PgConnectionConfig,
590 tcp_keepalive: Option<TcpKeepaliveConfig>,
591) -> anyhow::Result<PgClient> {
592 let mut pg_config = tokio_postgres::Config::new();
593 pg_config
594 .user(&config.user)
595 .password(&config.password)
596 .host(&config.host)
597 .port(config.port)
598 .dbname(&config.database);
599
600 if let Some(keepalive) = tcp_keepalive {
602 pg_config.keepalives(true);
603 pg_config.keepalives_idle(std::time::Duration::from_secs(
604 keepalive.tcp_keepalive_idle as u64,
605 ));
606 #[cfg(not(target_os = "windows"))]
607 {
608 pg_config.keepalives_interval(std::time::Duration::from_secs(
609 keepalive.tcp_keepalive_interval as u64,
610 ));
611 pg_config.keepalives_retries(keepalive.tcp_keepalive_count);
612 }
613 tracing::info!(
614 "TCP keepalive enabled: idle={}s, interval={}s, retries={}",
615 keepalive.tcp_keepalive_idle,
616 keepalive.tcp_keepalive_interval,
617 keepalive.tcp_keepalive_count
618 );
619 }
620
621 let verify_hostname = matches!(config.ssl_mode, SslMode::VerifyFull);
622
623 #[cfg(not(madsim))]
624 let connector = match config.ssl_mode {
625 SslMode::Disabled => {
626 pg_config.ssl_mode(tokio_postgres::config::SslMode::Disable);
627 MaybeMakeTlsConnector::NoTls(NoTls)
628 }
629 SslMode::Preferred => {
630 pg_config.ssl_mode(tokio_postgres::config::SslMode::Prefer);
631 match SslConnector::builder(SslMethod::tls()) {
632 Ok(mut builder) => {
633 builder.set_verify(SslVerifyMode::NONE);
635 MaybeMakeTlsConnector::Tls(MakeTlsConnector::new(builder.build()))
636 }
637 Err(e) => {
638 tracing::warn!(error = %e.as_report(), "SSL connector error");
639 MaybeMakeTlsConnector::NoTls(NoTls)
640 }
641 }
642 }
643 SslMode::Required => {
644 pg_config.ssl_mode(tokio_postgres::config::SslMode::Require);
645 let mut builder = SslConnector::builder(SslMethod::tls())?;
646 builder.set_verify(SslVerifyMode::NONE);
648 MaybeMakeTlsConnector::Tls(MakeTlsConnector::new(builder.build()))
649 }
650
651 SslMode::VerifyCa | SslMode::VerifyFull => {
652 pg_config.ssl_mode(tokio_postgres::config::SslMode::Require);
653 let mut builder = SslConnector::builder(SslMethod::tls())?;
654 if let Some(ssl_root_cert) = &config.ssl_root_cert {
655 builder.set_ca_file(ssl_root_cert).map_err(|e| {
656 anyhow!(format!("bad ssl root cert error: {}", e.to_report_string()))
657 })?;
658 }
659 let mut connector = MakeTlsConnector::new(builder.build());
660 if !verify_hostname {
661 connector.set_callback(|c, _| {
662 c.set_verify_hostname(false);
663 Ok(())
664 });
665 }
666 MaybeMakeTlsConnector::Tls(connector)
667 }
668 };
669 #[cfg(madsim)]
670 let connector = NoTls;
671
672 let (client, connection) = pg_config.connect(connector).await?;
673
674 tokio::spawn(async move {
675 if let Err(e) = connection.await {
676 tracing::error!(error = %e.as_report(), "postgres connection error");
677 }
678 });
679
680 Ok(client)
681}
682
683pub fn sea_type_to_rw_type(col_type: &SeaType) -> ConnectorResult<DataType> {
685 let dtype = match col_type {
686 SeaType::SmallInt | SeaType::SmallSerial => DataType::Int16,
687 SeaType::Integer | SeaType::Serial => DataType::Int32,
688 SeaType::BigInt | SeaType::BigSerial => DataType::Int64,
689 SeaType::Money | SeaType::Decimal(_) | SeaType::Numeric(_) => DataType::Decimal,
690 SeaType::Real => DataType::Float32,
691 SeaType::DoublePrecision => DataType::Float64,
692 SeaType::Varchar(_) | SeaType::Char(_) | SeaType::Text => DataType::Varchar,
693 SeaType::Bytea => DataType::Bytea,
694 SeaType::Timestamp(_) => DataType::Timestamp,
695 SeaType::TimestampWithTimeZone(_) => DataType::Timestamptz,
696 SeaType::Date => DataType::Date,
697 SeaType::Time(_) | SeaType::TimeWithTimeZone(_) => DataType::Time,
698 SeaType::Interval(_) => DataType::Interval,
699 SeaType::Boolean => DataType::Boolean,
700 SeaType::Point => DataType::Struct(StructType::new(vec![
701 ("x", DataType::Float32),
702 ("y", DataType::Float32),
703 ])),
704 SeaType::Uuid => DataType::Varchar,
705 SeaType::Xml => DataType::Varchar,
706 SeaType::Json => DataType::Jsonb,
707 SeaType::JsonBinary => DataType::Jsonb,
708 SeaType::Array(def) => {
709 let item_type = match def.col_type.as_ref() {
710 Some(ty) => sea_type_to_rw_type(ty.as_ref())?,
711 None => {
712 return Err(anyhow!("ARRAY type missing element type").into());
713 }
714 };
715
716 DataType::list(item_type)
717 }
718 SeaType::PgLsn => DataType::Int64,
719 SeaType::Cidr
720 | SeaType::Inet
721 | SeaType::MacAddr
722 | SeaType::MacAddr8
723 | SeaType::Int4Range
724 | SeaType::Int8Range
725 | SeaType::NumRange
726 | SeaType::TsRange
727 | SeaType::TsTzRange
728 | SeaType::DateRange
729 | SeaType::Enum(_) => DataType::Varchar,
730 SeaType::Line
731 | SeaType::Lseg
732 | SeaType::Box
733 | SeaType::Path
734 | SeaType::Polygon
735 | SeaType::Circle
736 | SeaType::Bit(_)
737 | SeaType::VarBit(_)
738 | SeaType::TsVector
739 | SeaType::TsQuery => {
740 bail!("{:?} type not supported", col_type);
741 }
742 SeaType::Unknown(name) => {
743 if let Some(dim) = parse_pgvector_dimension(name)? {
744 DataType::Vector(dim)
745 } else {
746 tracing::warn!("Unknown Postgres data type: {name}, map to varchar");
748 DataType::Varchar
749 }
750 }
751 };
752
753 Ok(dtype)
754}
755
756fn parse_pgvector_dimension(type_name: &str) -> ConnectorResult<Option<usize>> {
757 let normalized = type_name.trim().to_ascii_lowercase();
758 if normalized == "vector" {
759 bail!("pgvector type `vector` is missing dimension, expected `vector(n)`")
760 }
761 if !normalized.starts_with("vector(") || !normalized.ends_with(')') {
762 return Ok(None);
763 }
764
765 let dim_text = normalized
766 .trim_start_matches("vector(")
767 .trim_end_matches(')')
768 .trim();
769 let dim = dim_text
770 .parse::<usize>()
771 .map_err(|_| anyhow!("invalid pgvector dimension in type `{type_name}`"))?;
772
773 if !(1..=DataType::VEC_MAX_SIZE).contains(&dim) {
774 bail!(
775 "pgvector dimension out of range in type `{}`: expect 1..={}",
776 type_name,
777 DataType::VEC_MAX_SIZE
778 );
779 }
780
781 Ok(Some(dim))
782}
783
784fn sea_type_to_pg_type(sea_type: &SeaType) -> ConnectorResult<tokio_postgres::types::Type> {
789 use tokio_postgres::types::Type as PgType;
790 match sea_type {
791 SeaType::SmallInt => Ok(PgType::INT2),
792 SeaType::Integer => Ok(PgType::INT4),
793 SeaType::BigInt => Ok(PgType::INT8),
794 SeaType::Decimal(_) => Ok(PgType::NUMERIC),
795 SeaType::Numeric(_) => Ok(PgType::NUMERIC),
796 SeaType::Real => Ok(PgType::FLOAT4),
797 SeaType::DoublePrecision => Ok(PgType::FLOAT8),
798 SeaType::Varchar(_) => Ok(PgType::VARCHAR),
799 SeaType::Char(_) => Ok(PgType::CHAR),
800 SeaType::Text => Ok(PgType::TEXT),
801 SeaType::Bytea => Ok(PgType::BYTEA),
802 SeaType::Timestamp(_) => Ok(PgType::TIMESTAMP),
803 SeaType::TimestampWithTimeZone(_) => Ok(PgType::TIMESTAMPTZ),
804 SeaType::Date => Ok(PgType::DATE),
805 SeaType::Time(_) => Ok(PgType::TIME),
806 SeaType::TimeWithTimeZone(_) => Ok(PgType::TIMETZ),
807 SeaType::Interval(_) => Ok(PgType::INTERVAL),
808 SeaType::Boolean => Ok(PgType::BOOL),
809 SeaType::Point => Ok(PgType::POINT),
810 SeaType::Uuid => Ok(PgType::UUID),
811 SeaType::Json => Ok(PgType::JSON),
812 SeaType::JsonBinary => Ok(PgType::JSONB),
813 SeaType::Array(t) => {
814 let Some(t) = t.col_type.as_ref() else {
815 bail!("missing array type")
816 };
817 match t.as_ref() {
818 SeaType::SmallInt => Ok(PgType::INT2_ARRAY),
820 SeaType::Integer => Ok(PgType::INT4_ARRAY),
821 SeaType::BigInt => Ok(PgType::INT8_ARRAY),
822 SeaType::Decimal(_) => Ok(PgType::NUMERIC_ARRAY),
823 SeaType::Numeric(_) => Ok(PgType::NUMERIC_ARRAY),
824 SeaType::Real => Ok(PgType::FLOAT4_ARRAY),
825 SeaType::DoublePrecision => Ok(PgType::FLOAT8_ARRAY),
826 SeaType::Varchar(_) => Ok(PgType::VARCHAR_ARRAY),
827 SeaType::Char(_) => Ok(PgType::CHAR_ARRAY),
828 SeaType::Text => Ok(PgType::TEXT_ARRAY),
829 SeaType::Bytea => Ok(PgType::BYTEA_ARRAY),
830 SeaType::Timestamp(_) => Ok(PgType::TIMESTAMP_ARRAY),
831 SeaType::TimestampWithTimeZone(_) => Ok(PgType::TIMESTAMPTZ_ARRAY),
832 SeaType::Date => Ok(PgType::DATE_ARRAY),
833 SeaType::Time(_) => Ok(PgType::TIME_ARRAY),
834 SeaType::TimeWithTimeZone(_) => Ok(PgType::TIMETZ_ARRAY),
835 SeaType::Interval(_) => Ok(PgType::INTERVAL_ARRAY),
836 SeaType::Boolean => Ok(PgType::BOOL_ARRAY),
837 SeaType::Point => Ok(PgType::POINT_ARRAY),
838 SeaType::Uuid => Ok(PgType::UUID_ARRAY),
839 SeaType::Json => Ok(PgType::JSON_ARRAY),
840 SeaType::JsonBinary => Ok(PgType::JSONB_ARRAY),
841 SeaType::Array(_) => bail!("nested array type is not supported"),
842 SeaType::Unknown(name) => {
843 Ok(PgType::new(
845 name.clone(),
846 0,
847 PgKind::Array(PgType::new(
848 name.clone(),
849 0,
850 PgKind::Enum(vec![]),
851 "".into(),
852 )),
853 "".into(),
854 ))
855 }
856 _ => bail!("unsupported array type: {:?}", t),
857 }
858 }
859 SeaType::Unknown(name) => {
860 Ok(PgType::new(
862 name.clone(),
863 0,
864 PgKind::Enum(vec![]),
865 "".into(),
866 ))
867 }
868 _ => bail!("unsupported type: {:?}", sea_type),
869 }
870}
871
872#[cfg(test)]
873mod tests {
874 use super::{
875 format_grant_table_privilege, format_grant_usage, format_pg_table_name,
876 format_required_table_grants, parse_pgvector_dimension,
877 };
878
879 #[test]
880 fn test_parse_pgvector_dimension() {
881 assert_eq!(parse_pgvector_dimension("vector(3)").unwrap(), Some(3));
882 assert_eq!(parse_pgvector_dimension("VECTOR(768)").unwrap(), Some(768));
883 assert_eq!(parse_pgvector_dimension("varchar").unwrap(), None);
884 }
885
886 #[test]
887 fn test_parse_pgvector_dimension_requires_size() {
888 let err = parse_pgvector_dimension("vector").unwrap_err();
889 assert!(err.to_string().contains("missing dimension"));
890 }
891
892 #[test]
893 fn test_format_postgres_privilege_grants_quote_identifiers() {
894 assert_eq!(
895 format_pg_table_name("public", "GlobalBrandContentAnalysis"),
896 r#""public"."GlobalBrandContentAnalysis""#
897 );
898 assert_eq!(
899 format_grant_usage("tenant schema", r#"cdc"user"#),
900 r#"GRANT USAGE ON SCHEMA "tenant schema" TO "cdc""user";"#
901 );
902 assert_eq!(
903 format_grant_table_privilege("tenant schema", "Orders", r#"cdc"user"#, "SELECT"),
904 r#"GRANT SELECT ON TABLE "tenant schema"."Orders" TO "cdc""user";"#
905 );
906 assert_eq!(
907 format_required_table_grants("tenant schema", "Orders", r#"cdc"user"#, "SELECT"),
908 r#"GRANT USAGE ON SCHEMA "tenant schema" TO "cdc""user"; GRANT SELECT ON TABLE "tenant schema"."Orders" TO "cdc""user";"#
909 );
910 }
911}