changing profile id to schema in the whole project

This commit is contained in:
filipriec
2025-06-21 09:57:14 +02:00
parent 9477f53432
commit 63f1b4da2e
8 changed files with 84 additions and 64 deletions

View File

@@ -1,9 +1,12 @@
-- Add migration script here -- Add migration script here
CREATE TABLE profiles ( CREATE TABLE schemas (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE, name TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
description TEXT,
is_active BOOLEAN DEFAULT TRUE
); );
-- Create default profile for existing data -- Create default profile for existing data
INSERT INTO profiles (name) VALUES ('default'); INSERT INTO schemas (name) VALUES ('default');
CREATE SCHEMA IF NOT EXISTS "default";

View File

@@ -1,5 +1,4 @@
-- Main table definitions -- Main table definitions
CREATE SCHEMA IF NOT EXISTS gen;
CREATE TABLE table_definitions ( CREATE TABLE table_definitions (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
@@ -8,7 +7,7 @@ CREATE TABLE table_definitions (
columns JSONB NOT NULL, columns JSONB NOT NULL,
indexes JSONB NOT NULL, indexes JSONB NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
profile_id BIGINT NOT NULL REFERENCES profiles(id) DEFAULT 1 schema_id BIGINT NOT NULL REFERENCES schemas(id)
); );
-- Relationship table for multiple links -- Relationship table for multiple links
@@ -20,9 +19,10 @@ CREATE TABLE table_definition_links (
PRIMARY KEY (source_table_id, linked_table_id) PRIMARY KEY (source_table_id, linked_table_id)
); );
-- Create composite unique index for profile+table combination -- Create composite unique index for schema+table combination
CREATE UNIQUE INDEX idx_table_definitions_profile_table CREATE UNIQUE INDEX idx_table_definitions_schema_table
ON table_definitions (profile_id, table_name); ON table_definitions (schema_id, table_name);
CREATE INDEX idx_links_source ON table_definition_links (source_table_id); CREATE INDEX idx_links_source ON table_definition_links (source_table_id);
CREATE INDEX idx_links_target ON table_definition_links (linked_table_id); CREATE INDEX idx_links_target ON table_definition_links (linked_table_id);

View File

@@ -8,7 +8,7 @@ CREATE TABLE table_scripts (
script TEXT NOT NULL, script TEXT NOT NULL,
description TEXT, description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
profile_id BIGINT NOT NULL REFERENCES profiles(id) DEFAULT 1, schema_id BIGINT NOT NULL REFERENCES schemas(id),
UNIQUE(table_definitions_id, target_column) UNIQUE(table_definitions_id, target_column)
); );

View File

@@ -20,23 +20,22 @@ pub async fn qualify_table_name(
let definition_exists = sqlx::query!( let definition_exists = sqlx::query!(
r#"SELECT EXISTS ( r#"SELECT EXISTS (
SELECT 1 FROM table_definitions td SELECT 1 FROM table_definitions td
JOIN profiles p ON td.profile_id = p.id JOIN schemas s ON td.schema_id = s.id
WHERE p.name = $1 AND td.table_name = $2 WHERE s.name = $1 AND td.table_name = $2
)"#, )"#,
profile_name, profile_name,
table_name table_name
) )
.fetch_one(db_pool) .fetch_one(db_pool)
.await .await
.map_err(|e| Status::internal(format!("Schema lookup failed: {}", e)))? .map_err(|e| Status::internal(format!("Schema lookup failed: {}", e)))?
.exists .exists
.unwrap_or(false); .unwrap_or(false);
if definition_exists { if definition_exists {
// It's a user-defined table, so it lives in 'gen'. Ok(format!("{}.\"{}\"", profile_name, table_name))
Ok(format!("gen.\"{}\"", table_name))
} else { } else {
// It's not a user-defined table, so it must be a system table in 'public'. // It's not a user-defined table, so it must be a system table in 'public.
Ok(format!("\"{}\"", table_name)) Ok(format!("\"{}\"", table_name))
} }
} }

View File

