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);
}
message TableLink {
string linked_table_name = 1;
bool required = 2;
}
message PostTableDefinitionRequest {
string table_name = 1;
repeated ColumnDefinition columns = 2;
repeated string indexes = 3;
string profile_name = 4;
optional string linked_table_name = 5;
repeated TableLink links = 2;
repeated ColumnDefinition columns = 3;
repeated string indexes = 4;
string profile_name = 5;
optional string linked_table_name = 6;
}
message ColumnDefinition {

Binary file not shown.

View File

@@ -1,15 +1,24 @@
// This file is @generated by prost-build.
#[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 {
#[prost(string, tag = "1")]
pub table_name: ::prost::alloc::string::String,
#[prost(message, repeated, tag = "2")]
pub links: ::prost::alloc::vec::Vec<TableLink>,
#[prost(message, repeated, tag = "3")]
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>,
#[prost(string, tag = "4")]
#[prost(string, tag = "5")]
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>,
}
#[derive(Clone, PartialEq, ::prost::Message)]

View File

@@ -1,4 +1,4 @@
-- Add migration script here
-- Main table definitions
CREATE TABLE table_definitions (
id BIGSERIAL PRIMARY KEY,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
@@ -6,16 +6,21 @@ CREATE TABLE table_definitions (
columns JSONB NOT NULL,
indexes JSONB NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
profile_id BIGINT NOT NULL REFERENCES profiles(id) DEFAULT 1,
linked_table_id BIGINT REFERENCES table_definitions(id)
profile_id BIGINT NOT NULL REFERENCES profiles(id) DEFAULT 1
);
-- 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 UNIQUE INDEX idx_table_definitions_profile_table
ON table_definitions (profile_id, table_name);
-- Add self-referential foreign key constraint
ALTER TABLE table_definitions
ADD CONSTRAINT fk_linked_table
FOREIGN KEY (linked_table_id)
REFERENCES table_definitions(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);

View File

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