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);
|
||||
}
|
||||
|
||||
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.
@@ -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)]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user