SCRIPTS only scripts reference to a linked table from this commit

This commit is contained in:
filipriec
2025-07-11 08:55:32 +02:00
parent fbe8e53858
commit d2053b1d5a
2 changed files with 93 additions and 3 deletions

View File

@@ -1,4 +1,6 @@
use std::collections::HashMap;
// src/table_script/handlers/dependency_analyzer.rs
use std::collections::{HashMap, HashSet};
use tonic::Status;
use sqlx::PgPool;
use serde_json::{json, Value};
@@ -206,6 +208,8 @@ impl DependencyAnalyzer {
table_id: i64,
new_dependencies: &[Dependency],
) -> Result<(), DependencyError> {
// FIRST: Validate that structured table access respects link constraints
self.validate_link_constraints(tx, table_id, new_dependencies).await?;
// Get current dependency graph for this schema
let current_deps = sqlx::query!(
r#"SELECT sd.source_table_id, sd.target_table_id, st.table_name as source_name, tt.table_name as target_name
@@ -267,6 +271,75 @@ impl DependencyAnalyzer {
Ok(())
}
/// Validates that structured table access (steel_get_column functions) respects link constraints
/// Raw SQL access (steel_query_sql) is allowed to reference any table
///
/// Example:
/// - Table A is linked to Table B via table_definition_links
/// - Script for Table A can use: (steel_get_column "table_b" "column_name") ✅
/// - Script for Table A CANNOT use: (steel_get_column "table_c" "column_name") ❌
/// - Script for Table A CAN use: (steel_query_sql "SELECT * FROM table_c") ✅
async fn validate_link_constraints(
&self,
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
source_table_id: i64,
dependencies: &[Dependency],
) -> Result<(), DependencyError> {
// Get all valid linked tables for the source table
let linked_tables = sqlx::query!(
r#"SELECT td.table_name, tdl.is_required
FROM table_definition_links tdl
JOIN table_definitions td ON tdl.linked_table_id = td.id
WHERE tdl.source_table_id = $1"#,
source_table_id
)
.fetch_all(&mut **tx)
.await
.map_err(|e| DependencyError::DatabaseError { error: e.to_string() })?;
// Create a set of allowed table names for quick lookup
let allowed_tables: std::collections::HashSet<String> = linked_tables
.into_iter()
.map(|row| row.table_name)
.collect();
// Get the current table name for better error messages
let current_table_name = sqlx::query_scalar!(
"SELECT table_name FROM table_definitions WHERE id = $1",
source_table_id
)
.fetch_one(&mut **tx)
.await
.map_err(|e| DependencyError::DatabaseError { error: e.to_string() })?;
// Validate each dependency
for dep in dependencies {
match &dep.dependency_type {
// Structured access must respect link constraints
DependencyType::ColumnAccess { column } | DependencyType::IndexedAccess { column, .. } => {
if !allowed_tables.contains(&dep.target_table) {
return Err(DependencyError::InvalidTableReference {
table_name: dep.target_table.clone(),
script_context: format!(
"Table '{}' is not linked to '{}'. Add a link in the table definition to access '{}' via steel_get_column functions. Column attempted: '{}'",
dep.target_table,
current_table_name,
dep.target_table,
column
),
});
}
}
// Raw SQL access is unrestricted
DependencyType::SqlQuery { .. } => {
// No validation - raw SQL can access any table
}
}
}
Ok(())
}
/// Proper DFS-based cycle detection with state tracking
fn detect_cycles_dfs(
&self,

View File

@@ -1,4 +1,5 @@
// src/table_script/handlers/post_table_script.rs
// TODO MAKE THE SCRIPTS PUSH ONLY TO THE EMPTY FILES
use tonic::Status;
use sqlx::{PgPool, Error as SqlxError};
@@ -10,7 +11,6 @@ use crate::table_script::handlers::dependency_analyzer::DependencyAnalyzer;
const SYSTEM_COLUMNS: &[&str] = &["id", "deleted", "created_at"];
// TODO MAKE THE SCRIPTS PUSH ONLY TO THE EMPTY FILES
/// Validates the target column and ensures it is not a system column.
/// Returns the column type if valid.
fn validate_target_column(
@@ -159,7 +159,7 @@ fn generate_warnings(dependencies: &[crate::table_script::handlers::dependency_a
if sql_deps_count > 0 {
warnings.push(format!(
"Warning: Script contains {} SQL quer{}, ensure they are read-only and reference valid tables.",
"Warning: Script contains {} raw SQL quer{}, ensure they are read-only and reference valid tables.",
sql_deps_count,
if sql_deps_count == 1 { "y" } else { "ies" }
));
@@ -173,5 +173,22 @@ fn generate_warnings(dependencies: &[crate::table_script::handlers::dependency_a
));
}
// Count structured access dependencies
let structured_deps_count = dependencies.iter()
.filter(|d| matches!(
d.dependency_type,
crate::table_script::handlers::dependency_analyzer::DependencyType::ColumnAccess { .. } |
crate::table_script::handlers::dependency_analyzer::DependencyType::IndexedAccess { .. }
))
.count();
if structured_deps_count > 0 {
warnings.push(format!(
"Info: Script uses {} linked table{} via steel_get_column functions.",
structured_deps_count,
if structured_deps_count == 1 { "" } else { "s" }
));
}
warnings.join(" ")
}