1#[must_use]
12pub fn default_name_for_pk(table: &str) -> String {
13 format!("{table}_pkey")
14}
15
16#[must_use]
18pub fn default_name_for_fk(
19 table: &str,
20 columns: &[String],
21 _table_to: &str,
22 _columns_to: &[String],
23) -> String {
24 let first_column = columns.first().map_or("", String::as_str);
25 let desired = format!("{table}_{first_column}_fkey");
26
27 if desired.len() > 63 {
29 let hash = hash_string(&desired);
30 if table.len() < 63 - 18 {
31 format!("{table}_{hash}_fkey")
32 } else {
33 format!("{hash}_fkey")
34 }
35 } else {
36 desired
37 }
38}
39
40#[must_use]
42pub fn default_name_for_unique(table: &str, columns: &[String]) -> String {
43 truncate_identifier(&format!("{}_{}_key", table, columns.join("_")), "_key")
44}
45
46#[must_use]
48pub fn default_name_for_index(table: &str, columns: &[String]) -> String {
49 truncate_identifier(&format!("{}_{}_idx", table, columns.join("_")), "_idx")
50}
51
52#[must_use]
54pub fn default_name_for_identity_sequence(table: &str, column: &str) -> String {
55 format!("{table}_{column}_seq")
56}
57
58#[must_use]
66pub fn default_name_for_check(table: &str, index: usize) -> String {
67 format!("{table}_check{}", index + 1)
68}
69
70fn hash_string(s: &str) -> String {
76 use sha2::{Digest, Sha256};
77
78 let digest = Sha256::digest(s.as_bytes());
79 let mut out = String::with_capacity(12);
80 for byte in digest.iter().take(6) {
81 use std::fmt::Write;
82 let _ = write!(out, "{byte:02x}");
83 }
84 out
85}
86
87fn truncate_identifier(name: &str, suffix: &str) -> String {
91 const MAX_IDENTIFIER_LEN: usize = 63;
92 if name.len() <= MAX_IDENTIFIER_LEN {
93 return name.to_string();
94 }
95
96 let hash = hash_string(name);
97 let budget = MAX_IDENTIFIER_LEN - hash.len() - 1 - suffix.len();
99 let mut cutoff = budget.min(name.len());
100 while !name.is_char_boundary(cutoff) {
101 cutoff -= 1;
102 }
103 format!("{}_{hash}{suffix}", &name[..cutoff])
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub enum PgTypeCategory {
113 SmallInt,
114 Integer,
115 BigInt,
116 Numeric,
117 Real,
118 DoublePrecision,
119 Boolean,
120 Char,
121 Varchar,
122 Text,
123 Json,
124 Jsonb,
125 Time,
126 TimeTz,
127 Timestamp,
128 TimestampTz,
129 Date,
130 Uuid,
131 Interval,
132 Inet,
133 Cidr,
134 MacAddr,
135 MacAddr8,
136 Vector,
137 HalfVec,
138 SparseVec,
139 Bit,
140 Point,
141 Line,
142 Geometry,
143 Serial,
144 SmallSerial,
145 BigSerial,
146 Enum,
147 Custom,
148}
149
150impl PgTypeCategory {
151 fn type_name_rest<'a>(s: &'a str, type_name: &str) -> Option<&'a str> {
152 if !s.starts_with(type_name) {
153 return None;
154 }
155 let rest = &s[type_name.len()..];
156 if rest
157 .chars()
158 .next()
159 .is_some_and(|ch| ch.is_ascii_alphanumeric() || ch == '_')
160 {
161 return None;
162 }
163 Some(rest)
164 }
165
166 fn first_type_argument<'a>(s: &'a str, type_name: &str) -> Option<&'a str> {
167 let rest = Self::type_name_rest(s, type_name)?.trim_start();
168 let body = rest.strip_prefix('(')?;
169 let end = body.find([',', ')'])?;
170 Some(body[..end].trim())
171 }
172
173 fn match_numeric(s: &str) -> Option<Self> {
176 if s.starts_with("smallserial") {
178 return Some(Self::SmallSerial);
179 }
180 if s.starts_with("bigserial") {
181 return Some(Self::BigSerial);
182 }
183 if s.starts_with("serial") {
184 return Some(Self::Serial);
185 }
186
187 if s.starts_with("smallint") || s == "int2" {
188 return Some(Self::SmallInt);
189 }
190 if s.starts_with("integer") || s == "int" || s == "int4" {
191 return Some(Self::Integer);
192 }
193 if s.starts_with("bigint") || s == "int8" {
194 return Some(Self::BigInt);
195 }
196 if s.starts_with("numeric") || s.starts_with("decimal") {
197 return Some(Self::Numeric);
198 }
199 if s.starts_with("real") || s == "float4" {
200 return Some(Self::Real);
201 }
202 if s.starts_with("double") {
203 return Some(Self::DoublePrecision);
204 }
205 if s.starts_with("boolean") || s == "bool" {
206 return Some(Self::Boolean);
207 }
208 None
209 }
210
211 fn match_string_or_json(s: &str) -> Option<Self> {
214 if s.starts_with("varchar") || s.starts_with("character varying") {
215 return Some(Self::Varchar);
216 }
217 if s.starts_with("char") || s.starts_with("character") {
218 return Some(Self::Char);
219 }
220 if s.starts_with("text") {
221 return Some(Self::Text);
222 }
223 if s.starts_with("jsonb") {
224 return Some(Self::Jsonb);
225 }
226 if s.starts_with("json") {
227 return Some(Self::Json);
228 }
229 None
230 }
231
232 fn match_temporal(s: &str) -> Option<Self> {
235 if s.starts_with("timestamp") && s.contains("with time zone") {
236 return Some(Self::TimestampTz);
237 }
238 if s.starts_with("timestamp") {
239 return Some(Self::Timestamp);
240 }
241 if s.starts_with("time") && s.contains("with time zone") {
242 return Some(Self::TimeTz);
243 }
244 if s.starts_with("time") {
245 return Some(Self::Time);
246 }
247 if s.starts_with("date") {
248 return Some(Self::Date);
249 }
250 if s.starts_with("interval") {
251 return Some(Self::Interval);
252 }
253 None
254 }
255
256 fn match_specialized(s: &str) -> Option<Self> {
258 if s.starts_with("uuid") {
259 return Some(Self::Uuid);
260 }
261 if s.starts_with("inet") {
262 return Some(Self::Inet);
263 }
264 if s.starts_with("cidr") {
265 return Some(Self::Cidr);
266 }
267 if s.starts_with("macaddr8") {
269 return Some(Self::MacAddr8);
270 }
271 if s.starts_with("macaddr") {
272 return Some(Self::MacAddr);
273 }
274 if s.starts_with("vector") {
275 return Some(Self::Vector);
276 }
277 if s.starts_with("halfvec") {
278 return Some(Self::HalfVec);
279 }
280 if s.starts_with("sparsevec") {
281 return Some(Self::SparseVec);
282 }
283 if s.starts_with("bit") {
284 return Some(Self::Bit);
285 }
286 if Self::type_name_rest(s, "geometry").is_some() {
287 return Some(match Self::first_type_argument(s, "geometry") {
288 Some("point") => Self::Geometry,
289 _ => Self::Custom,
290 });
291 }
292 if Self::type_name_rest(s, "geography").is_some()
293 || Self::type_name_rest(s, "box2d").is_some()
294 || Self::type_name_rest(s, "box3d").is_some()
295 || Self::type_name_rest(s, "raster").is_some()
296 {
297 return Some(Self::Custom);
298 }
299 if s.starts_with("point") {
300 return Some(Self::Point);
301 }
302 if s.starts_with("line") {
303 return Some(Self::Line);
304 }
305 None
306 }
307
308 #[must_use]
310 pub fn from_sql_type(sql_type: &str) -> Self {
311 let s = sql_type.trim().to_lowercase();
312
313 Self::match_numeric(&s)
314 .or_else(|| Self::match_string_or_json(&s))
315 .or_else(|| Self::match_temporal(&s))
316 .or_else(|| Self::match_specialized(&s))
317 .unwrap_or(Self::Custom)
318 }
319
320 #[must_use]
322 pub const fn drizzle_import(&self) -> &'static str {
323 match self {
324 Self::SmallInt => "smallint",
325 Self::Integer => "integer",
326 Self::BigInt => "bigint",
327 Self::Numeric => "numeric",
328 Self::Real => "real",
329 Self::DoublePrecision => "doublePrecision",
330 Self::Boolean => "boolean",
331 Self::Char => "char",
332 Self::Varchar => "varchar",
333 Self::Text => "text",
334 Self::Json => "json",
335 Self::Jsonb => "jsonb",
336 Self::Time | Self::TimeTz => "time",
337 Self::Timestamp | Self::TimestampTz => "timestamp",
338 Self::Date => "date",
339 Self::Uuid => "uuid",
340 Self::Interval => "interval",
341 Self::Inet => "inet",
342 Self::Cidr => "cidr",
343 Self::MacAddr => "macaddr",
344 Self::MacAddr8 => "macaddr8",
345 Self::Vector => "vector",
346 Self::HalfVec => "halfvec",
347 Self::SparseVec => "sparsevec",
348 Self::Bit => "bit",
349 Self::Point => "point",
350 Self::Line => "line",
351 Self::Geometry => "geometry",
352 Self::Serial => "serial",
353 Self::SmallSerial => "smallserial",
354 Self::BigSerial => "bigserial",
355 Self::Enum => "pgEnum",
356 Self::Custom => "customType",
357 }
358 }
359
360 #[must_use]
362 pub const fn is_serial(&self) -> bool {
363 matches!(self, Self::Serial | Self::SmallSerial | Self::BigSerial)
364 }
365}
366
367#[must_use]
373pub fn parse_type_params(sql_type: &str) -> Option<(String, Option<String>)> {
374 let start = sql_type.find('(')?;
375 let end = sql_type.find(')')?;
376 let params = &sql_type[start + 1..end];
377
378 let parts: Vec<&str> = params.split(',').map(str::trim).collect();
379 match parts.len() {
380 1 => Some((parts[0].to_string(), None)),
381 2 => Some((parts[0].to_string(), Some(parts[1].to_string()))),
382 _ => None,
383 }
384}
385
386#[must_use]
388pub fn is_serial_expression(expr: &str, schema: &str) -> bool {
389 let schema_prefix = if schema == "public" {
390 String::new()
391 } else {
392 format!("{schema}.")
393 };
394
395 (expr.starts_with(&format!("nextval('{schema_prefix}"))
396 || expr.starts_with(&format!("nextval('\"{schema_prefix}")))
397 && (expr.ends_with("_seq'::regclass)") || expr.ends_with("_seq\"'::regclass)"))
398}
399
400#[must_use]
407pub fn extract_nextval_sequence(expr: &str) -> Option<String> {
408 let inner = expr
409 .strip_prefix("nextval('")?
410 .strip_suffix("'::regclass)")?;
411 let name_part = inner.rfind('.').map_or(inner, |pos| &inner[pos + 1..]);
412 let name = name_part.trim_matches('"');
413 if name.is_empty() {
414 return None;
415 }
416 Some(name.to_string())
417}
418
419pub struct IdentityDefaults;
425
426impl IdentityDefaults {
427 pub const START_WITH: &'static str = "1";
428 pub const INCREMENT: &'static str = "1";
429 pub const MIN: &'static str = "1";
430 pub const CACHE: i32 = 1;
431 pub const CYCLE: bool = false;
432
433 #[must_use]
437 pub fn max_for(column_type: &str) -> &'static str {
438 match column_type {
439 "smallint" => "32767",
440 "bigint" => "9223372036854775807",
441 _ => "2147483647",
443 }
444 }
445
446 #[must_use]
450 pub fn min_for(column_type: &str) -> &'static str {
451 match column_type {
452 "smallint" => "-32768",
453 "bigint" => "-9223372036854775808",
454 _ => "-2147483648",
456 }
457 }
458}
459
460pub const SYSTEM_NAMESPACE_NAMES: &[&str] = &["pg_toast", "pg_catalog", "information_schema"];
466
467#[must_use]
469pub fn is_system_namespace(name: &str) -> bool {
470 name.starts_with("pg_toast")
471 || name == "pg_default"
472 || name == "pg_global"
473 || name.starts_with("pg_temp_")
474 || SYSTEM_NAMESPACE_NAMES.contains(&name)
475}
476
477#[must_use]
479pub fn is_system_role(name: &str) -> bool {
480 name == "postgres" || name.starts_with("pg_")
481}
482
483pub struct PgDefaults;
489
490impl PgDefaults {
491 pub const TABLESPACE: &'static str = "pg_default";
493
494 pub const ACCESS_METHOD: &'static str = "heap";
496
497 pub const NULLS_NOT_DISTINCT: bool = false;
499
500 pub const INDEX_METHOD: &'static str = "btree";
502
503 pub const GEOMETRY_SRID: i32 = 0;
505}
506
507pub const VECTOR_OPS: &[&str] = &[
509 "vector_l2_ops",
510 "vector_ip_ops",
511 "vector_cosine_ops",
512 "vector_l1_ops",
513 "bit_hamming_ops",
514 "bit_jaccard_ops",
515 "halfvec_l2_ops",
516 "sparsevec_l2_ops",
517];
518
519#[must_use]
528pub fn parse_check_definition(value: &str) -> String {
529 let trimmed = value.trim();
530 let rest = trimmed
531 .strip_prefix("CHECK")
532 .or_else(|| trimmed.strip_prefix("check"))
533 .map_or(trimmed, str::trim_start);
534 rest.to_string()
535}
536
537#[must_use]
541pub fn parse_view_definition(value: &str) -> String {
542 value
543 .split_whitespace()
544 .collect::<Vec<_>>()
545 .join(" ")
546 .trim_end_matches(';')
547 .to_string()
548}
549
550#[cfg(test)]
551mod tests {
552 use super::*;
553
554 #[test]
555 fn test_default_name_for_pk() {
556 assert_eq!(default_name_for_pk("users"), "users_pkey");
557 }
558
559 #[test]
560 fn test_default_name_for_fk() {
561 let name = default_name_for_fk(
562 "posts",
563 &["author_id".to_string()],
564 "users",
565 &["id".to_string()],
566 );
567 assert_eq!(name, "posts_author_id_fkey");
568 }
569
570 #[test]
571 fn test_default_name_for_composite_fk_uses_first_column() {
572 let name = default_name_for_fk(
573 "order_lines",
574 &["order_id".to_string(), "tenant_id".to_string()],
575 "orders",
576 &["id".to_string(), "tenant_id".to_string()],
577 );
578 assert_eq!(name, "order_lines_order_id_fkey");
579 }
580
581 #[test]
582 fn test_default_name_for_unique() {
583 let name = default_name_for_unique("users", &["email".to_string()]);
584 assert_eq!(name, "users_email_key");
585 }
586
587 #[test]
588 fn test_default_name_for_index() {
589 let name = default_name_for_index("users", &["email".to_string(), "name".to_string()]);
590 assert_eq!(name, "users_email_name_idx");
591 }
592
593 #[test]
594 fn test_parse_type_params() {
595 assert_eq!(
596 parse_type_params("varchar(255)"),
597 Some(("255".to_string(), None))
598 );
599 assert_eq!(
600 parse_type_params("numeric(10,2)"),
601 Some(("10".to_string(), Some("2".to_string())))
602 );
603 assert_eq!(parse_type_params("text"), None);
604 }
605
606 #[test]
607 fn test_is_system_namespace() {
608 assert!(is_system_namespace("pg_catalog"));
609 assert!(is_system_namespace("pg_toast_12345"));
610 assert!(!is_system_namespace("public"));
611 assert!(!is_system_namespace("myschema"));
612 }
613
614 #[test]
615 fn test_identity_defaults() {
616 assert_eq!(IdentityDefaults::max_for("smallint"), "32767");
617 assert_eq!(IdentityDefaults::max_for("integer"), "2147483647");
618 assert_eq!(IdentityDefaults::max_for("bigint"), "9223372036854775807");
619 }
620
621 #[test]
622 fn test_from_sql_type_serial_vs_integer() {
623 assert_eq!(
625 PgTypeCategory::from_sql_type("integer"),
626 PgTypeCategory::Integer
627 );
628 assert_eq!(
629 PgTypeCategory::from_sql_type("int"),
630 PgTypeCategory::Integer
631 );
632 assert_eq!(
633 PgTypeCategory::from_sql_type("int4"),
634 PgTypeCategory::Integer
635 );
636 assert_eq!(
637 PgTypeCategory::from_sql_type("bigint"),
638 PgTypeCategory::BigInt
639 );
640 assert_eq!(
641 PgTypeCategory::from_sql_type("int8"),
642 PgTypeCategory::BigInt
643 );
644 assert_eq!(
645 PgTypeCategory::from_sql_type("smallint"),
646 PgTypeCategory::SmallInt
647 );
648 assert_eq!(
649 PgTypeCategory::from_sql_type("int2"),
650 PgTypeCategory::SmallInt
651 );
652
653 assert_eq!(
655 PgTypeCategory::from_sql_type("serial"),
656 PgTypeCategory::Serial
657 );
658 assert_eq!(
659 PgTypeCategory::from_sql_type("SERIAL"),
660 PgTypeCategory::Serial
661 );
662 assert_eq!(
663 PgTypeCategory::from_sql_type("bigserial"),
664 PgTypeCategory::BigSerial
665 );
666 assert_eq!(
667 PgTypeCategory::from_sql_type("smallserial"),
668 PgTypeCategory::SmallSerial
669 );
670
671 assert!(PgTypeCategory::Serial.is_serial());
672 assert!(PgTypeCategory::BigSerial.is_serial());
673 assert!(PgTypeCategory::SmallSerial.is_serial());
674 assert!(!PgTypeCategory::Integer.is_serial());
675 assert!(!PgTypeCategory::BigInt.is_serial());
676 }
677
678 #[test]
679 fn test_from_sql_type_common() {
680 assert_eq!(PgTypeCategory::from_sql_type("text"), PgTypeCategory::Text);
681 assert_eq!(
682 PgTypeCategory::from_sql_type("varchar(255)"),
683 PgTypeCategory::Varchar
684 );
685 assert_eq!(
686 PgTypeCategory::from_sql_type("boolean"),
687 PgTypeCategory::Boolean
688 );
689 assert_eq!(
690 PgTypeCategory::from_sql_type("bool"),
691 PgTypeCategory::Boolean
692 );
693 assert_eq!(PgTypeCategory::from_sql_type("uuid"), PgTypeCategory::Uuid);
694 assert_eq!(
695 PgTypeCategory::from_sql_type("jsonb"),
696 PgTypeCategory::Jsonb
697 );
698 assert_eq!(PgTypeCategory::from_sql_type("json"), PgTypeCategory::Json);
699 assert_eq!(
700 PgTypeCategory::from_sql_type("timestamp with time zone"),
701 PgTypeCategory::TimestampTz
702 );
703 assert_eq!(
704 PgTypeCategory::from_sql_type("timestamp without time zone"),
705 PgTypeCategory::Timestamp
706 );
707 assert_eq!(
708 PgTypeCategory::from_sql_type("timestamp"),
709 PgTypeCategory::Timestamp
710 );
711 assert_eq!(
712 PgTypeCategory::from_sql_type("time without time zone"),
713 PgTypeCategory::Time
714 );
715 assert_eq!(PgTypeCategory::from_sql_type("date"), PgTypeCategory::Date);
716 assert_eq!(
717 PgTypeCategory::from_sql_type("numeric(10,2)"),
718 PgTypeCategory::Numeric
719 );
720 assert_eq!(PgTypeCategory::from_sql_type("real"), PgTypeCategory::Real);
721 assert_eq!(
722 PgTypeCategory::from_sql_type("double precision"),
723 PgTypeCategory::DoublePrecision
724 );
725 assert_eq!(
726 PgTypeCategory::from_sql_type("macaddr8"),
727 PgTypeCategory::MacAddr8
728 );
729 assert_eq!(
730 PgTypeCategory::from_sql_type("macaddr"),
731 PgTypeCategory::MacAddr
732 );
733 }
734
735 #[test]
736 fn test_from_sql_type_postgis_surface() {
737 assert_eq!(
738 PgTypeCategory::from_sql_type("geometry(point)"),
739 PgTypeCategory::Geometry
740 );
741 assert_eq!(
742 PgTypeCategory::from_sql_type("geometry(point, 4326)"),
743 PgTypeCategory::Geometry
744 );
745 assert_eq!(
746 PgTypeCategory::from_sql_type("geometry(polygon, 4326)"),
747 PgTypeCategory::Custom
748 );
749 assert_eq!(
750 PgTypeCategory::from_sql_type("geography(point)"),
751 PgTypeCategory::Custom
752 );
753 assert_eq!(
754 PgTypeCategory::from_sql_type("box2d"),
755 PgTypeCategory::Custom
756 );
757 assert_eq!(
758 PgTypeCategory::from_sql_type("box3d"),
759 PgTypeCategory::Custom
760 );
761 assert_eq!(
762 PgTypeCategory::from_sql_type("raster"),
763 PgTypeCategory::Custom
764 );
765 }
766
767 #[test]
768 fn test_extract_nextval_sequence() {
769 assert_eq!(
770 extract_nextval_sequence("nextval('users_id_seq'::regclass)"),
771 Some("users_id_seq".to_string())
772 );
773 assert_eq!(
774 extract_nextval_sequence("nextval('public.users_id_seq'::regclass)"),
775 Some("users_id_seq".to_string())
776 );
777 assert_eq!(
778 extract_nextval_sequence("nextval('\"myschema\".\"users_id_seq\"'::regclass)"),
779 Some("users_id_seq".to_string())
780 );
781 assert_eq!(extract_nextval_sequence("not_a_nextval"), None);
782 assert_eq!(extract_nextval_sequence("nextval(''::regclass)"), None);
783 }
784}