completely changed system of how are tables linked together
This commit is contained in:
@@ -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.
@@ -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)]
|
||||||
|
|||||||
@@ -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);
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user