@@ -1,4 +1,4 @@
// server/src/table_definition/handlers/delete_table.rs // src/table_definition/handlers/delete_table.rs
use tonic::Status; use tonic::Status;
use sqlx::PgPool; use sqlx::PgPool;
use common::proto::multieko2::table_definition::{DeleteTableRequest, DeleteTableResponse}; use common::proto::multieko2::table_definition::{DeleteTableRequest, DeleteTableResponse};
@@ -10,25 +10,25 @@ pub async fn delete_table(
let mut transaction = db_pool.begin().await let mut transaction = db_pool.begin().await
.map_err(|e| Status::internal(format!("Failed to start transaction: {}", e)))?; .map_err(|e| Status::internal(format!("Failed to start transaction: {}", e)))?;
// Step 1: Get profile and validate existence // Step 1: Get schema and validate existence
let profile = sqlx::query!( let schema = sqlx::query!(
"SELECT id FROM profiles WHERE name = $1", "SELECT id, name FROM schemas WHERE name = $1",
request.profile_name request.profile_name
) )
.fetch_optional(&mut *transaction) .fetch_optional(&mut *transaction)
.await .await
.map_err(|e| Status::internal(format!("Profile lookup failed: {}", e)))?; .map_err(|e| Status::internal(format!("Schema lookup failed: {}", e)))?;
let profile_id = match profile { let (schema_id, schema_name) = match schema {
Some(p) => p.id, Some(s) => (s.id, s.name),
None => return Err(Status::not_found("Profile not found")), None => return Err(Status::not_found("Profile not found")),
}; };
// Step 2: Get table definition and validate existence // Step 2: Get table definition and validate existence
let table_def = sqlx::query!( let table_def = sqlx::query!(
"SELECT id FROM table_definitions "SELECT id FROM table_definitions
WHERE profile_id = $1 AND table_name = $2", WHERE schema_id = $1 AND table_name = $2",
profile_id, schema_id,
request.table_name request.table_name
) )
.fetch_optional(&mut *transaction) .fetch_optional(&mut *transaction)
@@ -40,8 +40,9 @@ pub async fn delete_table(
None => return Err(Status::not_found("Table not found in profile")), None => return Err(Status::not_found("Table not found in profile")),
}; };
// Step 3: Drop the actual PostgreSQL table with CASCADE // Step 3: Drop the actual PostgreSQL table with CASCADE (schema-qualified)
sqlx::query(&format!(r#"DROP TABLE IF EXISTS "{}" CASCADE"#, request.table_name)) let drop_table_sql = format!(r#"DROP TABLE IF EXISTS "{}"."{}" CASCADE"#, schema_name, request.table_name);
sqlx::query(&drop_table_sql)
.execute(&mut *transaction) .execute(&mut *transaction)
.await .await
.map_err(|e| Status::internal(format!("Table drop failed: {}", e)))?; .map_err(|e| Status::internal(format!("Table drop failed: {}", e)))?;
@@ -55,23 +56,31 @@ pub async fn delete_table(
.await .await
.map_err(|e| Status::internal(format!("Definition deletion failed: {}", e)))?; .map_err(|e| Status::internal(format!("Definition deletion failed: {}", e)))?;
// Step 5: Check and clean up profile if empty // Step 5: Check and clean up schema if empty
let remaining = sqlx::query!( let remaining = sqlx::query!(
"SELECT COUNT(*) as count FROM table_definitions WHERE profile_id = $1", "SELECT COUNT(*) as count FROM table_definitions WHERE schema_id = $1",
profile_id schema_id
) )
.fetch_one(&mut *transaction) .fetch_one(&mut *transaction)
.await .await
.map_err(|e| Status::internal(format!("Count query failed: {}", e)))?; .map_err(|e| Status::internal(format!("Count query failed: {}", e)))?;
if remaining.count.unwrap_or(1) == 0 { if remaining.count.unwrap_or(1) == 0 {
// Drop the PostgreSQL schema if empty
let drop_schema_sql = format!(r#"DROP SCHEMA IF EXISTS "{}" CASCADE"#, schema_name);
sqlx::query(&drop_schema_sql)
.execute(&mut *transaction)
.await
.map_err(|e| Status::internal(format!("Schema drop failed: {}", e)))?;
// Delete the schema record
sqlx::query!( sqlx::query!(
"DELETE FROM profiles WHERE id = $1", "DELETE FROM schemas WHERE id = $1",
profile_id schema_id
) )
.execute(&mut *transaction) .execute(&mut *transaction)
.await .await
.map_err(|e| Status::internal(format!("Profile cleanup failed: {}", e)))?; .map_err(|e| Status::internal(format!("Schema cleanup failed: {}", e)))?;
} }
transaction.commit().await transaction.commit().await

View File

@@ -15,13 +15,15 @@ pub async fn get_profile_tree(
) -> Result<Response<ProfileTreeResponse>, Status> { ) -> Result<Response<ProfileTreeResponse>, Status> {
let mut profiles = Vec::new(); let mut profiles = Vec::new();
// Get all profiles // Get all schemas (internally changed from profiles to schemas)
let profile_records = sqlx::query!("SELECT id, name FROM profiles") let schema_records = sqlx::query!(
.fetch_all(db_pool) "SELECT id, name FROM schemas ORDER BY name"
.await )
.map_err(|e| Status::internal(format!("Failed to fetch profiles: {}", e)))?; .fetch_all(db_pool)
.await
.map_err(|e| Status::internal(format!("Failed to fetch schemas: {}", e)))?;
for profile in profile_records { for schema in schema_records {
// Get all tables with their dependencies from the links table // Get all tables with their dependencies from the links table
let tables = sqlx::query!( let tables = sqlx::query!(
r#" r#"
@@ -35,15 +37,16 @@ pub async fn get_profile_tree(
'required', tdl.is_required 'required', tdl.is_required
) )
) FILTER (WHERE ltd.id IS NOT NULL), ) FILTER (WHERE ltd.id IS NOT NULL),
'[]' '[]'::json
) as dependencies ) as dependencies
FROM table_definitions td FROM table_definitions td
LEFT JOIN table_definition_links tdl ON td.id = tdl.source_table_id LEFT JOIN table_definition_links tdl ON td.id = tdl.source_table_id
LEFT JOIN table_definitions ltd ON tdl.linked_table_id = ltd.id LEFT JOIN table_definitions ltd ON tdl.linked_table_id = ltd.id
WHERE td.profile_id = $1 WHERE td.schema_id = $1
GROUP BY td.id, td.table_name GROUP BY td.id, td.table_name
ORDER BY td.table_name
"#, "#,
profile.id schema.id
) )
.fetch_all(db_pool) .fetch_all(db_pool)
.await .await
@@ -70,8 +73,9 @@ pub async fn get_profile_tree(
}) })
.collect(); .collect();
// External API still returns "profiles" for compatibility
profiles.push(Profile { profiles.push(Profile {
name: profile.name, name: schema.name,
tables: proto_tables tables: proto_tables
}); });
} }

