Skip to main content

drizzle_migrations/postgres/
grammar.rs

1//! `PostgreSQL` SQL type grammar and naming conventions
2//!
3//! This module provides type checking, naming conventions, and default value
4//! handling for `PostgreSQL` columns matching drizzle-kit grammar.ts
5
6// =============================================================================
7// Naming Conventions
8// =============================================================================
9
10/// Generate default name for a primary key constraint
11#[must_use]
12pub fn default_name_for_pk(table: &str) -> String {
13    format!("{table}_pkey")
14}
15
16/// Generate default name for a foreign key constraint
17#[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    // PostgreSQL identifier max length is 63
28    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/// Generate default name for a unique constraint
41#[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/// Generate default name for an index
47#[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/// Generate default name for an identity sequence
53#[must_use]
54pub fn default_name_for_identity_sequence(table: &str, column: &str) -> String {
55    format!("{table}_{column}_seq")
56}
57
58/// Generate default name for a check constraint.
59///
60/// Naming convention (matches the `#[PostgresTable]` macro): table-level
61/// checks are numbered 1-based — `{table}_check1`, `{table}_check2`, ...
62/// (the macro collapses a *single* table-level check to `{table}_check`;
63/// callers with that context should special-case it). Column-level checks
64/// use `{table}_{column}_check` and are not produced by this helper.
65#[must_use]
66pub fn default_name_for_check(table: &str, index: usize) -> String {
67    format!("{table}_check{}", index + 1)
68}
69
70/// Stable hash for constraint/index naming.
71///
72/// Uses SHA-256 (first 12 hex chars) so generated names are identical
73/// across runs, processes, and Rust versions — `DefaultHasher` output is
74/// explicitly not stable and would rename constraints between invocations.
75fn 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
87/// Enforce `PostgreSQL`'s 63-byte identifier limit: names longer than that
88/// are truncated and disambiguated with a stable hash, preserving `suffix`
89/// (e.g. `_key`, `_idx`) so the object kind stays recognizable.
90fn 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    // Reserve room for `_<hash>` + suffix.
98    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// =============================================================================
107// SQL Type Categories
108// =============================================================================
109
110/// `PostgreSQL` SQL type category
111#[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    /// Match serial and integer types. Serial aliases must be checked first so
174    /// `smallserial` isn't misclassified as `smallint`.
175    fn match_numeric(s: &str) -> Option<Self> {
176        // Serial aliases first (prefix collides with integer types).
177        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    /// Match string and JSON types. `varchar`/`character varying` must be
212    /// checked before `char`/`character`; `jsonb` before `json`.
213    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    /// Match time/date types. The `with time zone` variants are checked before
233    /// the base `timestamp` / `time` prefixes.
234    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    /// Match network, vector, bit, geometric and other specialized types.
257    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        // macaddr8 must be matched before macaddr
268        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    /// Determine the type category for a SQL type string
309    #[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    /// Get the drizzle import name for this type
321    #[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    /// Check if this is a serial type
361    #[must_use]
362    pub const fn is_serial(&self) -> bool {
363        matches!(self, Self::Serial | Self::SmallSerial | Self::BigSerial)
364    }
365}
366
367// =============================================================================
368// Type Parsing Utilities
369// =============================================================================
370
371/// Extract parameters from a type like "varchar(255)" or "numeric(10,2)"
372#[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/// Check if a string is a serial expression
387#[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/// Extract the sequence name from a `nextval('...'::regclass)` expression.
401///
402/// Returns just the sequence name (without schema prefix or quotes):
403/// - `nextval('users_id_seq'::regclass)` → `users_id_seq`
404/// - `nextval('public.users_id_seq'::regclass)` → `users_id_seq`
405/// - `nextval('"myschema"."users_id_seq"'::regclass)` → `users_id_seq`
406#[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
419// =============================================================================
420// Identity Defaults
421// =============================================================================
422
423/// Default values for identity columns
424pub 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    /// Get the maximum value for an identity column based on type.
434    ///
435    /// Falls back to the `integer` range for unknown/unspecified types.
436    #[must_use]
437    pub fn max_for(column_type: &str) -> &'static str {
438        match column_type {
439            "smallint" => "32767",
440            "bigint" => "9223372036854775807",
441            // "integer" and fallback share the same range
442            _ => "2147483647",
443        }
444    }
445
446    /// Get the minimum value for an identity column based on type.
447    ///
448    /// Falls back to the `integer` range for unknown/unspecified types.
449    #[must_use]
450    pub fn min_for(column_type: &str) -> &'static str {
451        match column_type {
452            "smallint" => "-32768",
453            "bigint" => "-9223372036854775808",
454            // "integer" and fallback share the same range
455            _ => "-2147483648",
456        }
457    }
458}
459
460// =============================================================================
461// System Checks
462// =============================================================================
463
464/// System namespace names that should be skipped
465pub const SYSTEM_NAMESPACE_NAMES: &[&str] = &["pg_toast", "pg_catalog", "information_schema"];
466
467/// Check if a namespace is a system namespace
468#[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/// Check if a role is a system role
478#[must_use]
479pub fn is_system_role(name: &str) -> bool {
480    name == "postgres" || name.starts_with("pg_")
481}
482
483// =============================================================================
484// Default Values
485// =============================================================================
486
487/// `PostgreSQL` default values and settings
488pub struct PgDefaults;
489
490impl PgDefaults {
491    /// Default tablespace
492    pub const TABLESPACE: &'static str = "pg_default";
493
494    /// Default access method
495    pub const ACCESS_METHOD: &'static str = "heap";
496
497    /// Default nulls not distinct setting
498    pub const NULLS_NOT_DISTINCT: bool = false;
499
500    /// Default index method
501    pub const INDEX_METHOD: &'static str = "btree";
502
503    /// Default geometry SRID
504    pub const GEOMETRY_SRID: i32 = 0;
505}
506
507/// Vector operator classes for indexes
508pub 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// =============================================================================
520// Parsing Helpers
521// =============================================================================
522
523/// Parse a CHECK constraint definition: strip a leading `CHECK` keyword,
524/// leaving the (possibly parenthesized) expression intact. Balanced outer
525/// parentheses are the caller's concern — naive suffix trimming corrupts
526/// expressions like `((a) AND (b))`.
527#[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/// Parse a VIEW definition.
538///
539/// Callers with `Option<&str>` can pair this with [`Option::map`].
540#[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        // These must NOT be classified as serial
624        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        // These must be serial
654        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}