completely changed system of how are tables linked together

This commit is contained in:
filipriec
2025-03-08 19:35:26 +01:00
parent 9396a08af0
commit ba672098c2
5 changed files with 108 additions and 67 deletions

View File

@@ -10,12 +10,18 @@ service TableDefinition {
rpc DeleteTable (DeleteTableRequest) returns (DeleteTableResponse); rpc DeleteTable (DeleteTableRequest) returns (DeleteTableResponse);
} }
message TableLink {
string linked_table_name = 1;
bool required = 2;
}
message PostTableDefinitionRequest { message PostTableDefinitionRequest {
string table_name = 1; string table_name = 1;
repeated ColumnDefinition columns = 2; repeated TableLink links = 2;
repeated string indexes = 3; repeated ColumnDefinition columns = 3;
string profile_name = 4; repeated string indexes = 4;
optional string linked_table_name = 5; string profile_name = 5;
optional string linked_table_name = 6;
} }
message ColumnDefinition { message ColumnDefinition {

Binary file not shown.

View File

@@ -1,15 +1,24 @@
// This file is @generated by prost-build. // This file is @generated by prost-build.
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct TableLink {
#[prost(string, tag = "1")]
pub linked_table_name: ::prost::alloc::string::String,
#[prost(bool, tag = "2")]
pub required: bool,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PostTableDefinitionRequest { pub struct PostTableDefinitionRequest {
#[prost(string, tag = "1")] #[prost(string, tag = "1")]
pub table_name: ::prost::alloc::string::String, pub table_name: ::prost::alloc::string::String,
#[prost(message, repeated, tag = "2")] #[prost(message, repeated, tag = "2")]
pub links: ::prost::alloc::vec::Vec<TableLink>,
#[prost(message, repeated, tag = "3")]
pub columns: ::prost::alloc::vec::Vec<ColumnDefinition>, pub columns: ::prost::alloc::vec::Vec<ColumnDefinition>,
#[prost(string, repeated, tag = "3")] #[prost(string, repeated, tag = "4")]
pub indexes: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, pub indexes: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
#[prost(string, tag = "4")] #[prost(string, tag = "5")]
pub profile_name: ::prost::alloc::string::String, pub profile_name: ::prost::alloc::string::String,
#[prost(string, optional, tag = "5")] #[prost(string, optional, tag = "6")]
pub linked_table_name: ::core::option::Option<::prost::alloc::string::String>, pub linked_table_name: ::core::option::Option<::prost::alloc::string::String>,
} }
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]

View File

@@ -1,4 +1,4 @@
-- Add migration script here -- Main table definitions
CREATE TABLE table_definitions ( CREATE TABLE table_definitions (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
deleted BOOLEAN NOT NULL DEFAULT FALSE, deleted BOOLEAN NOT NULL DEFAULT FALSE,
@@ -6,16 +6,21 @@ 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, profile_id BIGINT NOT NULL REFERENCES profiles(id) DEFAULT 1
linked_table_id BIGINT REFERENCES table_definitions(id) );
-- Relationship table for multiple links
CREATE TABLE table_definition_links (
source_table_id BIGINT NOT NULL REFERENCES table_definitions(id) ON DELETE CASCADE,
linked_table_id BIGINT NOT NULL REFERENCES table_definitions(id),
is_required BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (source_table_id, linked_table_id)
); );
-- Create composite unique index for profile+table combination -- Create composite unique index for profile+table combination
CREATE UNIQUE INDEX idx_table_definitions_profile_table CREATE UNIQUE INDEX idx_table_definitions_profile_table
ON table_definitions (profile_id, table_name); ON table_definitions (profile_id, table_name);
-- Add self-referential foreign key constraint CREATE INDEX idx_links_source ON table_definition_links (source_table_id);
ALTER TABLE table_definitions CREATE INDEX idx_links_target ON table_definition_links (linked_table_id);
ADD CONSTRAINT fk_linked_table
FOREIGN KEY (linked_table_id)
REFERENCES table_definitions(id);

View File

@@ -66,33 +66,30 @@ pub async fn post_table_definition(
.await .await
.map_err(|e| Status::internal(format!("Profile error: {}", e)))?; .map_err(|e| Status::internal(format!("Profile error: {}", e)))?;
// Declare linked_table_id here // Process table links
let linked_table_id; let mut links = Vec::new();
for link in request.links.drain(..) {
// Validate linked table if provided let linked_table = sqlx::query!(
if let Some(lt_name) = &request.linked_table_name {
let lt_record = sqlx::query!(
"SELECT id FROM table_definitions "SELECT id FROM table_definitions
WHERE profile_id = $1 AND table_name = $2", WHERE profile_id = $1 AND table_name = $2",
profile.id, profile.id,
lt_name link.linked_table_name
) )
.fetch_optional(db_pool) .fetch_optional(db_pool)
.await .await
.map_err(|e| Status::internal(format!("Linked table lookup failed: {}", e)))?; .map_err(|e| Status::internal(format!("Linked table lookup failed: {}", e)))?;
linked_table_id = match lt_record { let linked_id = linked_table.ok_or_else(||
Some(r) => Some(r.id), Status::not_found(format!("Linked table {} not found", link.linked_table_name))
None => return Err(Status::not_found("Linked table not found in profile")), )?.id;
};
} else { links.push((linked_id, link.required));
linked_table_id = None;
} }
// Process columns without year prefix // Process columns
let mut columns = Vec::new(); let mut columns = Vec::new();
for col_def in request.columns.drain(..) { for col_def in request.columns.drain(..) {
let col_name = sanitize_identifier(&col_def.name); // No year prefix let col_name = sanitize_identifier(&col_def.name);
if !is_valid_identifier(&col_def.name) { if !is_valid_identifier(&col_def.name) {
return Err(Status::invalid_argument("Invalid column name")); return Err(Status::invalid_argument("Invalid column name"));
} }
@@ -100,7 +97,7 @@ pub async fn post_table_definition(
columns.push(format!("\"{}\" {}", col_name, sql_type)); columns.push(format!("\"{}\" {}", col_name, sql_type));
} }
// Process indexes without year prefix // Process indexes
let mut indexes = Vec::new(); let mut indexes = Vec::new();
for idx in request.indexes.drain(..) { for idx in request.indexes.drain(..) {
let idx_name = sanitize_identifier(&idx); let idx_name = sanitize_identifier(&idx);
@@ -110,26 +107,21 @@ pub async fn post_table_definition(
indexes.push(idx_name); indexes.push(idx_name);
} }
// Generate SQL // Generate SQL with multiple links
let (create_sql, index_sql) = generate_table_sql( let (create_sql, index_sql) = generate_table_sql(db_pool, &table_name, &columns, &indexes, &links).await?;
&table_name,
&columns,
&indexes,
request.linked_table_name.as_deref()
);
// Store definition // Store main table definition
sqlx::query!( let table_def = sqlx::query!(
r#"INSERT INTO table_definitions r#"INSERT INTO table_definitions
(profile_id, table_name, columns, indexes, linked_table_id) (profile_id, table_name, columns, indexes)
VALUES ($1, $2, $3, $4, $5)"#, VALUES ($1, $2, $3, $4)
RETURNING id"#,
profile.id, profile.id,
&table_name, &table_name,
json!(columns), json!(columns),
json!(indexes), json!(indexes)
linked_table_id
) )
.execute(db_pool) .fetch_one(db_pool)
.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() {
@@ -140,6 +132,21 @@ pub async fn post_table_definition(
Status::internal(format!("Database error: {}", e)) Status::internal(format!("Database error: {}", e))
})?; })?;
// Store relationships
for (linked_id, is_required) in links {
sqlx::query!(
"INSERT INTO table_definition_links
(source_table_id, linked_table_id, is_required)
VALUES ($1, $2, $3)",
table_def.id,
linked_id,
is_required
)
.execute(db_pool)
.await
.map_err(|e| Status::internal(format!("Failed to save link: {}", e)))?;
}
// Execute generated SQL // Execute generated SQL
sqlx::query(&create_sql) sqlx::query(&create_sql)
.execute(db_pool) .execute(db_pool)
@@ -147,7 +154,7 @@ pub async fn post_table_definition(
.map_err(|e| Status::internal(format!("Table creation failed: {}", e)))?; .map_err(|e| Status::internal(format!("Table creation failed: {}", e)))?;
for sql in &index_sql { for sql in &index_sql {
sqlx::query(&sql) sqlx::query(sql)
.execute(db_pool) .execute(db_pool)
.await .await
.map_err(|e| Status::internal(format!("Index creation failed: {}", e)))?; .map_err(|e| Status::internal(format!("Index creation failed: {}", e)))?;
@@ -159,49 +166,51 @@ pub async fn post_table_definition(
}) })
} }
fn generate_table_sql( async fn generate_table_sql(
db_pool: &PgPool,
table_name: &str, table_name: &str,
columns: &[String], columns: &[String],
indexes: &[String], indexes: &[String],
linked_table: Option<&str>, links: &[(i64, bool)],
) -> (String, Vec<String>) { ) -> Result<(String, Vec<String>), Status> {
let mut system_columns = vec![ let mut system_columns = vec![
"id BIGSERIAL PRIMARY KEY".to_string(), "id BIGSERIAL PRIMARY KEY".to_string(),
"deleted BOOLEAN NOT NULL DEFAULT FALSE".to_string(), "deleted BOOLEAN NOT NULL DEFAULT FALSE".to_string(),
]; ];
if let Some(linked) = linked_table { // Add foreign key columns
let parts: Vec<&str> = linked.splitn(2, '_').collect(); let mut link_info = Vec::new();
let base_name = parts.get(1).unwrap_or(&linked); for (linked_id, required) in links {
let linked_table = get_table_name_by_id(db_pool, *linked_id).await?;
let base_name = linked_table.split('_').last().unwrap_or(&linked_table);
let null_clause = if *required { "NOT NULL" } else { "" };
system_columns.push( system_columns.push(
format!("\"{}_id\" BIGINT NOT NULL REFERENCES \"{}\"(id)", format!("\"{0}_id\" BIGINT {1} REFERENCES \"{2}\"(id)",
base_name, base_name, null_clause, linked_table
linked
) )
); );
link_info.push((base_name.to_string(), linked_table));
} }
// Combine all columns
let all_columns = system_columns let all_columns = system_columns
.iter() .iter()
.chain(columns.iter()) .chain(columns.iter())
.cloned() .cloned()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
// Build CREATE TABLE statement
let create_sql = format!( let create_sql = format!(
"CREATE TABLE \"{}\" (\n {},\n created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP\n)", "CREATE TABLE \"{}\" (\n {},\n created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP\n)",
table_name, table_name,
all_columns.join(",\n ") all_columns.join(",\n ")
); );
let mut system_indexes = vec![]; // Generate indexes
let mut system_indexes = Vec::new();
if let Some(linked) = linked_table { for (base_name, _) in &link_info {
let parts: Vec<&str> = linked.splitn(2, '_').collect();
let base_name = parts.get(1).unwrap_or(&linked);
system_indexes.push(format!( system_indexes.push(format!(
"CREATE INDEX idx_{}_{}_id ON \"{}\" (\"{}_id\")", "CREATE INDEX idx_{}_{}_fk ON \"{}\" (\"{}_id\")",
table_name, base_name, table_name, base_name table_name, base_name, table_name, base_name
)); ));
} }
@@ -212,7 +221,19 @@ fn generate_table_sql(
format!("CREATE INDEX idx_{}_{} ON \"{}\" (\"{}\")", format!("CREATE INDEX idx_{}_{} ON \"{}\" (\"{}\")",
table_name, idx, table_name, idx) table_name, idx, table_name, idx)
})) }))
.collect::<Vec<_>>(); .collect();
(create_sql, all_indexes) Ok((create_sql, all_indexes))
}
async fn get_table_name_by_id(db_pool: &PgPool, table_id: i64) -> Result<String, Status> {
let record = sqlx::query!(
"SELECT table_name FROM table_definitions WHERE id = $1",
table_id
)
.fetch_one(db_pool)
.await
.map_err(|e| Status::internal(format!("Table lookup failed: {}", e)))?;
Ok(record.table_name)
} }