Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion sea-orm-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,9 @@ pub enum GenerateSubcommands {
env = "DATABASE_SCHEMA",
long_help = "Database schema\n \
- For MySQL, this argument is ignored.\n \
- For PostgreSQL, this argument is optional with default value 'public'."
- For PostgreSQL, this argument is optional with default value 'public'.\n \
- Note: Types (such as enums) can be referenced from other schemas (e.g., 'public') \
even when generating entities for a different schema."
)]
database_schema: Option<String>,

Expand Down
108 changes: 99 additions & 9 deletions sea-orm-cli/src/commands/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,22 +189,108 @@ pub async fn run_generate_command(
#[cfg(feature = "sqlx-postgres")]
{
use sea_schema::postgres::discovery::SchemaDiscovery;
use sqlx::Postgres;
use sqlx::{Postgres, Row};
use std::collections::{HashMap, HashSet};

println!("Connecting to Postgres ...");
let schema = database_schema.as_deref().unwrap_or("public");
let connection = sqlx_connect::<Postgres>(
let pool = sqlx_connect::<Postgres>(
max_connections,
acquire_timeout,
url.as_str(),
Some(schema),
)
.await?;
println!("Discovering schema ...");
let schema_discovery = SchemaDiscovery::new(connection, schema);
let schema = schema_discovery.discover().await?;
let table_stmts = schema
.tables

// Discover all schemas that need to be included based on cross-schema references
println!("Analyzing cross-schema dependencies ...");

let mut schemas_to_discover = HashSet::new();
schemas_to_discover.insert(schema.to_string());

// Query to find all schemas referenced by the target schema via foreign keys
let fk_query = r#"
SELECT DISTINCT
fn.nspname AS foreign_schema
FROM pg_constraint c
JOIN pg_class t ON c.conrelid = t.oid
JOIN pg_namespace n ON t.relnamespace = n.oid
JOIN pg_class ft ON c.confrelid = ft.oid
JOIN pg_namespace fn ON ft.relnamespace = fn.oid
WHERE c.contype = 'f'
AND n.nspname = $1
AND fn.nspname != $1
"#;

let fk_rows = sqlx::query(fk_query).bind(schema).fetch_all(&pool).await?;

for row in fk_rows {
let foreign_schema: String = row.get("foreign_schema");
schemas_to_discover.insert(foreign_schema);
}

// Query to find all schemas that have enums used by the target schema
let enum_schema_query = r#"
SELECT DISTINCT tn.nspname AS type_schema
FROM pg_attribute a
JOIN pg_class c ON a.attrelid = c.oid
JOIN pg_namespace n ON c.relnamespace = n.oid
JOIN pg_type t ON a.atttypid = t.oid
JOIN pg_namespace tn ON t.typnamespace = tn.oid
WHERE n.nspname = $1
AND t.typtype = 'e'
AND tn.nspname != $1
"#;

let enum_schema_rows = sqlx::query(enum_schema_query)
.bind(schema)
.fetch_all(&pool)
.await?;

for row in enum_schema_rows {
let type_schema: String = row.get("type_schema");
schemas_to_discover.insert(type_schema);
}

println!("Will discover schemas: {:?}", schemas_to_discover);

// Discover all enums from all relevant schemas
let enum_query = r#"
SELECT n.nspname as schema, t.typname as typename, e.enumlabel
FROM pg_type t
JOIN pg_enum e ON t.oid = e.enumtypid
JOIN pg_namespace n ON n.oid = t.typnamespace
ORDER BY schema, typename, e.enumsortorder
"#;

let enum_rows = sqlx::query(enum_query).fetch_all(&pool).await?;
let mut all_enums: HashMap<String, Vec<String>> = HashMap::new();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this variable used for?

for row in enum_rows {
let typename: String = row.get("typename");
let enumlabel: String = row.get("enumlabel");
all_enums
.entry(typename)
.or_insert_with(Vec::new)
.push(enumlabel);
}

// Discover tables from all relevant schemas
let mut all_tables = Vec::new();

for discover_schema in schemas_to_discover.iter() {
println!("Discovering tables in schema: {}", discover_schema);
let discovery = SchemaDiscovery::new(pool.clone(), discover_schema);
let discovered = discovery.discover().await?;
all_tables.extend(discovered.tables);
}

println!(
"Discovered {} tables across {} schemas",
all_tables.len(),
schemas_to_discover.len()
Copy link
Member

@Huliiiiii Huliiiiii Oct 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May I ask, are you doing vibe coding? This code is hard to understand and print too much logs.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used Claude to clean up when I was done

);

let table_stmts = all_tables
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly, this will generate tables of all schemas.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It finds all the tables and only generates the necessary ones. For example if there's a foreign key on another schema it will bring the relation in, same enums and types in other schemas

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is your code, I didn't see you filtering out tables that are not part of the target schema.

 let mut all_tables = Vec::new();

for discover_schema in schemas_to_discover.iter() {
    println!("Discovering tables in schema: {}", discover_schema);
    let discovery = SchemaDiscovery::new(pool.clone(), discover_schema);
    let discovered = discovery.discover().await?;
    all_tables.extend(discovered.tables);
}

let table_stmts = all_tables

.into_iter()
.filter(|schema| filter_tables(&schema.info.name))
.filter(|schema| filter_hidden_tables(&schema.info.name))
Expand Down Expand Up @@ -278,10 +364,14 @@ where
let mut pool_options = sqlx::pool::PoolOptions::<DB>::new()
.max_connections(max_connections)
.acquire_timeout(time::Duration::from_secs(acquire_timeout));
// Set search_path for Postgres, E.g. Some("public") by default
// Set search_path for Postgres to allow cross-schema type resolution
// MySQL & SQLite connection initialize with schema `None`
if let Some(schema) = schema {
let sql = format!("SET search_path = '{schema}'");
// Always include "$user" and public in search_path to support cross-schema type references
// This allows types (like enums) defined in any schema to be discovered
// PostgreSQL will search in order: target schema, then "$user", then public
// See: https://www.postgresql.org/docs/current/ddl-schemas.html#DDL-SCHEMAS-PATH
let sql = format!("SET search_path = '{schema}', \"$user\", public");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this addresses the issue... If the referenced type comes from another schema, this would still fail. Also, there will be conflicts between tables/types with the same name from different schemas.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe my approach to testing locally is a bit naive, I tested with a custom schema and public schema and it generated the entities. I'll test again

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved the status_type to a new control_schema and it was generated under the public schema. I'll fix

pool_options = pool_options.after_connect(move |conn, _| {
let sql = sql.clone();
Box::pin(async move {
Expand Down