View File

@@ -100,8 +100,8 @@ fn is_invalid_table_name(table_name: &str) -> bool {
fn is_reserved_schema(schema_name: &str) -> bool { fn is_reserved_schema(schema_name: &str) -> bool {
let lower = schema_name.to_lowercase(); let lower = schema_name.to_lowercase();
lower == "public" || lower == "public" ||
lower == "information_schema" || lower == "information_schema" ||
lower.starts_with("pg_") lower.starts_with("pg_")
} }
@@ -115,12 +115,12 @@ pub async fn post_table_definition(
// Apply same sanitization rules as table names // Apply same sanitization rules as table names
let sanitized_profile_name = sanitize_identifier(&request.profile_name); let sanitized_profile_name = sanitize_identifier(&request.profile_name);
// Add validation to prevent reserved schemas // Add validation to prevent reserved schemas
if is_reserved_schema(&sanitized_profile_name) { if is_reserved_schema(&sanitized_profile_name) {
return Err(Status::invalid_argument("Profile name is reserved and cannot be used")); return Err(Status::invalid_argument("Profile name is reserved and cannot be used"));
} }
if !is_valid_identifier(&sanitized_profile_name) { if !is_valid_identifier(&sanitized_profile_name) {
return Err(Status::invalid_argument("Invalid profile name")); return Err(Status::invalid_argument("Invalid profile name"));
} }
@@ -184,28 +184,31 @@ async fn execute_table_definition(
table_name: String, table_name: String,
profile_name: String, profile_name: String,
) -> Result<TableDefinitionResponse, Status> { ) -> Result<TableDefinitionResponse, Status> {
let profile = sqlx::query!( // CHANGED: Use schemas table instead of profiles table
"INSERT INTO profiles (name) VALUES ($1) let schema = sqlx::query!(
"INSERT INTO schemas (name) VALUES ($1)
ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name
RETURNING id", RETURNING id",
request.profile_name request.profile_name
) )
.fetch_one(&mut **tx) .fetch_one(&mut **tx)
.await .await
.map_err(|e| Status::internal(format!("Profile error: {}", e)))?; .map_err(|e| Status::internal(format!("Schema error: {}", e)))?;
// Create schema if it doesn't exist // Create PostgreSQL schema if it doesn't exist
sqlx::query(&format!("CREATE SCHEMA IF NOT EXISTS \"{}\"", profile_name)) let create_schema_sql = format!("CREATE SCHEMA IF NOT EXISTS \"{}\"", profile_name);
sqlx::query(&create_schema_sql)
.execute(&mut **tx) .execute(&mut **tx)
.await .await
.map_err(|e| Status::internal(format!("Schema creation failed: {}", e)))?; .map_err(|e| Status::internal(format!("Schema creation failed: {}", e)))?;
let mut links = Vec::new(); let mut links = Vec::new();
for link in request.links.drain(..) { for link in request.links.drain(..) {
// CHANGED: Use schema_id instead of profile_id
let linked_table = sqlx::query!( let linked_table = sqlx::query!(
"SELECT id FROM table_definitions "SELECT id FROM table_definitions
WHERE profile_id = $1 AND table_name = $2", WHERE schema_id = $1 AND table_name = $2",
profile.id, schema.id,
link.linked_table_name link.linked_table_name
) )
.fetch_optional(&mut **tx) .fetch_optional(&mut **tx)
@@ -246,12 +249,13 @@ async fn execute_table_definition(
let (create_sql, index_sql) = generate_table_sql(tx, &profile_name, &table_name, &columns, &indexes, &links).await?; let (create_sql, index_sql) = generate_table_sql(tx, &profile_name, &table_name, &columns, &indexes, &links).await?;
// CHANGED: Use schema_id instead of profile_id
let table_def = sqlx::query!( let table_def = sqlx::query!(
r#"INSERT INTO table_definitions r#"INSERT INTO table_definitions
(profile_id, table_name, columns, indexes) (schema_id, table_name, columns, indexes)
VALUES ($1, $2, $3, $4) VALUES ($1, $2, $3, $4)
RETURNING id"#, RETURNING id"#,
profile.id, schema.id,
&table_name, &table_name,
json!(columns), json!(columns),
json!(indexes) json!(indexes)
@@ -260,7 +264,8 @@ async fn execute_table_definition(
.await .await
.map_err(|e| { .map_err(|e| {
if let Some(db_err) = e.as_database_error() { if let Some(db_err) = e.as_database_error() {
if db_err.constraint() == Some("idx_table_definitions_profile_table") { // CHANGED: Update constraint name to match new schema
if db_err.constraint() == Some("idx_table_definitions_schema_table") {
return Status::already_exists("Table already exists in this profile"); return Status::already_exists("Table already exists in this profile");
} }
} }

View File

@@ -20,11 +20,11 @@ pub async fn get_table_structure(
) -> Result<TableStructureResponse, Status> { ) -> Result<TableStructureResponse, Status> {
let profile_name = request.profile_name; let profile_name = request.profile_name;
let table_name = request.table_name; let table_name = request.table_name;
let table_schema = "gen"; let table_schema = &profile_name;
// 1. Validate Profile // 1. Validate Profile
let profile = sqlx::query!( let schema = sqlx::query!(
"SELECT id FROM profiles WHERE name = $1", "SELECT id FROM schemas WHERE name = $1",
profile_name profile_name
) )
.fetch_optional(db_pool) .fetch_optional(db_pool)
@@ -36,8 +36,8 @@ pub async fn get_table_structure(
)) ))
})?; })?;
let profile_id = match profile { let schema_id = match schema {
Some(p) => p.id, Some(s) => s.id,
None => { None => {
return Err(Status::not_found(format!( return Err(Status::not_found(format!(
"Profile '{}' not found", "Profile '{}' not found",
@@ -48,8 +48,8 @@ pub async fn get_table_structure(
// 2. Validate Table within Profile // 2. Validate Table within Profile
sqlx::query!( sqlx::query!(
"SELECT id FROM table_definitions WHERE profile_id = $1 AND table_name = $2", "SELECT id FROM table_definitions WHERE schema_id = $1 AND table_name = $2",
profile_id, schema_id,
table_name table_name
) )
.fetch_optional(db_pool) .fetch_optional(db_pool)