diff --git a/common/.gitignore b/common/.gitignore new file mode 100644 index 0000000..77f12ae --- /dev/null +++ b/common/.gitignore @@ -0,0 +1 @@ +docs/ diff --git a/common/Makefile b/common/Makefile new file mode 100644 index 0000000..dde423c --- /dev/null +++ b/common/Makefile @@ -0,0 +1,11 @@ +DOC_OUT := docs/grpc_reference.html + +.PHONY: docs + +docs: + @echo "Generating gRPC documentation..." + mkdir -p $(dir $(DOC_OUT)) + protoc \ + --doc_out=html,index.html:$(dir $(DOC_OUT)) \ + --proto_path=proto proto/*.proto + @echo "✅ Docs written to $(DOC_OUT)" diff --git a/common/proto/table_definition.proto b/common/proto/table_definition.proto index d5ac6a5..91effd8 100644 --- a/common/proto/table_definition.proto +++ b/common/proto/table_definition.proto @@ -4,56 +4,139 @@ package komp_ac.table_definition; import "common.proto"; +// The TableDefinition service manages the entire lifecycle of user-defined +// tables (stored as both metadata and physical PostgreSQL tables) inside +// logical "profiles" (schemas). Each table has stored structure, links, and +// validation rules. service TableDefinition { - rpc PostTableDefinition (PostTableDefinitionRequest) returns (TableDefinitionResponse); - rpc GetProfileTree (komp_ac.common.Empty) returns (ProfileTreeResponse); - rpc DeleteTable (DeleteTableRequest) returns (DeleteTableResponse); + // Creates a new table (and schema if missing) with system columns, + // linked-table foreign keys, user-defined columns, and optional indexes. + // Also inserts metadata and default validation rules. Entirely transactional. + rpc PostTableDefinition(PostTableDefinitionRequest) + returns (TableDefinitionResponse); + + // Lists all profiles (schemas) and their tables with declared dependencies. + // This provides a tree-like overview of table relationships. + rpc GetProfileTree(komp_ac.common.Empty) + returns (ProfileTreeResponse); + + // Drops a table and its metadata, then deletes the profile if it becomes empty. + rpc DeleteTable(DeleteTableRequest) + returns (DeleteTableResponse); } +// A single link to another table within the same profile (schema). message TableLink { - string linked_table_name = 1; - bool required = 2; + // Name of an existing table within the same profile to link to. + // For each link, a "_id" column is created on the new table. + // That column references ""(id) and adds an index automatically. + string linked_table_name = 1; + + // If true, the generated foreign key column is NOT NULL. + // Otherwise the column allows NULL. + // Duplicate links to the same target table in one request are rejected. + bool required = 2; } +// Defines the input for creating a new table definition. message PostTableDefinitionRequest { - string table_name = 1; - repeated TableLink links = 2; - repeated ColumnDefinition columns = 3; - repeated string indexes = 4; - string profile_name = 5; + // Table name to create inside the target profile. + // Must be lowercase, alphanumeric with underscores, + // start with a letter, and be <= 63 chars. + // Forbidden names: "id", "deleted", "created_at", or ending in "_id". + string table_name = 1; + + // List of links (foreign keys) to existing tables in the same profile. + // Each will automatically get a "_id" column and an index. + repeated TableLink links = 2; + + // List of user-defined columns (adds to system/id/fk columns). + repeated ColumnDefinition columns = 3; + + // List of column names to be indexed (must match existing user-defined columns). + // Indexes can target only user-defined columns; system columns ("id", "deleted", + // "created_at") and automatically generated foreign key ("*_id") columns already + // have indexes. Requests trying to index those columns are rejected. + repeated string indexes = 4; + + // Name of profile (Postgres schema) where the table will be created. + // Same naming rules as table_name; cannot collide with reserved schemas + // like "public", "information_schema", or ones starting with "pg_". + string profile_name = 5; } +// Describes one user-defined column for a table. message ColumnDefinition { - string name = 1; - string field_type = 2; + // Column name that follows the same validation rules as table_name. + // Must be lowercase, start with a letter, no uppercase characters, + // and cannot be "id", "deleted", "created_at", or end with "_id". + string name = 1; + + // Logical column type. Supported values (case-insensitive): + // TEXT / STRING + // BOOLEAN + // TIMESTAMP / TIMESTAMPTZ / TIME + // MONEY (= NUMERIC(14,4)) + // INTEGER / INT + // BIGINTEGER / BIGINT + // DATE + // DECIMAL(p,s) → NUMERIC(p,s) + // DECIMAL args must be integers (no sign, no dot, no leading zeros); + // s ≤ p and p ≥ 1. + string field_type = 2; } +// Response after table creation (success + DDL preview). message TableDefinitionResponse { - bool success = 1; - string sql = 2; + // True if all DB changes and metadata inserts succeeded. + bool success = 1; + + // The actual SQL executed: CREATE TABLE + CREATE INDEX statements. + string sql = 2; } +// Describes the tree of all profiles and their tables. message ProfileTreeResponse { - message Table { - int64 id = 1; - string name = 2; - repeated string depends_on = 3; - } + // Table entry in a profile. + message Table { + // Internal ID from table_definitions.id (metadata record). + int64 id = 1; - message Profile { - string name = 1; - repeated Table tables = 2; - } + // Table name within the profile (schema). + string name = 2; - repeated Profile profiles = 1; + // Other tables this one references (based on link definitions only). + repeated string depends_on = 3; + } + + // Profile (schema) entry. + message Profile { + // Name of the schema/profile (as stored in `schemas.name`). + string name = 1; + + // All tables in that schema and their dependencies. + repeated Table tables = 2; + } + + // All profiles in the system. + repeated Profile profiles = 1; } +// Request to delete one table definition entirely. message DeleteTableRequest { - string profile_name = 1; - string table_name = 2; + // Profile (schema) name owning the table (must exist). + string profile_name = 1; + + // Table to drop (must exist in the profile). + // Executes DROP TABLE "profile"."table" CASCADE and then removes metadata. + string table_name = 2; } +// Response after table deletion. message DeleteTableResponse { - bool success = 1; - string message = 2; + // True if table and metadata were successfully deleted in one transaction. + bool success = 1; + + // Human-readable summary of what was removed. + string message = 2; } diff --git a/common/proto/table_script.proto b/common/proto/table_script.proto index 4980f85..b3ea63e 100644 --- a/common/proto/table_script.proto +++ b/common/proto/table_script.proto @@ -1,18 +1,101 @@ +// common/proto/table_script.proto syntax = "proto3"; package komp_ac.table_script; +// Manages column-computation scripts for user-defined tables. +// Each script belongs to a single table (table_definition_id) and populates +// exactly one target column in that table. The server: +// - Validates script syntax (non-empty, balanced parentheses, starts with '(') +// - Validates the target column (exists, not a system column, allowed type) +// - Validates column/type usage inside math expressions +// - Validates referenced tables/columns against the schema +// - Enforces link constraints for structured access (see notes below) +// - Analyzes dependencies and prevents cycles across the schema +// - Transforms the script to decimal-safe math (steel_decimal) +// - Upserts into table_scripts and records dependencies in script_dependencies +// The whole operation is transactional. service TableScript { - rpc PostTableScript(PostTableScriptRequest) returns (TableScriptResponse); + // Create or update a script for a specific table and target column. + // + // Behavior: + // - Fetches the table by table_definition_id (must exist) + // - Validates "script" (syntax), "target_column" (exists and type rules), + // and all referenced tables/columns (must exist in same schema) + // - Validates math operations: prohibits using certain data types in math + // - Enforces link constraints for structured table access: + // • Allowed always: self-references (same table) + // • Structured access via steel_get_column / steel_get_column_with_index + // requires an explicit link in table_definition_links + // • Raw SQL access via steel_query_sql is permitted (still validated) + // - Detects and rejects circular dependencies across all scripts in the schema + // (self-references are allowed and not treated as cycles) + // - Transforms the script to decimal-safe operations (steel_decimal) + // - UPSERTS into table_scripts on (table_definitions_id, target_column) + // and saves a normalized dependency list into script_dependencies + rpc PostTableScript(PostTableScriptRequest) returns (TableScriptResponse); } +// Request to create or update a script bound to a specific table and column. message PostTableScriptRequest { - int64 table_definition_id = 1; - string target_column = 2; - string script = 3; - string description = 4; + // Required. The metadata ID from table_definitions.id that identifies the + // table this script belongs to. The table must exist; its schema determines + // where referenced tables/columns are validated and where dependencies are stored. + int64 table_definition_id = 1; + + // Required. The target column in the target table that this script computes. + // Must be an existing user-defined column in that table (not a system column). + // System columns are reserved: "id", "deleted", "created_at". + // The column's data type must NOT be one of the prohibited target types: + // BIGINT, DATE, TIMESTAMPTZ + // Note: BOOLEAN targets are allowed (values are converted to Steel #true/#false). + string target_column = 2; + + // Required. The script in the Steel DSL (S-expression style). + // Syntax requirements: + // - Non-empty, must start with '(' + // - Balanced parentheses + // + // Referencing data: + // - Structured table/column access (enforces link constraints): + // (steel_get_column "table_name" "column_name") + // (steel_get_column_with_index "table_name" index "column_name") + // • index must be a non-negative integer literal + // • self-references are allowed without links + // • other tables require an explicit link from the source table + // (table_definition_links) or the request fails + // - Raw SQL access (no link required, but still validated): + // (steel_query_sql "SELECT ...") + // • Basic checks disallow operations that imply prohibited types, + // e.g., EXTRACT(…), DATE_PART(…), ::DATE, ::TIMESTAMPTZ, ::BIGINT, CAST(…) + // - Self variable access in transformed scripts: + // (get-var "column_name") is treated as referencing the current table + // + // Math operations: + // - The script is transformed by steel_decimal; supported math forms include: + // +, -, *, /, ^, **, pow, sqrt, >, <, =, >=, <=, min, max, abs, round, + // ln, log, log10, exp, sin, cos, tan + // - Columns of the following types CANNOT be used inside math expressions: + // BIGINT, TEXT, BOOLEAN, DATE, TIMESTAMPTZ + // + // Dependency tracking and cycles: + // - Dependencies are extracted from steel_get_column(_with_index), get-var, + // and steel_query_sql and stored in script_dependencies with context + // - Cycles across tables are rejected (self-dependency is allowed) + string script = 3; + + // Optional. Free-text description stored alongside the script (no functional effect). + string description = 4; } +// Response after creating or updating a script. message TableScriptResponse { - int64 id = 1; - string warnings = 2; + // The ID of the script record in table_scripts (new or existing on upsert). + int64 id = 1; + + // Human-readable warnings concatenated into a single string. Possible messages: + // - Warning if the script references itself (may affect first population) + // - Count of raw SQL queries present + // - Info about number of structured linked-table accesses + // - Warning if many dependencies may affect performance + string warnings = 2; } diff --git a/common/proto/table_structure.proto b/common/proto/table_structure.proto index e9d2932..ac45f6c 100644 --- a/common/proto/table_structure.proto +++ b/common/proto/table_structure.proto @@ -1,25 +1,70 @@ -// proto/table_structure.proto +// common/proto/table_structure.proto syntax = "proto3"; package komp_ac.table_structure; import "common.proto"; -message GetTableStructureRequest { - string profile_name = 1; // e.g., "default" - string table_name = 2; // e.g., "2025_adresar6" +// Introspects the physical PostgreSQL table for a given logical table +// (defined in table_definitions) and returns its column structure. +// The server validates that: +// - The profile (schema) exists in `schemas` +// - The table is defined for that profile in `table_definitions` +// It then queries information_schema for the physical table and returns +// normalized column metadata. If the physical table is missing despite +// a definition, the response may contain an empty `columns` list. +service TableStructureService { + // Return the physical column list (name, normalized data_type, + // nullability, primary key flag) for a table in a profile. + // + // Behavior: + // - NOT_FOUND if profile doesn't exist in `schemas` + // - NOT_FOUND if table not defined for that profile in `table_definitions` + // - Queries information_schema.columns ordered by ordinal position + // - Normalizes data_type text (details under TableColumn.data_type) + // - Returns an empty list if the table is validated but has no visible + // columns in information_schema (e.g., physical table missing) + rpc GetTableStructure(GetTableStructureRequest) + returns (TableStructureResponse); } +// Request identifying the profile (schema) and table to inspect. +message GetTableStructureRequest { + // Required. Profile (PostgreSQL schema) name. Must exist in `schemas`. + string profile_name = 1; + + // Required. Table name within the profile. Must exist in `table_definitions` + // for the given profile. The physical table is then introspected via + // information_schema. + string table_name = 2; +} + +// Response with the ordered list of columns (by ordinal position). message TableStructureResponse { + // Columns of the physical table, including system columns (id, deleted, + // created_at), user-defined columns, and any foreign-key columns such as + // "_id". May be empty if the physical table is missing. repeated TableColumn columns = 1; } +// One physical column entry as reported by information_schema. message TableColumn { + // Column name exactly as defined in PostgreSQL. string name = 1; - string data_type = 2; // e.g., "TEXT", "BIGINT", "VARCHAR(255)", "TIMESTAMPTZ" + + // Normalized data type string derived from information_schema: + // - VARCHAR(n) when udt_name='varchar' with character_maximum_length + // - CHAR(n) when udt_name='bpchar' with character_maximum_length + // - NUMERIC(p,s) when udt_name='numeric' with precision and scale + // - NUMERIC(p) when udt_name='numeric' with precision only + // - [] for array types (udt_name starting with '_', e.g., INT[] ) + // - Otherwise UPPER(udt_name), e.g., TEXT, BIGINT, TIMESTAMPTZ + // Examples: "TEXT", "BIGINT", "VARCHAR(255)", "TIMESTAMPTZ", "NUMERIC(14,4)" + string data_type = 2; + + // True if information_schema reports the column as nullable. bool is_nullable = 3; + + // True if the column is part of the table's PRIMARY KEY. + // Typically true for the "id" column created by the system. bool is_primary_key = 4; } - -service TableStructureService { - rpc GetTableStructure (GetTableStructureRequest) returns (TableStructureResponse); -} diff --git a/common/proto/tables_data.proto b/common/proto/tables_data.proto index 9e9fd5a..b0a72c0 100644 --- a/common/proto/tables_data.proto +++ b/common/proto/tables_data.proto @@ -5,67 +5,220 @@ package komp_ac.tables_data; import "common.proto"; import "google/protobuf/struct.proto"; +// Read and write row data for user-defined tables inside profiles (schemas). +// Operations are performed against the physical PostgreSQL table that +// corresponds to the logical table definition and are scoped by profile +// (schema). Deletions are soft (set deleted = true). Typed binding and +// script-based validation are enforced consistently. service TablesData { - rpc PostTableData (PostTableDataRequest) returns (PostTableDataResponse); - rpc PutTableData (PutTableDataRequest) returns (PutTableDataResponse); - rpc DeleteTableData (DeleteTableDataRequest) returns (DeleteTableDataResponse); + // Insert a new row into a table with strict type binding and script validation. + // + // Behavior: + // - Validates that profile (schema) exists and table is defined for it + // - Validates provided columns exist (user-defined or allowed system/FK columns) + // - For columns targeted by scripts in this table, the client MUST provide the + // value, and it MUST equal the script’s calculated value (compared type-safely) + // - Binds values with correct SQL types, rejects invalid formats/ranges + // - Inserts the row and returns the new id; queues search indexing (best effort) + // - If the physical table is missing but the definition exists, returns INTERNAL + rpc PostTableData(PostTableDataRequest) returns (PostTableDataResponse); + + // Update existing row data with strict type binding and script validation. + // + // Behavior: + // - Validates profile and table, and that the record exists + // - If request data is empty, returns success without changing the row + // - For columns targeted by scripts: + // • If included in update, provided value must equal the script result + // • If not included, update must not cause the script result to differ + // from the current stored value; otherwise FAILED_PRECONDITION is returned + // - Binds values with correct SQL types; rejects invalid formats/ranges + // - Updates the row and returns the id; queues search indexing (best effort) + rpc PutTableData(PutTableDataRequest) returns (PutTableDataResponse); + + // Soft-delete a single record (sets deleted = true) if it exists and is not already deleted. + // + // Behavior: + // - Validates profile and table definition + // - Updates only rows with deleted = false + // - success = true means a row was actually changed; false means nothing to delete + // - If the physical table is missing but the definition exists, returns INTERNAL + rpc DeleteTableData(DeleteTableDataRequest) returns (DeleteTableDataResponse); + + // Fetch a single non-deleted row by id as textified values. + // + // Behavior: + // - Validates profile and table definition + // - Returns all columns as strings (COALESCE(col::TEXT, '') AS col) + // including: id, deleted, all user-defined columns, and FK columns + // named "_id" for each table link + // - 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 rpc GetTableData(GetTableDataRequest) returns (GetTableDataResponse); + + // Count non-deleted rows in a table. + // + // Behavior: + // - Validates profile and table definition + // - Returns komp_ac.common.CountResponse.count with rows where deleted = FALSE + // - If the physical table is missing but the definition exists, returns INTERNAL rpc GetTableDataCount(GetTableDataCountRequest) returns (komp_ac.common.CountResponse); + + // Fetch the N-th non-deleted row by id order (1-based), then return its full data. + // + // Behavior: + // - position is 1-based (position = 1 → first row by id ASC with deleted = FALSE) + // - Returns NOT_FOUND if position is out of bounds + // - Otherwise identical to GetTableData for the selected id rpc GetTableDataByPosition(GetTableDataByPositionRequest) returns (GetTableDataResponse); } +// Insert a new row. message PostTableDataRequest { + // Required. Profile (PostgreSQL schema) name that owns the table. + // Must exist in the schemas table. string profile_name = 1; + + // Required. Logical table (definition) name within the profile. + // Must exist in table_definitions for the given profile. string table_name = 2; + + // Required. Key-value data for columns to insert. + // + // Allowed keys: + // - User-defined columns from the table definition + // - System/FK columns: + // • "deleted" (BOOLEAN), optional; default FALSE if not provided + // • "_id" (BIGINT) for each table link + // + // Type expectations by SQL type: + // - TEXT: string value; empty string is treated as NULL + // - BOOLEAN: bool value + // - TIMESTAMPTZ: ISO 8601/RFC 3339 string (parsed to TIMESTAMPTZ) + // - INTEGER: number with no fractional part and within i32 range + // - BIGINT: number with no fractional part and within i64 range + // - NUMERIC(p,s): string representation only; empty string becomes NULL + // (numbers for NUMERIC are rejected to avoid precision loss) + // + // Script validation rules: + // - If a script exists for a target column, that column MUST be present here, + // and its provided value MUST equal the script’s computed value (type-aware + // comparison, e.g., decimals are compared numerically). + // + // Notes: + // - Unknown/invalid column names are rejected + // - Some application-specific validations may apply (e.g., max length for + // certain fields like "telefon") map data = 3; } +// Insert response. message PostTableDataResponse { + // True if the insert succeeded. bool success = 1; + + // Human-readable message. string message = 2; + + // The id of the inserted row. int64 inserted_id = 3; } +// Update an existing row. message PutTableDataRequest { + // Required. Profile (schema) name. string profile_name = 1; + + // Required. Table name within the profile. string table_name = 2; + + // Required. Id of the row to update. int64 id = 3; + + // Required. Columns to update (same typing rules as PostTableDataRequest.data). + // + // Special script rules: + // - If a script targets column X and X is included here, the value for X must + // equal the script’s result (type-aware). + // - If X is not included here but the update would cause the script’s result + // to change compared to the current stored value, the update is rejected with + // FAILED_PRECONDITION, instructing the caller to include X explicitly. + // + // Passing an empty map results in a no-op success response. map data = 4; } +// Update response. message PutTableDataResponse { + // True if the update succeeded (or no-op on empty data). bool success = 1; + + // Human-readable message. string message = 2; + + // The id of the updated row. int64 updated_id = 3; } +// Soft-delete a single row. message DeleteTableDataRequest { + // Required. Profile (schema) name. string profile_name = 1; + + // Required. Table name within the profile. string table_name = 2; + + // Required. Row id to soft-delete. int64 record_id = 3; } +// Soft-delete response. message DeleteTableDataResponse { + // True if a row was marked deleted (id existed and was not already deleted). bool success = 1; } +// Fetch a single non-deleted row by id. message GetTableDataRequest { + // Required. Profile (schema) name. string profile_name = 1; + + // Required. Table name within the profile. string table_name = 2; + + // Required. Id of the row to fetch. int64 id = 3; } +// Row payload: all columns returned as strings. message GetTableDataResponse { + // Map of column_name → stringified value for: + // - id, deleted + // - all user-defined columns from the table definition + // - FK columns named "_id" for each table link + // + // All values are returned as TEXT via col::TEXT and COALESCEed to empty string + // (NULL becomes ""). The row is returned only if deleted = FALSE. map data = 1; } +// Count non-deleted rows. message GetTableDataCountRequest { - string profile_name = 1; - string table_name = 2; + // Required. Profile (schema) name. + string profile_name = 1; + + // Required. Table name within the profile. + string table_name = 2; } +// Fetch by ordinal position among non-deleted rows (1-based). message GetTableDataByPositionRequest { + // Required. Profile (schema) name. string profile_name = 1; + + // Required. Table name within the profile. string table_name = 2; + + // Required. 1-based position by id ascending among rows with deleted = FALSE. int32 position = 3; } diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index 61967eb..a093404 100644 Binary files a/common/src/proto/descriptor.bin and b/common/src/proto/descriptor.bin differ diff --git a/common/src/proto/komp_ac.table_definition.rs b/common/src/proto/komp_ac.table_definition.rs index 71137c6..e90dd30 100644 --- a/common/src/proto/komp_ac.table_definition.rs +++ b/common/src/proto/komp_ac.table_definition.rs @@ -1,77 +1,133 @@ // This file is @generated by prost-build. +/// A single link to another table within the same profile (schema). #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct TableLink { + /// Name of an existing table within the same profile to link to. + /// For each link, a "_id" column is created on the new table. + /// That column references ""(id) and adds an index automatically. #[prost(string, tag = "1")] pub linked_table_name: ::prost::alloc::string::String, + /// If true, the generated foreign key column is NOT NULL. + /// Otherwise the column allows NULL. + /// Duplicate links to the same target table in one request are rejected. #[prost(bool, tag = "2")] pub required: bool, } +/// Defines the input for creating a new table definition. #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct PostTableDefinitionRequest { + /// Table name to create inside the target profile. + /// Must be lowercase, alphanumeric with underscores, + /// start with a letter, and be <= 63 chars. + /// Forbidden names: "id", "deleted", "created_at", or ending in "_id". #[prost(string, tag = "1")] pub table_name: ::prost::alloc::string::String, + /// List of links (foreign keys) to existing tables in the same profile. + /// Each will automatically get a "_id" column and an index. #[prost(message, repeated, tag = "2")] pub links: ::prost::alloc::vec::Vec, + /// List of user-defined columns (adds to system/id/fk columns). #[prost(message, repeated, tag = "3")] pub columns: ::prost::alloc::vec::Vec, + /// List of column names to be indexed (must match existing user-defined columns). + /// Indexes can target only user-defined columns; system columns ("id", "deleted", + /// "created_at") and automatically generated foreign key ("*_id") columns already + /// have indexes. Requests trying to index those columns are rejected. #[prost(string, repeated, tag = "4")] pub indexes: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, + /// Name of profile (Postgres schema) where the table will be created. + /// Same naming rules as table_name; cannot collide with reserved schemas + /// like "public", "information_schema", or ones starting with "pg_". #[prost(string, tag = "5")] pub profile_name: ::prost::alloc::string::String, } +/// Describes one user-defined column for a table. #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ColumnDefinition { + /// Column name that follows the same validation rules as table_name. + /// Must be lowercase, start with a letter, no uppercase characters, + /// and cannot be "id", "deleted", "created_at", or end with "_id". #[prost(string, tag = "1")] pub name: ::prost::alloc::string::String, + /// Logical column type. Supported values (case-insensitive): + /// TEXT / STRING + /// BOOLEAN + /// TIMESTAMP / TIMESTAMPTZ / TIME + /// MONEY (= NUMERIC(14,4)) + /// INTEGER / INT + /// BIGINTEGER / BIGINT + /// DATE + /// DECIMAL(p,s) → NUMERIC(p,s) + /// DECIMAL args must be integers (no sign, no dot, no leading zeros); + /// s ≤ p and p ≥ 1. #[prost(string, tag = "2")] pub field_type: ::prost::alloc::string::String, } +/// Response after table creation (success + DDL preview). #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct TableDefinitionResponse { + /// True if all DB changes and metadata inserts succeeded. #[prost(bool, tag = "1")] pub success: bool, + /// The actual SQL executed: CREATE TABLE + CREATE INDEX statements. #[prost(string, tag = "2")] pub sql: ::prost::alloc::string::String, } +/// Describes the tree of all profiles and their tables. #[derive(Clone, PartialEq, ::prost::Message)] pub struct ProfileTreeResponse { + /// All profiles in the system. #[prost(message, repeated, tag = "1")] pub profiles: ::prost::alloc::vec::Vec, } /// Nested message and enum types in `ProfileTreeResponse`. pub mod profile_tree_response { + /// Table entry in a profile. #[derive(Clone, PartialEq, ::prost::Message)] pub struct Table { + /// Internal ID from table_definitions.id (metadata record). #[prost(int64, tag = "1")] pub id: i64, + /// Table name within the profile (schema). #[prost(string, tag = "2")] pub name: ::prost::alloc::string::String, + /// Other tables this one references (based on link definitions only). #[prost(string, repeated, tag = "3")] pub depends_on: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, } + /// Profile (schema) entry. #[derive(Clone, PartialEq, ::prost::Message)] pub struct Profile { + /// Name of the schema/profile (as stored in `schemas.name`). #[prost(string, tag = "1")] pub name: ::prost::alloc::string::String, + /// All tables in that schema and their dependencies. #[prost(message, repeated, tag = "2")] pub tables: ::prost::alloc::vec::Vec, } } +/// Request to delete one table definition entirely. #[derive(Clone, PartialEq, ::prost::Message)] pub struct DeleteTableRequest { + /// Profile (schema) name owning the table (must exist). #[prost(string, tag = "1")] pub profile_name: ::prost::alloc::string::String, + /// Table to drop (must exist in the profile). + /// Executes DROP TABLE "profile"."table" CASCADE and then removes metadata. #[prost(string, tag = "2")] pub table_name: ::prost::alloc::string::String, } +/// Response after table deletion. #[derive(Clone, PartialEq, ::prost::Message)] pub struct DeleteTableResponse { + /// True if table and metadata were successfully deleted in one transaction. #[prost(bool, tag = "1")] pub success: bool, + /// Human-readable summary of what was removed. #[prost(string, tag = "2")] pub message: ::prost::alloc::string::String, } @@ -86,6 +142,10 @@ pub mod table_definition_client { )] use tonic::codegen::*; use tonic::codegen::http::Uri; + /// The TableDefinition service manages the entire lifecycle of user-defined + /// tables (stored as both metadata and physical PostgreSQL tables) inside + /// logical "profiles" (schemas). Each table has stored structure, links, and + /// validation rules. #[derive(Debug, Clone)] pub struct TableDefinitionClient { inner: tonic::client::Grpc, @@ -166,6 +226,9 @@ pub mod table_definition_client { self.inner = self.inner.max_encoding_message_size(limit); self } + /// Creates a new table (and schema if missing) with system columns, + /// linked-table foreign keys, user-defined columns, and optional indexes. + /// Also inserts metadata and default validation rules. Entirely transactional. pub async fn post_table_definition( &mut self, request: impl tonic::IntoRequest, @@ -195,6 +258,8 @@ pub mod table_definition_client { ); self.inner.unary(req, path, codec).await } + /// Lists all profiles (schemas) and their tables with declared dependencies. + /// This provides a tree-like overview of table relationships. pub async fn get_profile_tree( &mut self, request: impl tonic::IntoRequest, @@ -224,6 +289,7 @@ pub mod table_definition_client { ); self.inner.unary(req, path, codec).await } + /// Drops a table and its metadata, then deletes the profile if it becomes empty. pub async fn delete_table( &mut self, request: impl tonic::IntoRequest, @@ -268,6 +334,9 @@ pub mod table_definition_server { /// Generated trait containing gRPC methods that should be implemented for use with TableDefinitionServer. #[async_trait] pub trait TableDefinition: std::marker::Send + std::marker::Sync + 'static { + /// Creates a new table (and schema if missing) with system columns, + /// linked-table foreign keys, user-defined columns, and optional indexes. + /// Also inserts metadata and default validation rules. Entirely transactional. async fn post_table_definition( &self, request: tonic::Request, @@ -275,6 +344,8 @@ pub mod table_definition_server { tonic::Response, tonic::Status, >; + /// Lists all profiles (schemas) and their tables with declared dependencies. + /// This provides a tree-like overview of table relationships. async fn get_profile_tree( &self, request: tonic::Request, @@ -282,6 +353,7 @@ pub mod table_definition_server { tonic::Response, tonic::Status, >; + /// Drops a table and its metadata, then deletes the profile if it becomes empty. async fn delete_table( &self, request: tonic::Request, @@ -290,6 +362,10 @@ pub mod table_definition_server { tonic::Status, >; } + /// The TableDefinition service manages the entire lifecycle of user-defined + /// tables (stored as both metadata and physical PostgreSQL tables) inside + /// logical "profiles" (schemas). Each table has stored structure, links, and + /// validation rules. #[derive(Debug)] pub struct TableDefinitionServer { inner: Arc, diff --git a/common/src/proto/komp_ac.table_script.rs b/common/src/proto/komp_ac.table_script.rs index d6901c9..672b26e 100644 --- a/common/src/proto/komp_ac.table_script.rs +++ b/common/src/proto/komp_ac.table_script.rs @@ -1,21 +1,70 @@ // This file is @generated by prost-build. +/// Request to create or update a script bound to a specific table and column. #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct PostTableScriptRequest { + /// Required. The metadata ID from table_definitions.id that identifies the + /// table this script belongs to. The table must exist; its schema determines + /// where referenced tables/columns are validated and where dependencies are stored. #[prost(int64, tag = "1")] pub table_definition_id: i64, + /// Required. The target column in the target table that this script computes. + /// Must be an existing user-defined column in that table (not a system column). + /// System columns are reserved: "id", "deleted", "created_at". + /// The column's data type must NOT be one of the prohibited target types: + /// BIGINT, DATE, TIMESTAMPTZ + /// Note: BOOLEAN targets are allowed (values are converted to Steel #true/#false). #[prost(string, tag = "2")] pub target_column: ::prost::alloc::string::String, + /// Required. The script in the Steel DSL (S-expression style). + /// Syntax requirements: + /// - Non-empty, must start with '(' + /// - Balanced parentheses + /// + /// Referencing data: + /// - Structured table/column access (enforces link constraints): + /// (steel_get_column "table_name" "column_name") + /// (steel_get_column_with_index "table_name" index "column_name") + /// • index must be a non-negative integer literal + /// • self-references are allowed without links + /// • other tables require an explicit link from the source table + /// (table_definition_links) or the request fails + /// - Raw SQL access (no link required, but still validated): + /// (steel_query_sql "SELECT ...") + /// • Basic checks disallow operations that imply prohibited types, + /// e.g., EXTRACT(…), DATE_PART(…), ::DATE, ::TIMESTAMPTZ, ::BIGINT, CAST(…) + /// - Self variable access in transformed scripts: + /// (get-var "column_name") is treated as referencing the current table + /// + /// Math operations: + /// - The script is transformed by steel_decimal; supported math forms include: + /// +, -, *, /, ^, **, pow, sqrt, >, <, =, >=, <=, min, max, abs, round, + /// ln, log, log10, exp, sin, cos, tan + /// - Columns of the following types CANNOT be used inside math expressions: + /// BIGINT, TEXT, BOOLEAN, DATE, TIMESTAMPTZ + /// + /// Dependency tracking and cycles: + /// - Dependencies are extracted from steel_get_column(_with_index), get-var, + /// and steel_query_sql and stored in script_dependencies with context + /// - Cycles across tables are rejected (self-dependency is allowed) #[prost(string, tag = "3")] pub script: ::prost::alloc::string::String, + /// Optional. Free-text description stored alongside the script (no functional effect). #[prost(string, tag = "4")] pub description: ::prost::alloc::string::String, } +/// Response after creating or updating a script. #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct TableScriptResponse { + /// The ID of the script record in table_scripts (new or existing on upsert). #[prost(int64, tag = "1")] pub id: i64, + /// Human-readable warnings concatenated into a single string. Possible messages: + /// - Warning if the script references itself (may affect first population) + /// - Count of raw SQL queries present + /// - Info about number of structured linked-table accesses + /// - Warning if many dependencies may affect performance #[prost(string, tag = "2")] pub warnings: ::prost::alloc::string::String, } @@ -30,6 +79,18 @@ pub mod table_script_client { )] use tonic::codegen::*; use tonic::codegen::http::Uri; + /// Manages column-computation scripts for user-defined tables. + /// Each script belongs to a single table (table_definition_id) and populates + /// exactly one target column in that table. The server: + /// - Validates script syntax (non-empty, balanced parentheses, starts with '(') + /// - Validates the target column (exists, not a system column, allowed type) + /// - Validates column/type usage inside math expressions + /// - Validates referenced tables/columns against the schema + /// - Enforces link constraints for structured access (see notes below) + /// - Analyzes dependencies and prevents cycles across the schema + /// - Transforms the script to decimal-safe math (steel_decimal) + /// - Upserts into table_scripts and records dependencies in script_dependencies + /// The whole operation is transactional. #[derive(Debug, Clone)] pub struct TableScriptClient { inner: tonic::client::Grpc, @@ -110,6 +171,23 @@ pub mod table_script_client { self.inner = self.inner.max_encoding_message_size(limit); self } + /// Create or update a script for a specific table and target column. + /// + /// Behavior: + /// - Fetches the table by table_definition_id (must exist) + /// - Validates "script" (syntax), "target_column" (exists and type rules), + /// and all referenced tables/columns (must exist in same schema) + /// - Validates math operations: prohibits using certain data types in math + /// - Enforces link constraints for structured table access: + /// • Allowed always: self-references (same table) + /// • Structured access via steel_get_column / steel_get_column_with_index + /// requires an explicit link in table_definition_links + /// • Raw SQL access via steel_query_sql is permitted (still validated) + /// - Detects and rejects circular dependencies across all scripts in the schema + /// (self-references are allowed and not treated as cycles) + /// - Transforms the script to decimal-safe operations (steel_decimal) + /// - UPSERTS into table_scripts on (table_definitions_id, target_column) + /// and saves a normalized dependency list into script_dependencies pub async fn post_table_script( &mut self, request: impl tonic::IntoRequest, @@ -154,6 +232,23 @@ pub mod table_script_server { /// Generated trait containing gRPC methods that should be implemented for use with TableScriptServer. #[async_trait] pub trait TableScript: std::marker::Send + std::marker::Sync + 'static { + /// Create or update a script for a specific table and target column. + /// + /// Behavior: + /// - Fetches the table by table_definition_id (must exist) + /// - Validates "script" (syntax), "target_column" (exists and type rules), + /// and all referenced tables/columns (must exist in same schema) + /// - Validates math operations: prohibits using certain data types in math + /// - Enforces link constraints for structured table access: + /// • Allowed always: self-references (same table) + /// • Structured access via steel_get_column / steel_get_column_with_index + /// requires an explicit link in table_definition_links + /// • Raw SQL access via steel_query_sql is permitted (still validated) + /// - Detects and rejects circular dependencies across all scripts in the schema + /// (self-references are allowed and not treated as cycles) + /// - Transforms the script to decimal-safe operations (steel_decimal) + /// - UPSERTS into table_scripts on (table_definitions_id, target_column) + /// and saves a normalized dependency list into script_dependencies async fn post_table_script( &self, request: tonic::Request, @@ -162,6 +257,18 @@ pub mod table_script_server { tonic::Status, >; } + /// Manages column-computation scripts for user-defined tables. + /// Each script belongs to a single table (table_definition_id) and populates + /// exactly one target column in that table. The server: + /// - Validates script syntax (non-empty, balanced parentheses, starts with '(') + /// - Validates the target column (exists, not a system column, allowed type) + /// - Validates column/type usage inside math expressions + /// - Validates referenced tables/columns against the schema + /// - Enforces link constraints for structured access (see notes below) + /// - Analyzes dependencies and prevents cycles across the schema + /// - Transforms the script to decimal-safe math (steel_decimal) + /// - Upserts into table_scripts and records dependencies in script_dependencies + /// The whole operation is transactional. #[derive(Debug)] pub struct TableScriptServer { inner: Arc, diff --git a/common/src/proto/komp_ac.table_structure.rs b/common/src/proto/komp_ac.table_structure.rs index ea2f418..1e48052 100644 --- a/common/src/proto/komp_ac.table_structure.rs +++ b/common/src/proto/komp_ac.table_structure.rs @@ -1,27 +1,46 @@ // This file is @generated by prost-build. +/// Request identifying the profile (schema) and table to inspect. #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetTableStructureRequest { - /// e.g., "default" + /// Required. Profile (PostgreSQL schema) name. Must exist in `schemas`. #[prost(string, tag = "1")] pub profile_name: ::prost::alloc::string::String, - /// e.g., "2025_adresar6" + /// Required. Table name within the profile. Must exist in `table_definitions` + /// for the given profile. The physical table is then introspected via + /// information_schema. #[prost(string, tag = "2")] pub table_name: ::prost::alloc::string::String, } +/// Response with the ordered list of columns (by ordinal position). #[derive(Clone, PartialEq, ::prost::Message)] pub struct TableStructureResponse { + /// Columns of the physical table, including system columns (id, deleted, + /// created_at), user-defined columns, and any foreign-key columns such as + /// "_id". May be empty if the physical table is missing. #[prost(message, repeated, tag = "1")] pub columns: ::prost::alloc::vec::Vec, } +/// One physical column entry as reported by information_schema. #[derive(Clone, PartialEq, ::prost::Message)] pub struct TableColumn { + /// Column name exactly as defined in PostgreSQL. #[prost(string, tag = "1")] pub name: ::prost::alloc::string::String, - /// e.g., "TEXT", "BIGINT", "VARCHAR(255)", "TIMESTAMPTZ" + /// Normalized data type string derived from information_schema: + /// - VARCHAR(n) when udt_name='varchar' with character_maximum_length + /// - CHAR(n) when udt_name='bpchar' with character_maximum_length + /// - NUMERIC(p,s) when udt_name='numeric' with precision and scale + /// - NUMERIC(p) when udt_name='numeric' with precision only + /// - \[\] for array types (udt_name starting with '_', e.g., INT\[\] ) + /// - Otherwise UPPER(udt_name), e.g., TEXT, BIGINT, TIMESTAMPTZ + /// Examples: "TEXT", "BIGINT", "VARCHAR(255)", "TIMESTAMPTZ", "NUMERIC(14,4)" #[prost(string, tag = "2")] pub data_type: ::prost::alloc::string::String, + /// True if information_schema reports the column as nullable. #[prost(bool, tag = "3")] pub is_nullable: bool, + /// True if the column is part of the table's PRIMARY KEY. + /// Typically true for the "id" column created by the system. #[prost(bool, tag = "4")] pub is_primary_key: bool, } @@ -36,6 +55,14 @@ pub mod table_structure_service_client { )] use tonic::codegen::*; use tonic::codegen::http::Uri; + /// Introspects the physical PostgreSQL table for a given logical table + /// (defined in table_definitions) and returns its column structure. + /// The server validates that: + /// - The profile (schema) exists in `schemas` + /// - The table is defined for that profile in `table_definitions` + /// It then queries information_schema for the physical table and returns + /// normalized column metadata. If the physical table is missing despite + /// a definition, the response may contain an empty `columns` list. #[derive(Debug, Clone)] pub struct TableStructureServiceClient { inner: tonic::client::Grpc, @@ -116,6 +143,16 @@ pub mod table_structure_service_client { self.inner = self.inner.max_encoding_message_size(limit); self } + /// Return the physical column list (name, normalized data_type, + /// nullability, primary key flag) for a table in a profile. + /// + /// Behavior: + /// - NOT_FOUND if profile doesn't exist in `schemas` + /// - NOT_FOUND if table not defined for that profile in `table_definitions` + /// - Queries information_schema.columns ordered by ordinal position + /// - Normalizes data_type text (details under TableColumn.data_type) + /// - Returns an empty list if the table is validated but has no visible + /// columns in information_schema (e.g., physical table missing) pub async fn get_table_structure( &mut self, request: impl tonic::IntoRequest, @@ -160,6 +197,16 @@ pub mod table_structure_service_server { /// Generated trait containing gRPC methods that should be implemented for use with TableStructureServiceServer. #[async_trait] pub trait TableStructureService: std::marker::Send + std::marker::Sync + 'static { + /// Return the physical column list (name, normalized data_type, + /// nullability, primary key flag) for a table in a profile. + /// + /// Behavior: + /// - NOT_FOUND if profile doesn't exist in `schemas` + /// - NOT_FOUND if table not defined for that profile in `table_definitions` + /// - Queries information_schema.columns ordered by ordinal position + /// - Normalizes data_type text (details under TableColumn.data_type) + /// - Returns an empty list if the table is validated but has no visible + /// columns in information_schema (e.g., physical table missing) async fn get_table_structure( &self, request: tonic::Request, @@ -168,6 +215,14 @@ pub mod table_structure_service_server { tonic::Status, >; } + /// Introspects the physical PostgreSQL table for a given logical table + /// (defined in table_definitions) and returns its column structure. + /// The server validates that: + /// - The profile (schema) exists in `schemas` + /// - The table is defined for that profile in `table_definitions` + /// It then queries information_schema for the physical table and returns + /// normalized column metadata. If the physical table is missing despite + /// a definition, the response may contain an empty `columns` list. #[derive(Debug)] pub struct TableStructureServiceServer { inner: Arc, diff --git a/common/src/proto/komp_ac.tables_data.rs b/common/src/proto/komp_ac.tables_data.rs index f9cd644..a738033 100644 --- a/common/src/proto/komp_ac.tables_data.rs +++ b/common/src/proto/komp_ac.tables_data.rs @@ -1,92 +1,170 @@ // This file is @generated by prost-build. +/// Insert a new row. #[derive(Clone, PartialEq, ::prost::Message)] pub struct PostTableDataRequest { + /// Required. Profile (PostgreSQL schema) name that owns the table. + /// Must exist in the schemas table. #[prost(string, tag = "1")] pub profile_name: ::prost::alloc::string::String, + /// Required. Logical table (definition) name within the profile. + /// Must exist in table_definitions for the given profile. #[prost(string, tag = "2")] pub table_name: ::prost::alloc::string::String, + /// Required. Key-value data for columns to insert. + /// + /// Allowed keys: + /// - User-defined columns from the table definition + /// - System/FK columns: + /// • "deleted" (BOOLEAN), optional; default FALSE if not provided + /// • "_id" (BIGINT) for each table link + /// + /// Type expectations by SQL type: + /// - TEXT: string value; empty string is treated as NULL + /// - BOOLEAN: bool value + /// - TIMESTAMPTZ: ISO 8601/RFC 3339 string (parsed to TIMESTAMPTZ) + /// - INTEGER: number with no fractional part and within i32 range + /// - BIGINT: number with no fractional part and within i64 range + /// - NUMERIC(p,s): string representation only; empty string becomes NULL + /// (numbers for NUMERIC are rejected to avoid precision loss) + /// + /// Script validation rules: + /// - If a script exists for a target column, that column MUST be present here, + /// and its provided value MUST equal the script’s computed value (type-aware + /// comparison, e.g., decimals are compared numerically). + /// + /// Notes: + /// - Unknown/invalid column names are rejected + /// - Some application-specific validations may apply (e.g., max length for + /// certain fields like "telefon") #[prost(map = "string, message", tag = "3")] pub data: ::std::collections::HashMap< ::prost::alloc::string::String, ::prost_types::Value, >, } +/// Insert response. #[derive(Clone, PartialEq, ::prost::Message)] pub struct PostTableDataResponse { + /// True if the insert succeeded. #[prost(bool, tag = "1")] pub success: bool, + /// Human-readable message. #[prost(string, tag = "2")] pub message: ::prost::alloc::string::String, + /// The id of the inserted row. #[prost(int64, tag = "3")] pub inserted_id: i64, } +/// Update an existing row. #[derive(Clone, PartialEq, ::prost::Message)] pub struct PutTableDataRequest { + /// Required. Profile (schema) name. #[prost(string, tag = "1")] pub profile_name: ::prost::alloc::string::String, + /// Required. Table name within the profile. #[prost(string, tag = "2")] pub table_name: ::prost::alloc::string::String, + /// Required. Id of the row to update. #[prost(int64, tag = "3")] pub id: i64, + /// Required. Columns to update (same typing rules as PostTableDataRequest.data). + /// + /// Special script rules: + /// - If a script targets column X and X is included here, the value for X must + /// equal the script’s result (type-aware). + /// - If X is not included here but the update would cause the script’s result + /// to change compared to the current stored value, the update is rejected with + /// FAILED_PRECONDITION, instructing the caller to include X explicitly. + /// + /// Passing an empty map results in a no-op success response. #[prost(map = "string, message", tag = "4")] pub data: ::std::collections::HashMap< ::prost::alloc::string::String, ::prost_types::Value, >, } +/// Update response. #[derive(Clone, PartialEq, ::prost::Message)] pub struct PutTableDataResponse { + /// True if the update succeeded (or no-op on empty data). #[prost(bool, tag = "1")] pub success: bool, + /// Human-readable message. #[prost(string, tag = "2")] pub message: ::prost::alloc::string::String, + /// The id of the updated row. #[prost(int64, tag = "3")] pub updated_id: i64, } +/// Soft-delete a single row. #[derive(Clone, PartialEq, ::prost::Message)] pub struct DeleteTableDataRequest { + /// Required. Profile (schema) name. #[prost(string, tag = "1")] pub profile_name: ::prost::alloc::string::String, + /// Required. Table name within the profile. #[prost(string, tag = "2")] pub table_name: ::prost::alloc::string::String, + /// Required. Row id to soft-delete. #[prost(int64, tag = "3")] pub record_id: i64, } +/// Soft-delete response. #[derive(Clone, Copy, PartialEq, ::prost::Message)] pub struct DeleteTableDataResponse { + /// True if a row was marked deleted (id existed and was not already deleted). #[prost(bool, tag = "1")] pub success: bool, } +/// Fetch a single non-deleted row by id. #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetTableDataRequest { + /// Required. Profile (schema) name. #[prost(string, tag = "1")] pub profile_name: ::prost::alloc::string::String, + /// Required. Table name within the profile. #[prost(string, tag = "2")] pub table_name: ::prost::alloc::string::String, + /// Required. Id of the row to fetch. #[prost(int64, tag = "3")] pub id: i64, } +/// Row payload: all columns returned as strings. #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetTableDataResponse { + /// Map of column_name → stringified value for: + /// - id, deleted + /// - all user-defined columns from the table definition + /// - FK columns named "_id" for each table link + /// + /// All values are returned as TEXT via col::TEXT and COALESCEed to empty string + /// (NULL becomes ""). The row is returned only if deleted = FALSE. #[prost(map = "string, string", tag = "1")] pub data: ::std::collections::HashMap< ::prost::alloc::string::String, ::prost::alloc::string::String, >, } +/// Count non-deleted rows. #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetTableDataCountRequest { + /// Required. Profile (schema) name. #[prost(string, tag = "1")] pub profile_name: ::prost::alloc::string::String, + /// Required. Table name within the profile. #[prost(string, tag = "2")] pub table_name: ::prost::alloc::string::String, } +/// Fetch by ordinal position among non-deleted rows (1-based). #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetTableDataByPositionRequest { + /// Required. Profile (schema) name. #[prost(string, tag = "1")] pub profile_name: ::prost::alloc::string::String, + /// Required. Table name within the profile. #[prost(string, tag = "2")] pub table_name: ::prost::alloc::string::String, + /// Required. 1-based position by id ascending among rows with deleted = FALSE. #[prost(int32, tag = "3")] pub position: i32, } @@ -101,6 +179,11 @@ pub mod tables_data_client { )] use tonic::codegen::*; use tonic::codegen::http::Uri; + /// Read and write row data for user-defined tables inside profiles (schemas). + /// Operations are performed against the physical PostgreSQL table that + /// corresponds to the logical table definition and are scoped by profile + /// (schema). Deletions are soft (set deleted = true). Typed binding and + /// script-based validation are enforced consistently. #[derive(Debug, Clone)] pub struct TablesDataClient { inner: tonic::client::Grpc, @@ -181,6 +264,16 @@ pub mod tables_data_client { self.inner = self.inner.max_encoding_message_size(limit); self } + /// Insert a new row into a table with strict type binding and script validation. + /// + /// Behavior: + /// - Validates that profile (schema) exists and table is defined for it + /// - Validates provided columns exist (user-defined or allowed system/FK columns) + /// - For columns targeted by scripts in this table, the client MUST provide the + /// value, and it MUST equal the script’s calculated value (compared type-safely) + /// - Binds values with correct SQL types, rejects invalid formats/ranges + /// - Inserts the row and returns the new id; queues search indexing (best effort) + /// - If the physical table is missing but the definition exists, returns INTERNAL pub async fn post_table_data( &mut self, request: impl tonic::IntoRequest, @@ -207,6 +300,17 @@ pub mod tables_data_client { ); self.inner.unary(req, path, codec).await } + /// Update existing row data with strict type binding and script validation. + /// + /// Behavior: + /// - Validates profile and table, and that the record exists + /// - If request data is empty, returns success without changing the row + /// - For columns targeted by scripts: + /// • If included in update, provided value must equal the script result + /// • If not included, update must not cause the script result to differ + /// from the current stored value; otherwise FAILED_PRECONDITION is returned + /// - Binds values with correct SQL types; rejects invalid formats/ranges + /// - Updates the row and returns the id; queues search indexing (best effort) pub async fn put_table_data( &mut self, request: impl tonic::IntoRequest, @@ -233,6 +337,13 @@ pub mod tables_data_client { ); self.inner.unary(req, path, codec).await } + /// Soft-delete a single record (sets deleted = true) if it exists and is not already deleted. + /// + /// Behavior: + /// - Validates profile and table definition + /// - Updates only rows with deleted = false + /// - success = true means a row was actually changed; false means nothing to delete + /// - If the physical table is missing but the definition exists, returns INTERNAL pub async fn delete_table_data( &mut self, request: impl tonic::IntoRequest, @@ -259,6 +370,15 @@ pub mod tables_data_client { ); self.inner.unary(req, path, codec).await } + /// Fetch a single non-deleted row by id as textified values. + /// + /// Behavior: + /// - Validates profile and table definition + /// - Returns all columns as strings (COALESCE(col::TEXT, '') AS col) + /// including: id, deleted, all user-defined columns, and FK columns + /// named "_id" for each table link + /// - 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 pub async fn get_table_data( &mut self, request: impl tonic::IntoRequest, @@ -285,6 +405,12 @@ pub mod tables_data_client { ); self.inner.unary(req, path, codec).await } + /// Count non-deleted rows in a table. + /// + /// Behavior: + /// - Validates profile and table definition + /// - Returns komp_ac.common.CountResponse.count with rows where deleted = FALSE + /// - If the physical table is missing but the definition exists, returns INTERNAL pub async fn get_table_data_count( &mut self, request: impl tonic::IntoRequest, @@ -314,6 +440,12 @@ pub mod tables_data_client { ); self.inner.unary(req, path, codec).await } + /// Fetch the N-th non-deleted row by id order (1-based), then return its full data. + /// + /// Behavior: + /// - position is 1-based (position = 1 → first row by id ASC with deleted = FALSE) + /// - Returns NOT_FOUND if position is out of bounds + /// - Otherwise identical to GetTableData for the selected id pub async fn get_table_data_by_position( &mut self, request: impl tonic::IntoRequest, @@ -358,6 +490,16 @@ pub mod tables_data_server { /// Generated trait containing gRPC methods that should be implemented for use with TablesDataServer. #[async_trait] pub trait TablesData: std::marker::Send + std::marker::Sync + 'static { + /// Insert a new row into a table with strict type binding and script validation. + /// + /// Behavior: + /// - Validates that profile (schema) exists and table is defined for it + /// - Validates provided columns exist (user-defined or allowed system/FK columns) + /// - For columns targeted by scripts in this table, the client MUST provide the + /// value, and it MUST equal the script’s calculated value (compared type-safely) + /// - Binds values with correct SQL types, rejects invalid formats/ranges + /// - Inserts the row and returns the new id; queues search indexing (best effort) + /// - If the physical table is missing but the definition exists, returns INTERNAL async fn post_table_data( &self, request: tonic::Request, @@ -365,6 +507,17 @@ pub mod tables_data_server { tonic::Response, tonic::Status, >; + /// Update existing row data with strict type binding and script validation. + /// + /// Behavior: + /// - Validates profile and table, and that the record exists + /// - If request data is empty, returns success without changing the row + /// - For columns targeted by scripts: + /// • If included in update, provided value must equal the script result + /// • If not included, update must not cause the script result to differ + /// from the current stored value; otherwise FAILED_PRECONDITION is returned + /// - Binds values with correct SQL types; rejects invalid formats/ranges + /// - Updates the row and returns the id; queues search indexing (best effort) async fn put_table_data( &self, request: tonic::Request, @@ -372,6 +525,13 @@ pub mod tables_data_server { tonic::Response, tonic::Status, >; + /// Soft-delete a single record (sets deleted = true) if it exists and is not already deleted. + /// + /// Behavior: + /// - Validates profile and table definition + /// - Updates only rows with deleted = false + /// - success = true means a row was actually changed; false means nothing to delete + /// - If the physical table is missing but the definition exists, returns INTERNAL async fn delete_table_data( &self, request: tonic::Request, @@ -379,6 +539,15 @@ pub mod tables_data_server { tonic::Response, tonic::Status, >; + /// Fetch a single non-deleted row by id as textified values. + /// + /// Behavior: + /// - Validates profile and table definition + /// - Returns all columns as strings (COALESCE(col::TEXT, '') AS col) + /// including: id, deleted, all user-defined columns, and FK columns + /// named "_id" for each table link + /// - 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 async fn get_table_data( &self, request: tonic::Request, @@ -386,6 +555,12 @@ pub mod tables_data_server { tonic::Response, tonic::Status, >; + /// Count non-deleted rows in a table. + /// + /// Behavior: + /// - Validates profile and table definition + /// - Returns komp_ac.common.CountResponse.count with rows where deleted = FALSE + /// - If the physical table is missing but the definition exists, returns INTERNAL async fn get_table_data_count( &self, request: tonic::Request, @@ -393,6 +568,12 @@ pub mod tables_data_server { tonic::Response, tonic::Status, >; + /// Fetch the N-th non-deleted row by id order (1-based), then return its full data. + /// + /// Behavior: + /// - position is 1-based (position = 1 → first row by id ASC with deleted = FALSE) + /// - Returns NOT_FOUND if position is out of bounds + /// - Otherwise identical to GetTableData for the selected id async fn get_table_data_by_position( &self, request: tonic::Request, @@ -401,6 +582,11 @@ pub mod tables_data_server { tonic::Status, >; } + /// Read and write row data for user-defined tables inside profiles (schemas). + /// Operations are performed against the physical PostgreSQL table that + /// corresponds to the logical table definition and are scoped by profile + /// (schema). Deletions are soft (set deleted = true). Typed binding and + /// script-based validation are enforced consistently. #[derive(Debug)] pub struct TablesDataServer { inner: Arc, diff --git a/flake.nix b/flake.nix index f121004..d6963be 100644 --- a/flake.nix +++ b/flake.nix @@ -19,7 +19,7 @@ cargo rustfmt clippy - cargo-watch + cargo-watch # C build tools (for your linker issue) gcc @@ -32,10 +32,11 @@ # PostgreSQL for sqlx postgresql - sqlx-cli + sqlx-cli # Protocol Buffers compiler for gRPC protobuf + protoc-gen-doc ]; shellHook = ''