1use super::ddl::{
7 CheckConstraint, Column, Enum, ForeignKey, Index, IndexColumn, Policy, PostgresEntity,
8 PrimaryKey, Role, Schema, Sequence, Table, UniqueConstraint, View,
9};
10use super::grammar::{is_system_namespace, is_system_role};
11use super::snapshot::PostgresSnapshot;
12
13#[derive(Debug, Clone)]
15pub struct IntrospectError {
16 pub message: String,
17 pub table: Option<String>,
18 pub schema: Option<String>,
19}
20
21impl std::fmt::Display for IntrospectError {
22 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 match (&self.schema, &self.table) {
24 (Some(s), Some(t)) => {
25 write!(f, "Introspection error for '{}.{}': {}", s, t, self.message)
26 }
27 (Some(s), None) => write!(f, "Introspection error in schema '{}': {}", s, self.message),
28 (None, Some(t)) => write!(f, "Introspection error for '{}': {}", t, self.message),
29 (None, None) => write!(f, "Introspection error: {}", self.message),
30 }
31 }
32}
33
34impl std::error::Error for IntrospectError {}
35
36pub type IntrospectResult<T> = Result<T, IntrospectError>;
38
39#[derive(Debug, Clone)]
45pub struct RawTableInfo {
46 pub schema: String,
47 pub name: String,
48 pub is_rls_enabled: bool,
49 pub is_unlogged: bool,
50 pub is_temporary: bool,
51 pub tablespace: Option<String>,
52 pub comment: Option<String>,
53}
54
55#[derive(Debug, Clone)]
57pub struct RawColumnInfo {
58 pub schema: String,
59 pub table: String,
60 pub name: String,
61 pub column_type: String,
62 pub type_schema: Option<String>,
63 pub not_null: bool,
64 pub default_value: Option<String>,
65 pub is_identity: bool,
66 pub identity_type: Option<String>,
67 pub is_generated: bool,
68 pub generated_expression: Option<String>,
69 pub generated_stored: bool,
70 pub dimensions: Option<i32>,
71 pub comment: Option<String>,
72 pub ordinal_position: i32,
73}
74
75#[derive(Debug, Clone)]
77pub struct RawEnumInfo {
78 pub schema: String,
79 pub name: String,
80 pub values: Vec<String>,
81}
82
83#[derive(Debug, Clone)]
88pub struct RawSequenceInfo {
89 pub schema: String,
90 pub name: String,
91 pub data_type: Option<String>,
92 pub start_value: Option<String>,
93 pub min_value: Option<String>,
94 pub max_value: Option<String>,
95 pub increment: Option<String>,
96 pub cycle: Option<bool>,
97 pub cache_value: Option<String>,
98 pub owned_by: Option<String>,
102}
103
104#[derive(Debug, Clone)]
106pub struct RawIndexInfo {
107 pub schema: String,
108 pub table: String,
109 pub name: String,
110 pub is_unique: bool,
111 pub is_primary: bool,
112 pub method: String,
113 pub columns: Vec<RawIndexColumnInfo>,
114 pub where_clause: Option<String>,
115 pub concurrent: bool,
116}
117
118#[derive(Debug, Clone)]
120pub struct RawIndexColumnInfo {
121 pub name: String,
122 pub is_expression: bool,
123 pub asc: bool,
124 pub nulls_first: bool,
125 pub opclass: Option<String>,
126}
127
128#[derive(Debug, Clone)]
130pub struct RawForeignKeyInfo {
131 pub schema: String,
132 pub table: String,
133 pub name: String,
134 pub columns: Vec<String>,
135 pub schema_to: String,
136 pub table_to: String,
137 pub columns_to: Vec<String>,
138 pub on_update: String,
139 pub on_delete: String,
140 pub deferrable: bool,
141 pub initially_deferred: bool,
142}
143
144#[derive(Debug, Clone)]
146pub struct RawPrimaryKeyInfo {
147 pub schema: String,
148 pub table: String,
149 pub name: String,
150 pub columns: Vec<String>,
151}
152
153#[derive(Debug, Clone)]
155pub struct RawUniqueInfo {
156 pub schema: String,
157 pub table: String,
158 pub name: String,
159 pub columns: Vec<String>,
160 pub nulls_not_distinct: bool,
161 pub deferrable: bool,
162 pub initially_deferred: bool,
163}
164
165#[derive(Debug, Clone)]
167pub struct RawCheckInfo {
168 pub schema: String,
169 pub table: String,
170 pub name: String,
171 pub expression: String,
172}
173
174#[derive(Debug, Clone)]
176pub struct RawViewInfo {
177 pub schema: String,
178 pub name: String,
179 pub definition: String,
180 pub is_materialized: bool,
181}
182
183#[derive(Debug, Clone)]
185pub struct RawPolicyInfo {
186 pub schema: String,
187 pub table: String,
188 pub name: String,
189 pub as_clause: String,
190 pub for_clause: String,
191 pub to: Vec<String>,
192 pub using: Option<String>,
193 pub with_check: Option<String>,
194}
195
196#[derive(Debug, Clone)]
198pub struct RawRoleInfo {
199 pub name: String,
200 pub create_db: bool,
201 pub create_role: bool,
202 pub inherit: bool,
203}
204
205#[derive(Debug, Clone, Default)]
207pub struct RawIntrospection {
208 pub schemas: Vec<Schema>,
209 pub tables: Vec<RawTableInfo>,
210 pub columns: Vec<RawColumnInfo>,
211 pub enums: Vec<RawEnumInfo>,
212 pub sequences: Vec<RawSequenceInfo>,
213 pub views: Vec<RawViewInfo>,
214 pub indexes: Vec<RawIndexInfo>,
215 pub foreign_keys: Vec<RawForeignKeyInfo>,
216 pub primary_keys: Vec<RawPrimaryKeyInfo>,
217 pub unique_constraints: Vec<RawUniqueInfo>,
218 pub check_constraints: Vec<RawCheckInfo>,
219 pub roles: Vec<RawRoleInfo>,
220 pub policies: Vec<RawPolicyInfo>,
221}
222
223#[must_use]
225pub fn assemble_ddl(raw: RawIntrospection) -> super::PostgresDDL {
226 let mut ddl = super::PostgresDDL::new();
227 for schema in raw.schemas {
228 ddl.schemas.push(schema);
229 }
230 for value in process_enums(&raw.enums) {
231 ddl.enums.push(value);
232 }
233 for sequence in process_sequences(&raw.sequences) {
234 ddl.sequences.push(sequence);
235 }
236 for role in process_roles(&raw.roles) {
237 ddl.roles.push(role);
238 }
239 for policy in process_policies(&raw.policies) {
240 ddl.policies.push(policy);
241 }
242 for table in process_tables(&raw.tables) {
243 ddl.tables.push(table);
244 }
245 for column in process_columns(&raw.columns) {
246 ddl.columns.push(column);
247 }
248 for index in process_indexes(&raw.indexes) {
249 ddl.indexes.push(index);
250 }
251 for foreign_key in process_foreign_keys(&raw.foreign_keys) {
252 ddl.fks.push(foreign_key);
253 }
254 for primary_key in process_primary_keys(&raw.primary_keys) {
255 ddl.pks.push(primary_key);
256 }
257 for unique in process_unique_constraints(&raw.unique_constraints) {
258 ddl.uniques.push(unique);
259 }
260 for check in process_check_constraints(&raw.check_constraints) {
261 ddl.checks.push(check);
262 }
263 for view in process_views(&raw.views) {
264 ddl.views.push(view);
265 }
266 ddl
267}
268
269#[derive(Debug, Clone, Default)]
275pub struct IntrospectionResult {
276 pub schemas: Vec<Schema>,
277 pub enums: Vec<Enum>,
278 pub sequences: Vec<Sequence>,
279 pub roles: Vec<Role>,
280 pub tables: Vec<Table>,
281 pub columns: Vec<Column>,
282 pub indexes: Vec<Index>,
283 pub foreign_keys: Vec<ForeignKey>,
284 pub primary_keys: Vec<PrimaryKey>,
285 pub unique_constraints: Vec<UniqueConstraint>,
286 pub check_constraints: Vec<CheckConstraint>,
287 pub views: Vec<View>,
288 pub policies: Vec<Policy>,
289 pub errors: Vec<IntrospectError>,
290}
291
292impl IntrospectionResult {
293 #[must_use]
295 pub fn to_snapshot(&self) -> PostgresSnapshot {
296 let mut snapshot = PostgresSnapshot::new();
297
298 for schema in &self.schemas {
299 snapshot.add_entity(PostgresEntity::Schema(schema.clone()));
300 }
301 for e in &self.enums {
302 snapshot.add_entity(PostgresEntity::Enum(e.clone()));
303 }
304 for seq in &self.sequences {
305 snapshot.add_entity(PostgresEntity::Sequence(seq.clone()));
309 }
310 for role in &self.roles {
311 snapshot.add_entity(PostgresEntity::Role(role.clone()));
312 }
313 for table in &self.tables {
314 snapshot.add_entity(PostgresEntity::Table(table.clone()));
315 }
316 for column in &self.columns {
317 snapshot.add_entity(PostgresEntity::Column(column.clone()));
318 }
319 for index in &self.indexes {
320 snapshot.add_entity(PostgresEntity::Index(index.clone()));
321 }
322 for fk in &self.foreign_keys {
323 snapshot.add_entity(PostgresEntity::ForeignKey(fk.clone()));
324 }
325 for pk in &self.primary_keys {
326 snapshot.add_entity(PostgresEntity::PrimaryKey(pk.clone()));
327 }
328 for unique in &self.unique_constraints {
329 snapshot.add_entity(PostgresEntity::UniqueConstraint(unique.clone()));
330 }
331 for check in &self.check_constraints {
332 snapshot.add_entity(PostgresEntity::CheckConstraint(check.clone()));
333 }
334 for view in &self.views {
335 snapshot.add_entity(PostgresEntity::View(view.clone()));
336 }
337 for policy in &self.policies {
338 snapshot.add_entity(PostgresEntity::Policy(policy.clone()));
339 }
340
341 snapshot
342 }
343
344 #[must_use]
346 pub const fn has_errors(&self) -> bool {
347 !self.errors.is_empty()
348 }
349
350 #[must_use]
352 pub fn to_entities(&self) -> Vec<PostgresEntity> {
353 let mut entities = Vec::new();
354
355 for s in &self.schemas {
356 entities.push(PostgresEntity::Schema(s.clone()));
357 }
358 for e in &self.enums {
359 entities.push(PostgresEntity::Enum(e.clone()));
360 }
361 for s in &self.sequences {
362 entities.push(PostgresEntity::Sequence(s.clone()));
363 }
364 for r in &self.roles {
365 entities.push(PostgresEntity::Role(r.clone()));
366 }
367 for t in &self.tables {
368 entities.push(PostgresEntity::Table(t.clone()));
369 }
370 for c in &self.columns {
371 entities.push(PostgresEntity::Column(c.clone()));
372 }
373 for i in &self.indexes {
374 entities.push(PostgresEntity::Index(i.clone()));
375 }
376 for f in &self.foreign_keys {
377 entities.push(PostgresEntity::ForeignKey(f.clone()));
378 }
379 for p in &self.primary_keys {
380 entities.push(PostgresEntity::PrimaryKey(p.clone()));
381 }
382 for u in &self.unique_constraints {
383 entities.push(PostgresEntity::UniqueConstraint(u.clone()));
384 }
385 for c in &self.check_constraints {
386 entities.push(PostgresEntity::CheckConstraint(c.clone()));
387 }
388 for v in &self.views {
389 entities.push(PostgresEntity::View(v.clone()));
390 }
391 for p in &self.policies {
392 entities.push(PostgresEntity::Policy(p.clone()));
393 }
394
395 entities
396 }
397}
398
399#[must_use]
405pub fn process_tables(raw_tables: &[RawTableInfo]) -> Vec<Table> {
406 raw_tables
407 .iter()
408 .filter(|t| !is_system_namespace(&t.schema))
409 .map(|t| Table {
410 schema: t.schema.clone().into(),
411 name: t.name.clone().into(),
412 is_unlogged: if t.is_unlogged { Some(true) } else { None },
413 is_temporary: if t.is_temporary { Some(true) } else { None },
414 inherits: None,
415 tablespace: t.tablespace.clone().map(Into::into),
416 is_rls_enabled: Some(t.is_rls_enabled),
417 comment: t.comment.clone().map(Into::into),
418 })
419 .collect()
420}
421
422#[derive(Debug, Clone, Default)]
424struct IdentityOptions {
425 start: Option<String>,
426 increment: Option<String>,
427 min: Option<String>,
428 max: Option<String>,
429 cycle: Option<bool>,
430}
431
432fn parse_identity_type(raw: &str) -> (String, IdentityOptions) {
436 let trimmed = raw.trim();
437 if trimmed.starts_with('{')
438 && let Ok(value) = serde_json::from_str::<serde_json::Value>(trimmed)
439 {
440 let get = |key: &str| {
441 value
442 .get(key)
443 .and_then(serde_json::Value::as_str)
444 .map(ToString::to_string)
445 };
446 let type_str = get("type").unwrap_or_else(|| "ALWAYS".to_string());
447 let options = IdentityOptions {
448 start: get("start"),
449 increment: get("increment"),
450 min: get("min"),
451 max: get("max"),
452 cycle: value.get("cycle").and_then(serde_json::Value::as_bool),
453 };
454 return (type_str, options);
455 }
456 (trimmed.to_string(), IdentityOptions::default())
457}
458
459#[must_use]
461pub fn process_columns(raw_columns: &[RawColumnInfo]) -> Vec<Column> {
462 use super::ddl::{GeneratedType, IdentityType};
463
464 raw_columns
465 .iter()
466 .filter(|c| !is_system_namespace(&c.schema))
467 .map(|c| {
468 let generated = if c.is_generated {
469 c.generated_expression
470 .as_ref()
471 .map(|expr| super::ddl::Generated {
472 expression: expr.clone().into(),
473 gen_type: if c.generated_stored {
474 GeneratedType::Stored
475 } else {
476 GeneratedType::Virtual
477 },
478 })
479 } else {
480 None
481 };
482
483 let identity = if c.is_identity {
484 c.identity_type.as_ref().map(|t| {
485 let (type_str, options) = parse_identity_type(t);
490 let identity_type = if type_str.eq_ignore_ascii_case("always") {
491 IdentityType::Always
492 } else {
493 IdentityType::ByDefault
494 };
495 super::ddl::Identity {
496 name: format!("{}_{}_seq", c.table, c.name).into(),
497 schema: Some(c.schema.clone().into()),
498 type_: identity_type,
499 increment: options.increment.map(Into::into),
500 min_value: options.min.map(Into::into),
501 max_value: options.max.map(Into::into),
502 start_with: options.start.map(Into::into),
503 cache: None,
504 cycle: options.cycle,
505 }
506 })
507 } else {
508 None
509 };
510
511 let dimensions = c.dimensions.filter(|dims| *dims > 0).or_else(|| {
512 if c.column_type.starts_with('_') {
513 Some(1)
514 } else {
515 None
516 }
517 });
518 let column_type = if dimensions.is_some() {
519 c.column_type
520 .strip_prefix('_')
521 .unwrap_or(&c.column_type)
522 .to_string()
523 } else {
524 c.column_type.clone()
525 };
526
527 Column {
528 schema: c.schema.clone().into(),
529 table: c.table.clone().into(),
530 name: c.name.clone().into(),
531 sql_type: column_type.into(),
532 type_schema: c.type_schema.clone().map(std::convert::Into::into),
533 not_null: c.not_null,
534 default: c.default_value.clone().map(std::convert::Into::into),
535 generated,
536 identity,
537 dimensions,
538 comment: c.comment.clone().map(std::convert::Into::into),
539 collate: None,
544 ordinal_position: Some(c.ordinal_position),
545 }
546 })
547 .collect()
548}
549
550#[must_use]
552pub fn process_enums(raw_enums: &[RawEnumInfo]) -> Vec<Enum> {
553 raw_enums
554 .iter()
555 .filter(|e| !is_system_namespace(&e.schema))
556 .map(|e| Enum {
557 schema: e.schema.clone().into(),
558 name: e.name.clone().into(),
559 values: e.values.iter().map(|v| v.clone().into()).collect(),
560 })
561 .collect()
562}
563
564#[must_use]
571pub fn process_sequences(raw_sequences: &[RawSequenceInfo]) -> Vec<Sequence> {
572 raw_sequences
573 .iter()
574 .filter(|s| !is_system_namespace(&s.schema) && s.owned_by.is_none())
575 .map(|s| Sequence {
576 schema: s.schema.clone().into(),
577 name: s.name.clone().into(),
578 increment_by: s.increment.clone().map(Into::into),
579 min_value: s.min_value.clone().map(Into::into),
580 max_value: s.max_value.clone().map(Into::into),
581 start_with: s.start_value.clone().map(Into::into),
582 cache_size: s.cache_value.as_deref().and_then(|v| v.parse().ok()),
583 cycle: s.cycle,
584 })
585 .collect()
586}
587
588#[must_use]
590pub fn process_indexes(raw_indexes: &[RawIndexInfo]) -> Vec<Index> {
591 use super::ddl::Opclass;
592
593 raw_indexes
594 .iter()
595 .filter(|i| !is_system_namespace(&i.schema) && !i.is_primary)
596 .map(|i| {
597 let columns: Vec<IndexColumn> = i
598 .columns
599 .iter()
600 .map(|c| IndexColumn {
601 value: c.name.clone().into(),
602 is_expression: c.is_expression,
603 asc: c.asc,
604 nulls_first: c.nulls_first,
605 opclass: c.opclass.clone().map(Opclass::new),
606 })
607 .collect();
608
609 Index {
610 schema: i.schema.clone().into(),
611 table: i.table.clone().into(),
612 name: i.name.clone().into(),
613 name_explicit: true,
614 columns,
615 is_unique: i.is_unique,
616 where_clause: i.where_clause.clone().map(std::convert::Into::into),
617 method: Some(i.method.clone().into()),
618 concurrently: i.concurrent,
619 r#with: None,
620 }
621 })
622 .collect()
623}
624
625#[must_use]
627pub fn process_foreign_keys(raw_fks: &[RawForeignKeyInfo]) -> Vec<ForeignKey> {
628 raw_fks
629 .iter()
630 .filter(|f| !is_system_namespace(&f.schema))
631 .map(|f| ForeignKey {
632 schema: f.schema.clone().into(),
633 table: f.table.clone().into(),
634 name: f.name.clone().into(),
635 name_explicit: true,
636 columns: f.columns.iter().map(|c| c.clone().into()).collect(),
637 schema_to: f.schema_to.clone().into(),
638 table_to: f.table_to.clone().into(),
639 columns_to: f.columns_to.iter().map(|c| c.clone().into()).collect(),
640 on_update: Some(f.on_update.clone().into()),
641 on_delete: Some(f.on_delete.clone().into()),
642 deferrable: f.deferrable,
643 initially_deferred: f.initially_deferred,
644 })
645 .collect()
646}
647
648#[must_use]
650pub fn process_primary_keys(raw_pks: &[RawPrimaryKeyInfo]) -> Vec<PrimaryKey> {
651 raw_pks
652 .iter()
653 .filter(|p| !is_system_namespace(&p.schema))
654 .map(|p| PrimaryKey {
655 schema: p.schema.clone().into(),
656 table: p.table.clone().into(),
657 name: p.name.clone().into(),
658 name_explicit: true,
659 columns: p.columns.iter().map(|c| c.clone().into()).collect(),
660 })
661 .collect()
662}
663
664#[must_use]
666pub fn process_unique_constraints(raw_uniques: &[RawUniqueInfo]) -> Vec<UniqueConstraint> {
667 raw_uniques
668 .iter()
669 .filter(|u| !is_system_namespace(&u.schema))
670 .map(|u| UniqueConstraint {
671 schema: u.schema.clone().into(),
672 table: u.table.clone().into(),
673 name: u.name.clone().into(),
674 name_explicit: true,
675 columns: u.columns.iter().map(|c| c.clone().into()).collect(),
676 nulls_not_distinct: u.nulls_not_distinct,
677 deferrable: u.deferrable,
678 initially_deferred: u.initially_deferred,
679 })
680 .collect()
681}
682
683#[must_use]
685pub fn process_check_constraints(raw_checks: &[RawCheckInfo]) -> Vec<CheckConstraint> {
686 raw_checks
687 .iter()
688 .filter(|c| !is_system_namespace(&c.schema))
689 .map(|c| CheckConstraint {
690 schema: c.schema.clone().into(),
691 table: c.table.clone().into(),
692 name: c.name.clone().into(),
693 value: c.expression.clone().into(),
694 })
695 .collect()
696}
697
698#[must_use]
700pub fn process_views(raw_views: &[RawViewInfo]) -> Vec<View> {
701 raw_views
702 .iter()
703 .filter(|v| !is_system_namespace(&v.schema))
704 .map(|v| View {
705 schema: v.schema.clone().into(),
706 name: v.name.clone().into(),
707 definition: Some(v.definition.clone().into()),
708 materialized: v.is_materialized,
709 r#with: None,
710 is_existing: false,
711 with_no_data: None,
712 using: None,
713 tablespace: None,
714 })
715 .collect()
716}
717
718#[must_use]
720pub fn process_policies(raw_policies: &[RawPolicyInfo]) -> Vec<Policy> {
721 use std::borrow::Cow;
722
723 raw_policies
724 .iter()
725 .filter(|p| !is_system_namespace(&p.schema))
726 .map(|p| {
727 let roles = p.to.iter().cloned().map(Cow::Owned).collect();
728
729 Policy {
730 schema: p.schema.clone().into(),
731 table: p.table.clone().into(),
732 name: p.name.clone().into(),
733 as_clause: Some(p.as_clause.clone().into()),
734 for_clause: Some(p.for_clause.clone().into()),
735 to: Some(roles),
736 using: p.using.clone().map(std::convert::Into::into),
737 with_check: p.with_check.clone().map(std::convert::Into::into),
738 }
739 })
740 .collect()
741}
742
743#[must_use]
745pub fn process_roles(raw_roles: &[RawRoleInfo]) -> Vec<Role> {
746 raw_roles
747 .iter()
748 .filter(|r| !is_system_role(&r.name))
749 .map(|r| Role {
750 name: r.name.clone().into(),
751 superuser: None,
752 create_db: Some(r.create_db),
753 create_role: Some(r.create_role),
754 inherit: Some(r.inherit),
755 can_login: None,
756 replication: None,
757 bypass_rls: None,
758 conn_limit: None,
759 password: None,
760 valid_until: None,
761 })
762 .collect()
763}
764
765pub mod queries {
771 pub const SCHEMAS_QUERY: &str = r"
773 SELECT n.nspname AS name
774 FROM pg_namespace n
775 WHERE n.nspname NOT LIKE 'pg_%'
776 AND n.nspname != 'information_schema'
777 AND has_schema_privilege(current_user, n.oid, 'USAGE')
778 ORDER BY n.nspname
779 ";
780
781 pub const TABLES_QUERY: &str = r"
783 SELECT
784 n.nspname AS schema,
785 c.relname AS name,
786 c.relrowsecurity AS is_rls_enabled,
787 c.relpersistence = 'u' AS is_unlogged,
788 c.relpersistence = 't' AS is_temporary,
789 tsp.spcname AS tablespace,
790 obj_description(c.oid, 'pg_class') AS comment
791 FROM pg_class c
792 JOIN pg_namespace n ON n.oid = c.relnamespace
793 LEFT JOIN pg_tablespace tsp ON tsp.oid = c.reltablespace
794 WHERE c.relkind IN ('r', 'p')
795 AND n.nspname NOT LIKE 'pg_%'
796 AND n.nspname != 'information_schema'
797 AND has_schema_privilege(current_user, n.oid, 'USAGE')
798 AND has_table_privilege(current_user, c.oid, 'SELECT')
799 ORDER BY n.nspname, c.relname
800 ";
801
802 pub const COLUMNS_QUERY: &str = r"
818 SELECT
819 c.table_schema AS schema,
820 c.table_name AS table,
821 c.column_name AS name,
822 c.udt_name || CASE
823 WHEN c.data_type != 'ARRAY' AND c.character_maximum_length IS NOT NULL
824 THEN '(' || c.character_maximum_length || ')'
825 WHEN c.udt_name IN ('numeric', 'decimal')
826 AND c.numeric_precision IS NOT NULL
827 AND c.numeric_scale IS NOT NULL
828 THEN '(' || c.numeric_precision || ',' || c.numeric_scale || ')'
829 ELSE ''
830 END AS column_type,
831 c.udt_schema AS type_schema,
832 c.is_nullable = 'NO' AS not_null,
833 c.column_default AS default_value,
834 c.is_identity = 'YES' AS is_identity,
835 CASE
836 WHEN c.is_identity = 'YES' THEN json_build_object(
837 'type', c.identity_generation,
838 'start', c.identity_start,
839 'increment', c.identity_increment,
840 'min', c.identity_minimum,
841 'max', c.identity_maximum,
842 'cycle', c.identity_cycle = 'YES'
843 )::text
844 ELSE NULL
845 END AS identity_type,
846 c.is_generated = 'ALWAYS' AS is_generated,
847 c.generation_expression AS generated_expression,
848 COALESCE(a.attgenerated = 's', false) AS generated_stored,
849 NULLIF(a.attndims::int4, 0) AS dimensions,
850 col_description(cls.oid, a.attnum) AS comment,
851 c.ordinal_position
852 FROM information_schema.columns c
853 LEFT JOIN pg_namespace n
854 ON n.nspname = c.table_schema
855 LEFT JOIN pg_class cls
856 ON cls.relnamespace = n.oid
857 AND cls.relname = c.table_name
858 LEFT JOIN pg_attribute a
859 ON a.attrelid = cls.oid
860 AND a.attname = c.column_name
861 AND a.attnum > 0
862 AND NOT a.attisdropped
863 WHERE c.table_schema NOT LIKE 'pg_%'
864 AND c.table_schema != 'information_schema'
865 AND n.oid IS NOT NULL
866 AND has_schema_privilege(current_user, n.oid, 'USAGE')
867 UNION ALL
868 -- Materialized-view columns: information_schema.columns excludes
869 -- matviews entirely, so read them straight from pg_attribute.
870 SELECT
871 mn.nspname AS schema,
872 mc.relname AS table,
873 ma.attname AS name,
874 mt.typname || COALESCE(
875 substring(format_type(ma.atttypid, ma.atttypmod) from '\(.*\)'),
876 ''
877 ) AS column_type,
878 mtn.nspname AS type_schema,
879 ma.attnotnull AS not_null,
880 NULL::text AS default_value,
881 FALSE AS is_identity,
882 NULL::text AS identity_type,
883 FALSE AS is_generated,
884 NULL::text AS generated_expression,
885 FALSE AS generated_stored,
886 NULLIF(ma.attndims::int4, 0) AS dimensions,
887 col_description(mc.oid, ma.attnum) AS comment,
888 ma.attnum::int4 AS ordinal_position
889 FROM pg_class mc
890 JOIN pg_namespace mn ON mn.oid = mc.relnamespace
891 JOIN pg_attribute ma ON ma.attrelid = mc.oid
892 JOIN pg_type mt ON mt.oid = ma.atttypid
893 JOIN pg_namespace mtn ON mtn.oid = mt.typnamespace
894 WHERE mc.relkind = 'm'
895 AND ma.attnum > 0
896 AND NOT ma.attisdropped
897 AND mn.nspname NOT LIKE 'pg_%'
898 AND mn.nspname != 'information_schema'
899 AND has_schema_privilege(current_user, mn.oid, 'USAGE')
900 AND has_table_privilege(current_user, mc.oid, 'SELECT')
901 ORDER BY 1, 2, 15
902 ";
903
904 pub const ENUMS_QUERY: &str = r"
906 SELECT
907 n.nspname AS schema,
908 t.typname AS name,
909 array_agg(e.enumlabel ORDER BY e.enumsortorder) AS values
910 FROM pg_type t
911 JOIN pg_enum e ON t.oid = e.enumtypid
912 JOIN pg_namespace n ON n.oid = t.typnamespace
913 WHERE n.nspname NOT LIKE 'pg_%'
914 AND n.nspname != 'information_schema'
915 AND has_schema_privilege(current_user, n.oid, 'USAGE')
916 GROUP BY n.nspname, t.typname
917 ORDER BY n.nspname, t.typname
918 ";
919
920 pub const SEQUENCES_QUERY: &str = r"
931 SELECT
932 n.nspname AS schema,
933 c.relname AS name,
934 format_type(s.seqtypid, NULL)::text AS data_type,
935 s.seqstart::text AS start_value,
936 s.seqmin::text AS min_value,
937 s.seqmax::text AS max_value,
938 s.seqincrement::text AS increment,
939 s.seqcycle AS cycle,
940 s.seqcache::text AS cache_value,
941 -- Owning column's schema.table when the sequence is auto-owned by
942 -- a serial (deptype 'a') or identity (deptype 'i') column; NULL
943 -- for standalone, hand-managed sequences.
944 (
945 SELECT format('%s.%s', dn.nspname, dc.relname)
946 FROM pg_depend d
947 JOIN pg_class dc ON dc.oid = d.refobjid
948 JOIN pg_namespace dn ON dn.oid = dc.relnamespace
949 WHERE d.objid = s.seqrelid
950 AND d.classid = 'pg_class'::regclass
951 AND d.refclassid = 'pg_class'::regclass
952 AND d.refobjsubid > 0
953 AND d.deptype IN ('a', 'i')
954 LIMIT 1
955 )::text AS owned_by
956 FROM pg_sequence s
957 JOIN pg_class c ON c.oid = s.seqrelid
958 JOIN pg_namespace n ON n.oid = c.relnamespace
959 WHERE n.nspname NOT LIKE 'pg_%'
960 AND n.nspname != 'information_schema'
961 AND has_schema_privilege(current_user, n.oid, 'USAGE')
962 AND (
963 -- Reference s.seqrelid (not c.oid) so this qual only depends on
964 -- pg_sequence: PostgreSQL 18's planner can push it down to the
965 -- pg_class scan before the join filters to sequences, and
966 -- has_sequence_privilege errors on non-sequence relations.
967 has_sequence_privilege(current_user, s.seqrelid, 'USAGE')
968 OR has_sequence_privilege(current_user, s.seqrelid, 'SELECT')
969 )
970 ORDER BY n.nspname, c.relname
971 ";
972
973 pub const VIEWS_QUERY: &str = r"
978 SELECT
979 n.nspname AS schema,
980 c.relname AS name,
981 pg_get_viewdef(c.oid) AS definition,
982 FALSE AS is_materialized
983 FROM pg_class c
984 JOIN pg_namespace n ON n.oid = c.relnamespace
985 WHERE (
986 ($1::text[] IS NOT NULL AND n.nspname = ANY($1::text[]))
987 OR ($1::text[] IS NULL AND n.nspname NOT LIKE 'pg_%'
988 AND n.nspname != 'information_schema')
989 )
990 AND c.relkind = 'v'
991 AND has_schema_privilege(current_user, n.oid, 'USAGE')
992 AND has_table_privilege(current_user, c.oid, 'SELECT')
993 AND pg_get_viewdef(c.oid) IS NOT NULL
994 UNION ALL
995 SELECT
996 n.nspname AS schema,
997 c.relname AS name,
998 pg_get_viewdef(c.oid) AS definition,
999 TRUE AS is_materialized
1000 FROM pg_class c
1001 JOIN pg_namespace n ON n.oid = c.relnamespace
1002 WHERE (
1003 ($1::text[] IS NOT NULL AND n.nspname = ANY($1::text[]))
1004 OR ($1::text[] IS NULL AND n.nspname NOT LIKE 'pg_%'
1005 AND n.nspname != 'information_schema')
1006 )
1007 AND c.relkind = 'm'
1008 AND has_schema_privilege(current_user, n.oid, 'USAGE')
1009 AND has_table_privilege(current_user, c.oid, 'SELECT')
1010 AND pg_get_viewdef(c.oid) IS NOT NULL
1011 ORDER BY schema, name
1012 ";
1013
1014 pub const INDEXES_QUERY: &str = r"
1016SELECT
1017 ns.nspname AS schema,
1018 tbl.relname AS table,
1019 idx.relname AS name,
1020 ix.indisunique AS is_unique,
1021 ix.indisprimary AS is_primary,
1022 am.amname AS method,
1023 array_agg(pg_get_indexdef(ix.indexrelid, s.n, true) ORDER BY s.n) AS columns,
1024 pg_get_expr(ix.indpred, ix.indrelid) AS where_clause
1025FROM pg_index ix
1026JOIN pg_class idx ON idx.oid = ix.indexrelid
1027JOIN pg_class tbl ON tbl.oid = ix.indrelid
1028JOIN pg_namespace ns ON ns.oid = tbl.relnamespace
1029JOIN pg_am am ON am.oid = idx.relam
1030JOIN generate_series(1, ix.indnkeyatts) AS s(n) ON TRUE
1031WHERE ns.nspname NOT LIKE 'pg_%'
1032 AND ns.nspname <> 'information_schema'
1033 AND has_schema_privilege(current_user, ns.oid, 'USAGE')
1034 AND has_table_privilege(current_user, tbl.oid, 'SELECT')
1035GROUP BY ns.nspname, tbl.relname, idx.relname, ix.indisunique, ix.indisprimary, am.amname, ix.indpred, ix.indrelid
1036ORDER BY ns.nspname, tbl.relname, idx.relname
1037";
1038
1039 pub const INDEXES_QUERY_FILTERED: &str = r"
1046SELECT
1047 ns.nspname AS schema,
1048 tbl.relname AS table,
1049 idx.relname AS name,
1050 ix.indisunique AS is_unique,
1051 ix.indisprimary AS is_primary,
1052 am.amname AS method,
1053 array_agg(pg_get_indexdef(ix.indexrelid, s.n, true) ORDER BY s.n) AS columns,
1054 pg_get_expr(ix.indpred, ix.indrelid) AS where_clause
1055FROM pg_index ix
1056JOIN pg_class idx ON idx.oid = ix.indexrelid
1057JOIN pg_class tbl ON tbl.oid = ix.indrelid
1058JOIN pg_namespace ns ON ns.oid = tbl.relnamespace
1059JOIN pg_am am ON am.oid = idx.relam
1060JOIN generate_series(1, ix.indnkeyatts) AS s(n) ON TRUE
1061WHERE ns.nspname = ANY($1::text[])
1062 AND has_schema_privilege(current_user, ns.oid, 'USAGE')
1063 AND has_table_privilege(current_user, tbl.oid, 'SELECT')
1064GROUP BY ns.nspname, tbl.relname, idx.relname, ix.indisunique, ix.indisprimary, am.amname, ix.indpred, ix.indrelid
1065ORDER BY ns.nspname, tbl.relname, idx.relname
1066";
1067
1068 pub const FOREIGN_KEYS_QUERY: &str = r"
1070SELECT
1071 ns.nspname AS schema,
1072 tbl.relname AS table,
1073 con.conname AS name,
1074 array_agg(src.attname ORDER BY s.ord) AS columns,
1075 ns_to.nspname AS schema_to,
1076 tbl_to.relname AS table_to,
1077 array_agg(dst.attname ORDER BY s.ord) AS columns_to,
1078 con.confupdtype::text AS on_update,
1079 con.confdeltype::text AS on_delete,
1080 con.condeferrable AS deferrable,
1081 con.condeferred AS initially_deferred
1082FROM pg_constraint con
1083JOIN pg_class tbl ON tbl.oid = con.conrelid
1084JOIN pg_namespace ns ON ns.oid = tbl.relnamespace
1085JOIN pg_class tbl_to ON tbl_to.oid = con.confrelid
1086JOIN pg_namespace ns_to ON ns_to.oid = tbl_to.relnamespace
1087JOIN unnest(con.conkey) WITH ORDINALITY AS s(attnum, ord) ON TRUE
1088JOIN pg_attribute src ON src.attrelid = tbl.oid AND src.attnum = s.attnum
1089JOIN unnest(con.confkey) WITH ORDINALITY AS r(attnum, ord) ON r.ord = s.ord
1090JOIN pg_attribute dst ON dst.attrelid = tbl_to.oid AND dst.attnum = r.attnum
1091WHERE con.contype = 'f'
1092 AND ns.nspname NOT LIKE 'pg_%'
1093 AND ns.nspname <> 'information_schema'
1094 AND has_schema_privilege(current_user, ns.oid, 'USAGE')
1095 AND has_table_privilege(current_user, tbl.oid, 'SELECT')
1096GROUP BY ns.nspname, tbl.relname, con.conname, ns_to.nspname, tbl_to.relname, con.confupdtype, con.confdeltype, con.condeferrable, con.condeferred
1097ORDER BY ns.nspname, tbl.relname, con.conname
1098";
1099
1100 pub const PRIMARY_KEYS_QUERY: &str = r"
1102SELECT
1103 ns.nspname AS schema,
1104 tbl.relname AS table,
1105 con.conname AS name,
1106 array_agg(att.attname ORDER BY s.ord) AS columns
1107FROM pg_constraint con
1108JOIN pg_class tbl ON tbl.oid = con.conrelid
1109JOIN pg_namespace ns ON ns.oid = tbl.relnamespace
1110JOIN unnest(con.conkey) WITH ORDINALITY AS s(attnum, ord) ON TRUE
1111JOIN pg_attribute att ON att.attrelid = tbl.oid AND att.attnum = s.attnum
1112WHERE con.contype = 'p'
1113 AND ns.nspname NOT LIKE 'pg_%'
1114 AND ns.nspname <> 'information_schema'
1115 AND has_schema_privilege(current_user, ns.oid, 'USAGE')
1116 AND has_table_privilege(current_user, tbl.oid, 'SELECT')
1117GROUP BY ns.nspname, tbl.relname, con.conname
1118ORDER BY ns.nspname, tbl.relname, con.conname
1119";
1120
1121 pub const UNIQUES_QUERY: &str = r"
1129SELECT
1130 ns.nspname AS schema,
1131 tbl.relname AS table,
1132 con.conname AS name,
1133 array_agg(att.attname ORDER BY s.ord) AS columns,
1134 COALESCE((
1135 SELECT (to_jsonb(ix) ->> 'indnullsnotdistinct')::bool
1136 FROM pg_index ix
1137 WHERE ix.indexrelid = con.conindid
1138 ), FALSE) AS nulls_not_distinct,
1139 con.condeferrable AS deferrable,
1140 con.condeferred AS initially_deferred
1141FROM pg_constraint con
1142JOIN pg_class tbl ON tbl.oid = con.conrelid
1143JOIN pg_namespace ns ON ns.oid = tbl.relnamespace
1144JOIN unnest(con.conkey) WITH ORDINALITY AS s(attnum, ord) ON TRUE
1145JOIN pg_attribute att ON att.attrelid = tbl.oid AND att.attnum = s.attnum
1146WHERE con.contype = 'u'
1147 AND ns.nspname NOT LIKE 'pg_%'
1148 AND ns.nspname <> 'information_schema'
1149 AND has_schema_privilege(current_user, ns.oid, 'USAGE')
1150 AND has_table_privilege(current_user, tbl.oid, 'SELECT')
1151GROUP BY ns.nspname, tbl.relname, con.conname, con.conindid, con.condeferrable, con.condeferred
1152ORDER BY ns.nspname, tbl.relname, con.conname
1153";
1154
1155 pub const CHECKS_QUERY: &str = r"
1157SELECT
1158 ns.nspname AS schema,
1159 tbl.relname AS table,
1160 con.conname AS name,
1161 pg_get_expr(con.conbin, con.conrelid) AS expression
1162FROM pg_constraint con
1163JOIN pg_class tbl ON tbl.oid = con.conrelid
1164JOIN pg_namespace ns ON ns.oid = tbl.relnamespace
1165WHERE con.contype = 'c'
1166 AND ns.nspname NOT LIKE 'pg_%'
1167 AND ns.nspname <> 'information_schema'
1168 AND has_schema_privilege(current_user, ns.oid, 'USAGE')
1169 AND has_table_privilege(current_user, tbl.oid, 'SELECT')
1170ORDER BY ns.nspname, tbl.relname, con.conname
1171";
1172
1173 pub const CHECKS_QUERY_FILTERED: &str = r"
1179SELECT
1180 ns.nspname AS schema,
1181 tbl.relname AS table,
1182 con.conname AS name,
1183 pg_get_expr(con.conbin, con.conrelid) AS expression
1184FROM pg_constraint con
1185JOIN pg_class tbl ON tbl.oid = con.conrelid
1186JOIN pg_namespace ns ON ns.oid = tbl.relnamespace
1187WHERE con.contype = 'c'
1188 AND ns.nspname = ANY($1::text[])
1189 AND has_schema_privilege(current_user, ns.oid, 'USAGE')
1190 AND has_table_privilege(current_user, tbl.oid, 'SELECT')
1191ORDER BY ns.nspname, tbl.relname, con.conname
1192";
1193
1194 pub const ROLES_QUERY: &str = r"
1196SELECT
1197 rolname AS name,
1198 rolcreatedb AS create_db,
1199 rolcreaterole AS create_role,
1200 rolinherit AS inherit
1201FROM pg_roles
1202ORDER BY rolname
1203";
1204
1205 pub const POLICIES_QUERY: &str = r#"
1207SELECT
1208 n.nspname AS schema,
1209 c.relname AS table,
1210 p.polname AS name,
1211 CASE
1212 WHEN p.polpermissive THEN 'PERMISSIVE'::text
1213 ELSE 'RESTRICTIVE'::text
1214 END AS as_clause,
1215 CASE p.polcmd
1216 WHEN 'r'::"char" THEN 'SELECT'::text
1217 WHEN 'a'::"char" THEN 'INSERT'::text
1218 WHEN 'w'::"char" THEN 'UPDATE'::text
1219 WHEN 'd'::"char" THEN 'DELETE'::text
1220 WHEN '*'::"char" THEN 'ALL'::text
1221 ELSE NULL::text
1222 END AS for_clause,
1223 CASE
1224 WHEN p.polroles = '{0}'::oid[] THEN (string_to_array('public'::text, ''::text))::name[]
1225 ELSE ARRAY(
1226 SELECT pg_authid.rolname
1227 FROM pg_authid
1228 WHERE pg_authid.oid = ANY(p.polroles)
1229 ORDER BY pg_authid.rolname
1230 )
1231 END AS to,
1232 pg_get_expr(p.polqual, p.polrelid) AS using,
1233 pg_get_expr(p.polwithcheck, p.polrelid) AS with_check
1234FROM pg_policy p
1235JOIN pg_class c ON c.oid = p.polrelid
1236JOIN pg_namespace n ON n.oid = c.relnamespace
1237WHERE n.nspname NOT LIKE 'pg_%'
1238 AND n.nspname <> 'information_schema'
1239 AND has_schema_privilege(current_user, n.oid, 'USAGE')
1240 AND has_table_privilege(current_user, c.oid, 'SELECT')
1241ORDER BY n.nspname, c.relname, p.polname
1242"#;
1243}
1244
1245#[must_use]
1253pub fn action_code_to_string(code: &str) -> String {
1254 match code {
1255 "r" => "RESTRICT",
1256 "c" => "CASCADE",
1257 "n" => "SET NULL",
1258 "d" => "SET DEFAULT",
1259 _ => "NO ACTION",
1261 }
1262 .to_string()
1263}
1264
1265fn strip_trailing_directive<'a>(value: &'a str, directive: &str) -> Option<&'a str> {
1268 let value = value.trim_end();
1269 if value.len() <= directive.len() {
1270 return None;
1271 }
1272 let split = value.len() - directive.len();
1273 if !value.is_char_boundary(split) {
1274 return None;
1275 }
1276 let (head, tail) = value.split_at(split);
1277 if tail.eq_ignore_ascii_case(directive) && head.ends_with(char::is_whitespace) {
1278 Some(head.trim_end())
1279 } else {
1280 None
1281 }
1282}
1283
1284fn is_plain_identifier(value: &str) -> bool {
1286 !value.is_empty()
1287 && value
1288 .chars()
1289 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')
1290 && value
1291 .chars()
1292 .next()
1293 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
1294}
1295
1296fn parens_balanced(value: &str) -> bool {
1300 let mut depth = 0_i32;
1301 for ch in value.chars() {
1302 match ch {
1303 '(' => depth += 1,
1304 ')' => {
1305 depth -= 1;
1306 if depth < 0 {
1307 return false;
1308 }
1309 }
1310 _ => {}
1311 }
1312 }
1313 depth == 0
1314}
1315
1316fn unquote_identifier(value: &str) -> Option<String> {
1319 let inner = value.strip_prefix('"')?.strip_suffix('"')?;
1320 Some(inner.replace("\"\"", "\""))
1321}
1322
1323#[must_use]
1329pub fn parse_index_columns(cols: Vec<String>) -> Vec<RawIndexColumnInfo> {
1330 cols.into_iter()
1331 .map(|c| {
1332 let mut core = c.trim().to_string();
1333
1334 let mut nulls_first: Option<bool> = None;
1338 if let Some(rest) = strip_trailing_directive(&core, "NULLS FIRST") {
1339 nulls_first = Some(true);
1340 core = rest.to_string();
1341 } else if let Some(rest) = strip_trailing_directive(&core, "NULLS LAST") {
1342 nulls_first = Some(false);
1343 core = rest.to_string();
1344 }
1345 let asc = if let Some(rest) = strip_trailing_directive(&core, "DESC") {
1346 core = rest.to_string();
1347 false
1348 } else {
1349 if let Some(rest) = strip_trailing_directive(&core, "ASC") {
1350 core = rest.to_string();
1351 }
1352 true
1353 };
1354 let nulls_first = nulls_first.unwrap_or(!asc);
1357
1358 let mut opclass: Option<String> = None;
1363 if let Some(idx) = core.rfind(char::is_whitespace) {
1364 let candidate = core[idx..].trim();
1365 let rest = core[..idx].trim_end();
1366 if is_plain_identifier(candidate)
1367 && (candidate.ends_with("_ops")
1368 || super::grammar::VECTOR_OPS.contains(&candidate))
1369 && !rest.is_empty()
1370 && parens_balanced(rest)
1371 {
1372 opclass = Some(candidate.to_string());
1373 core = rest.to_string();
1374 }
1375 }
1376
1377 let (name, is_expression) = if let Some(unquoted) = unquote_identifier(&core) {
1380 (unquoted, false)
1381 } else if is_plain_identifier(&core) {
1382 (core, false)
1383 } else {
1384 (core, true)
1385 };
1386
1387 RawIndexColumnInfo {
1388 name,
1389 is_expression,
1390 asc,
1391 nulls_first,
1392 opclass,
1393 }
1394 })
1395 .collect()
1396}
1397
1398#[cfg(test)]
1399mod tests {
1400 use super::*;
1401
1402 #[test]
1403 fn test_process_tables() {
1404 let raw = vec![
1405 RawTableInfo {
1406 schema: "public".to_string(),
1407 name: "users".to_string(),
1408 is_unlogged: false,
1409 is_temporary: false,
1410 tablespace: None,
1411 comment: None,
1412 is_rls_enabled: false,
1413 },
1414 RawTableInfo {
1415 schema: "pg_catalog".to_string(),
1416 name: "pg_class".to_string(),
1417 is_unlogged: false,
1418 is_temporary: false,
1419 tablespace: None,
1420 comment: None,
1421 is_rls_enabled: false,
1422 },
1423 ];
1424
1425 let tables = process_tables(&raw);
1426 assert_eq!(tables.len(), 1);
1427 assert_eq!(tables[0].name, "users");
1428 }
1429
1430 #[test]
1431 fn test_introspection_result_to_snapshot() {
1432 let mut result = IntrospectionResult::default();
1433 result.schemas.push(Schema::new("public"));
1434 result.tables.push(Table {
1435 schema: "public".into(),
1436 name: "users".into(),
1437 is_unlogged: None,
1438 is_temporary: None,
1439 inherits: None,
1440 tablespace: None,
1441 is_rls_enabled: None,
1442 comment: None,
1443 });
1444
1445 let snapshot = result.to_snapshot();
1446 assert_eq!(snapshot.ddl.len(), 2);
1447 }
1448
1449 #[test]
1450 fn parse_index_columns_handles_realistic_indexdef_output() {
1451 let cols = parse_index_columns(vec![
1452 "(price * quantity)".to_string(),
1453 "\"userName\" DESC".to_string(),
1454 "lower(email) varchar_pattern_ops".to_string(),
1455 "col DESC NULLS LAST".to_string(),
1456 "\"say\"\"hi\"\"\"".to_string(),
1457 "plain_col".to_string(),
1458 "name text_pattern_ops".to_string(),
1459 ]);
1460
1461 assert_eq!(cols[0].name, "(price * quantity)");
1463 assert!(cols[0].is_expression);
1464 assert_eq!(cols[0].opclass, None);
1465 assert!(cols[0].asc);
1466 assert!(!cols[0].nulls_first);
1467
1468 assert_eq!(cols[1].name, "userName");
1470 assert!(!cols[1].is_expression);
1471 assert!(!cols[1].asc);
1472 assert!(cols[1].nulls_first);
1473
1474 assert_eq!(cols[2].name, "lower(email)");
1476 assert!(cols[2].is_expression);
1477 assert_eq!(cols[2].opclass.as_deref(), Some("varchar_pattern_ops"));
1478
1479 assert_eq!(cols[3].name, "col");
1481 assert!(!cols[3].asc);
1482 assert!(!cols[3].nulls_first);
1483
1484 assert_eq!(cols[4].name, "say\"hi\"");
1486 assert!(!cols[4].is_expression);
1487
1488 assert_eq!(cols[5].name, "plain_col");
1490 assert!(!cols[5].is_expression);
1491 assert!(cols[5].asc);
1492 assert!(!cols[5].nulls_first);
1493
1494 assert_eq!(cols[6].name, "name");
1496 assert!(!cols[6].is_expression);
1497 assert_eq!(cols[6].opclass.as_deref(), Some("text_pattern_ops"));
1498 }
1499
1500 #[test]
1501 fn process_columns_populates_identity_options_from_packed_json() {
1502 let raw = RawColumnInfo {
1503 schema: "public".to_string(),
1504 table: "users".to_string(),
1505 name: "id".to_string(),
1506 column_type: "int4".to_string(),
1507 type_schema: Some("pg_catalog".to_string()),
1508 not_null: true,
1509 default_value: None,
1510 is_identity: true,
1511 identity_type: Some(
1512 r#"{"type":"ALWAYS","start":"100","increment":"5","min":"1","max":"1000","cycle":true}"#
1513 .to_string(),
1514 ),
1515 is_generated: false,
1516 generated_expression: None,
1517 generated_stored: false,
1518 dimensions: None,
1519 comment: None,
1520 ordinal_position: 1,
1521 };
1522
1523 let columns = process_columns(&[raw]);
1524 let identity = columns[0].identity.as_ref().expect("identity");
1525 assert_eq!(identity.start_with.as_deref(), Some("100"));
1526 assert_eq!(identity.increment.as_deref(), Some("5"));
1527 assert_eq!(identity.min_value.as_deref(), Some("1"));
1528 assert_eq!(identity.max_value.as_deref(), Some("1000"));
1529 assert_eq!(identity.cycle, Some(true));
1530 }
1531
1532 #[test]
1533 fn process_columns_accepts_legacy_plain_identity_type() {
1534 let raw = RawColumnInfo {
1535 schema: "public".to_string(),
1536 table: "users".to_string(),
1537 name: "id".to_string(),
1538 column_type: "int4".to_string(),
1539 type_schema: Some("pg_catalog".to_string()),
1540 not_null: true,
1541 default_value: None,
1542 is_identity: true,
1543 identity_type: Some("BY DEFAULT".to_string()),
1544 is_generated: false,
1545 generated_expression: None,
1546 generated_stored: false,
1547 dimensions: None,
1548 comment: None,
1549 ordinal_position: 1,
1550 };
1551
1552 let columns = process_columns(&[raw]);
1553 let identity = columns[0].identity.as_ref().expect("identity");
1554 assert_eq!(identity.type_, super::super::ddl::IdentityType::ByDefault);
1555 assert_eq!(identity.increment, None);
1556 }
1557
1558 #[test]
1559 fn postgres_catalog_queries_are_privilege_scoped() {
1560 use queries::{
1561 CHECKS_QUERY, COLUMNS_QUERY, ENUMS_QUERY, FOREIGN_KEYS_QUERY, INDEXES_QUERY,
1562 POLICIES_QUERY, PRIMARY_KEYS_QUERY, SCHEMAS_QUERY, SEQUENCES_QUERY, TABLES_QUERY,
1563 UNIQUES_QUERY, VIEWS_QUERY,
1564 };
1565
1566 for query in [
1567 SCHEMAS_QUERY,
1568 TABLES_QUERY,
1569 COLUMNS_QUERY,
1570 ENUMS_QUERY,
1571 SEQUENCES_QUERY,
1572 VIEWS_QUERY,
1573 INDEXES_QUERY,
1574 FOREIGN_KEYS_QUERY,
1575 PRIMARY_KEYS_QUERY,
1576 UNIQUES_QUERY,
1577 CHECKS_QUERY,
1578 POLICIES_QUERY,
1579 ] {
1580 assert!(
1581 query.contains("has_schema_privilege"),
1582 "query is not schema-privilege scoped: {query}"
1583 );
1584 }
1585
1586 for query in [
1587 TABLES_QUERY,
1588 VIEWS_QUERY,
1589 INDEXES_QUERY,
1590 FOREIGN_KEYS_QUERY,
1591 PRIMARY_KEYS_QUERY,
1592 UNIQUES_QUERY,
1593 CHECKS_QUERY,
1594 POLICIES_QUERY,
1595 ] {
1596 assert!(
1597 query.contains("has_table_privilege"),
1598 "query is not table-privilege scoped: {query}"
1599 );
1600 }
1601 }
1602}