This commit is contained in:
Priec
2026-08-08 12:40:42 +02:00
parent 5be1a5c661
commit 751bfd9226
12 changed files with 91 additions and 36 deletions

2
client

Submodule client updated: 8538beba2a...da1557a2d4

View File

@@ -53,16 +53,18 @@ message PostTableDefinitionRequest {
// Table name to create inside the target profile. // Table name to create inside the target profile.
// Must be lowercase, alphanumeric with underscores, // Must be lowercase, alphanumeric with underscores,
// start with a letter, and be <= 63 chars. // start with a letter, and be <= 63 chars.
// Forbidden names: "id", "deleted", "created_at", "row_revision", or ending in "_id". // Forbidden names: "id", "deleted", "created_at", "row_revision" -- the
// system columns, which share one namespace with table names wherever the
// two are named side by side. The "_id" suffix is allowed.
string table_name = 1; string table_name = 1;
// List of user-defined columns (adds to system/id/fk columns). // The table's columns, including its links. System columns are added on top.
repeated ColumnDefinition columns = 3; repeated ColumnDefinition columns = 3;
// List of column names to be indexed (must match existing user-defined columns). // Column names to index, matching names declared above. System columns
// Indexes can target only user-defined columns; system columns ("id", "deleted", // ("id", "deleted", "created_at", "row_revision") already have indexes, and
// "created_at", "row_revision") and automatically generated foreign key ("*_id") columns already // a LINK column is indexed when it is created. Requests naming either are
// have indexes. Requests trying to index those columns are rejected. // rejected.
repeated string indexes = 4; repeated string indexes = 4;
// Name of profile (Postgres schema) where the table will be created. // Name of profile (Postgres schema) where the table will be created.
@@ -142,9 +144,13 @@ enum MoneyRounding {
// Describes one user-defined column for a table. // Describes one user-defined column for a table.
message ColumnDefinition { message ColumnDefinition {
// Column name that follows the same validation rules as table_name. // Must be lowercase, start with a letter, and use only lowercase letters,
// Must be lowercase, start with a letter, no uppercase characters, // digits and underscores.
// and cannot be "id", "deleted", "created_at", "row_revision", or end with "_id". //
// Cannot be "id", "deleted", "created_at" or "row_revision": those are
// system columns, and a data request names system and user columns in one
// namespace. Any other name is free, including one ending in "_id" -- no
// column name is derived from a table name, so nothing collides.
string name = 1; string name = 1;
// Logical column type. Supported values (case-insensitive): // Logical column type. Supported values (case-insensitive):

View File

@@ -181,6 +181,16 @@ message ScriptDependency {
string operation = 4; string operation = 4;
// Relationship table used to match the owner row to the related collection. // Relationship table used to match the owner row to the related collection.
string via_table = 5; string via_table = 5;
// Column of the current row holding the referenced row's id: the link this
// dependency travels, resolved when the script was saved. Empty when no link
// is travelled -- a self-reference, or an aggregate, which follows a path
// instead. A table may link to one target several times, so this is the only
// thing that says which link was meant; never derive it from target_table.
string link_column = 6;
// The first argument exactly as the script wrote it: a link alias, or the
// target table's name where one link makes that unambiguous. The Steel
// runtime looks its inputs up by this text.
string name_in_script = 7;
} }
// Identifies the active form row whose external Steel inputs must be hydrated. // Identifies the active form row whose external Steel inputs must be hydrated.

View File

@@ -46,7 +46,7 @@ message GetTableStructureResponse {
message TableStructureResponse { message TableStructureResponse {
// Columns of the physical table, including system columns (id, deleted, // Columns of the physical table, including system columns (id, deleted,
// created_at, row_revision), user-defined columns, and any foreign-key columns such as // created_at, row_revision), user-defined columns, and any foreign-key columns such as
// "<linked_table>_id", plus the dedicated "account_id" column on // named by whoever declared each link, plus the dedicated "account_id" on
// ACCOUNTING-enabled tables. May be empty if the physical table is missing. // ACCOUNTING-enabled tables. May be empty if the physical table is missing.
repeated TableColumn columns = 1; repeated TableColumn columns = 1;
} }

View File

@@ -62,7 +62,7 @@ service TablesData {
// - Validates profile and table definition // - Validates profile and table definition
// - Returns all columns as strings (COALESCE(col::TEXT, '') AS col) // - Returns all columns as strings (COALESCE(col::TEXT, '') AS col)
// including: id, deleted, row_revision, all user-defined columns, and FK columns // including: id, deleted, row_revision, all user-defined columns, and FK columns
// named "<linked_table>_id" for each table link, plus "account_id" on // named by whoever declared each link, plus "account_id" on
// ACCOUNTING-enabled tables // ACCOUNTING-enabled tables
// - Fails with NOT_FOUND if record does not exist or is soft-deleted // - Fails with NOT_FOUND if record does not exist or is soft-deleted
// - If the physical table is missing but the definition exists, returns INTERNAL // - If the physical table is missing but the definition exists, returns INTERNAL
@@ -101,7 +101,7 @@ message PostTableDataRequest {
// - User-defined columns from the table definition // - User-defined columns from the table definition
// - System/FK columns: // - System/FK columns:
// • "deleted" (BOOLEAN), optional; default FALSE if not provided // • "deleted" (BOOLEAN), optional; default FALSE if not provided
// • "<linked_table>_id" (BIGINT) for each table link // • one BIGINT per link, named by whoever declared it
// • "account_id" (BIGINT) on ACCOUNTING-enabled tables // • "account_id" (BIGINT) on ACCOUNTING-enabled tables
// //
// Type expectations by SQL type: // Type expectations by SQL type:
@@ -250,7 +250,7 @@ message GetTableDataResponse {
// Map of column_name → stringified value for: // Map of column_name → stringified value for:
// - id, deleted // - id, deleted
// - all user-defined columns from the table definition // - all user-defined columns from the table definition
// - FK columns named "<linked_table>_id" for each table link // - one column per link, named by whoever declared it
// - account_id for ACCOUNTING-enabled tables // - account_id for ACCOUNTING-enabled tables
// //
// All values are returned as TEXT via col::TEXT and COALESCEed to empty string // All values are returned as TEXT via col::TEXT and COALESCEed to empty string

Binary file not shown.

View File

@@ -6,16 +6,18 @@ pub struct PostTableDefinitionRequest {
/// Table name to create inside the target profile. /// Table name to create inside the target profile.
/// Must be lowercase, alphanumeric with underscores, /// Must be lowercase, alphanumeric with underscores,
/// start with a letter, and be \<= 63 chars. /// start with a letter, and be \<= 63 chars.
/// Forbidden names: "id", "deleted", "created_at", "row_revision", or ending in "\_id". /// Forbidden names: "id", "deleted", "created_at", "row_revision" -- the
/// system columns, which share one namespace with table names wherever the
/// two are named side by side. The "\_id" suffix is allowed.
#[prost(string, tag = "1")] #[prost(string, tag = "1")]
pub table_name: ::prost::alloc::string::String, pub table_name: ::prost::alloc::string::String,
/// List of user-defined columns (adds to system/id/fk columns). /// The table's columns, including its links. System columns are added on top.
#[prost(message, repeated, tag = "3")] #[prost(message, repeated, tag = "3")]
pub columns: ::prost::alloc::vec::Vec<ColumnDefinition>, pub columns: ::prost::alloc::vec::Vec<ColumnDefinition>,
/// List of column names to be indexed (must match existing user-defined columns). /// Column names to index, matching names declared above. System columns
/// Indexes can target only user-defined columns; system columns ("id", "deleted", /// ("id", "deleted", "created_at", "row_revision") already have indexes, and
/// "created_at", "row_revision") and automatically generated foreign key ("\*\_id") columns already /// a LINK column is indexed when it is created. Requests naming either are
/// have indexes. Requests trying to index those columns are rejected. /// rejected.
#[prost(string, repeated, tag = "4")] #[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>,
/// Name of profile (Postgres schema) where the table will be created. /// Name of profile (Postgres schema) where the table will be created.
@@ -106,9 +108,13 @@ pub struct AddTableColumnsRequest {
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct ColumnDefinition { pub struct ColumnDefinition {
/// Column name that follows the same validation rules as table_name. /// Must be lowercase, start with a letter, and use only lowercase letters,
/// Must be lowercase, start with a letter, no uppercase characters, /// digits and underscores.
/// and cannot be "id", "deleted", "created_at", "row_revision", or end with "\_id". ///
/// Cannot be "id", "deleted", "created_at" or "row_revision": those are
/// system columns, and a data request names system and user columns in one
/// namespace. Any other name is free, including one ending in "\_id" -- no
/// column name is derived from a table name, so nothing collides.
#[prost(string, tag = "1")] #[prost(string, tag = "1")]
pub name: ::prost::alloc::string::String, pub name: ::prost::alloc::string::String,
/// Logical column type. Supported values (case-insensitive): /// Logical column type. Supported values (case-insensitive):

View File

@@ -127,6 +127,18 @@ pub struct ScriptDependency {
/// Relationship table used to match the owner row to the related collection. /// Relationship table used to match the owner row to the related collection.
#[prost(string, tag = "5")] #[prost(string, tag = "5")]
pub via_table: ::prost::alloc::string::String, pub via_table: ::prost::alloc::string::String,
/// Column of the current row holding the referenced row's id: the link this
/// dependency travels, resolved when the script was saved. Empty when no link
/// is travelled -- a self-reference, or an aggregate, which follows a path
/// instead. A table may link to one target several times, so this is the only
/// thing that says which link was meant; never derive it from target_table.
#[prost(string, tag = "6")]
pub link_column: ::prost::alloc::string::String,
/// The first argument exactly as the script wrote it: a link alias, or the
/// target table's name where one link makes that unambiguous. The Steel
/// runtime looks its inputs up by this text.
#[prost(string, tag = "7")]
pub name_in_script: ::prost::alloc::string::String,
} }
/// Identifies the active form row whose external Steel inputs must be hydrated. /// Identifies the active form row whose external Steel inputs must be hydrated.
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]

View File

@@ -26,7 +26,7 @@ pub struct GetTableStructureResponse {
pub struct TableStructureResponse { pub struct TableStructureResponse {
/// Columns of the physical table, including system columns (id, deleted, /// Columns of the physical table, including system columns (id, deleted,
/// created_at, row_revision), user-defined columns, and any foreign-key columns such as /// created_at, row_revision), user-defined columns, and any foreign-key columns such as
/// "\<linked_table>\_id", plus the dedicated "account_id" column on /// named by whoever declared each link, plus the dedicated "account_id" on
/// ACCOUNTING-enabled tables. May be empty if the physical table is missing. /// ACCOUNTING-enabled tables. May be empty if the physical table is missing.
#[prost(message, repeated, tag = "1")] #[prost(message, repeated, tag = "1")]
pub columns: ::prost::alloc::vec::Vec<TableColumn>, pub columns: ::prost::alloc::vec::Vec<TableColumn>,

View File

@@ -17,7 +17,7 @@ pub struct PostTableDataRequest {
/// * User-defined columns from the table definition /// * User-defined columns from the table definition
/// * System/FK columns: /// * System/FK columns:
/// • "deleted" (BOOLEAN), optional; default FALSE if not provided /// • "deleted" (BOOLEAN), optional; default FALSE if not provided
/// • "\<linked_table>\_id" (BIGINT) for each table link /// • one BIGINT per link, named by whoever declared it
/// • "account_id" (BIGINT) on ACCOUNTING-enabled tables /// • "account_id" (BIGINT) on ACCOUNTING-enabled tables
/// ///
/// Type expectations by SQL type: /// Type expectations by SQL type:
@@ -190,7 +190,7 @@ pub struct GetTableDataResponse {
/// ///
/// * id, deleted /// * id, deleted
/// * all user-defined columns from the table definition /// * all user-defined columns from the table definition
/// * FK columns named "\<linked_table>\_id" for each table link /// * one column per link, named by whoever declared it
/// * account_id for ACCOUNTING-enabled tables /// * account_id for ACCOUNTING-enabled tables
/// ///
/// All values are returned as TEXT via col::TEXT and COALESCEed to empty string /// All values are returned as TEXT via col::TEXT and COALESCEed to empty string
@@ -482,7 +482,7 @@ pub mod tables_data_client {
/// * Validates profile and table definition /// * Validates profile and table definition
/// * Returns all columns as strings (COALESCE(col::TEXT, '') AS col) /// * Returns all columns as strings (COALESCE(col::TEXT, '') AS col)
/// including: id, deleted, row_revision, all user-defined columns, and FK columns /// including: id, deleted, row_revision, all user-defined columns, and FK columns
/// named "\<linked_table>\_id" for each table link, plus "account_id" on /// named by whoever declared each link, plus "account_id" on
/// ACCOUNTING-enabled tables /// ACCOUNTING-enabled tables
/// * Fails with NOT_FOUND if record does not exist or is soft-deleted /// * Fails with NOT_FOUND if record does not exist or is soft-deleted
/// * If the physical table is missing but the definition exists, returns INTERNAL /// * If the physical table is missing but the definition exists, returns INTERNAL
@@ -675,7 +675,7 @@ pub mod tables_data_server {
/// * Validates profile and table definition /// * Validates profile and table definition
/// * Returns all columns as strings (COALESCE(col::TEXT, '') AS col) /// * Returns all columns as strings (COALESCE(col::TEXT, '') AS col)
/// including: id, deleted, row_revision, all user-defined columns, and FK columns /// including: id, deleted, row_revision, all user-defined columns, and FK columns
/// named "\<linked_table>\_id" for each table link, plus "account_id" on /// named by whoever declared each link, plus "account_id" on
/// ACCOUNTING-enabled tables /// ACCOUNTING-enabled tables
/// * Fails with NOT_FOUND if record does not exist or is soft-deleted /// * Fails with NOT_FOUND if record does not exist or is soft-deleted
/// * If the physical table is missing but the definition exists, returns INTERNAL /// * If the physical table is missing but the definition exists, returns INTERNAL

2
server

Submodule server updated: 79e64a2bd5...c5ff2c81f1

View File

@@ -670,12 +670,11 @@ pub(crate) fn validate_identifier(
"{label} may only use lowercase letters, digits and underscores." "{label} may only use lowercase letters, digits and underscores."
)); ));
} }
// Only the system columns are reserved. The `_id` suffix is free: no
// column name is derived from a table name any more, so it collides with
// nothing.
if reject_table_reserved if reject_table_reserved
&& (value == "id" && matches!(value, "id" | "deleted" | "created_at" | "row_revision")
|| value == "deleted"
|| value == "created_at"
|| value == "row_revision"
|| value.ends_with("_id"))
{ {
return Some(format!("{label} uses a reserved name.")); return Some(format!("{label} uses a reserved name."));
} }
@@ -1127,8 +1126,6 @@ pub(crate) mod tests {
draft.name_input = "Total".to_string(); draft.name_input = "Total".to_string();
assert!(draft.add_from_inputs().is_err()); assert!(draft.add_from_inputs().is_err());
draft.name_input = "customer_id".to_string();
assert!(draft.add_from_inputs().is_err());
draft.name_input = "created_at".to_string(); draft.name_input = "created_at".to_string();
assert!(draft.add_from_inputs().is_err()); assert!(draft.add_from_inputs().is_err());
@@ -1334,3 +1331,27 @@ pub(crate) mod tests {
assert_eq!(draft.selected_index_names(), vec!["number"]); assert_eq!(draft.selected_index_names(), vec!["number"]);
} }
} }
#[cfg(test)]
mod link_alias_tests {
use super::validate_identifier;
/// The `_id` suffix is an ordinary part of a column's name: nothing is
/// derived from a table name any more.
#[test]
fn a_column_name_may_end_in_id() {
assert_eq!(validate_identifier("external_id", "Column name", true), None);
}
/// The system columns stay reserved, since they share one namespace with
/// user columns in a data request.
#[test]
fn the_system_columns_stay_reserved() {
for name in ["id", "deleted", "created_at", "row_revision"] {
assert!(
validate_identifier(name, "Column name", true).is_some(),
"`{name}` must stay reserved"
);
}
}
}