From 339d06ce7ee842ff8beb5ec421b6a673cff4cebe Mon Sep 17 00:00:00 2001 From: Priec Date: Sun, 26 Oct 2025 22:02:44 +0100 Subject: [PATCH] table structure docs are made --- common/.gitignore | 1 + common/Makefile | 11 ++ common/proto/table_definition.proto | 139 +++++++++++--- common/proto/table_script.proto | 97 +++++++++- common/proto/table_structure.proto | 63 ++++++- common/proto/tables_data.proto | 163 +++++++++++++++- common/src/proto/descriptor.bin | Bin 33667 -> 50754 bytes common/src/proto/komp_ac.table_definition.rs | 76 ++++++++ common/src/proto/komp_ac.table_script.rs | 107 +++++++++++ common/src/proto/komp_ac.table_structure.rs | 61 +++++- common/src/proto/komp_ac.tables_data.rs | 186 +++++++++++++++++++ flake.nix | 5 +- 12 files changed, 855 insertions(+), 54 deletions(-) create mode 100644 common/.gitignore create mode 100644 common/Makefile 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 61967eb249bdbab789494fea05f4c80560792ffd..a093404b00603f5e9a5ce9206fbc6cfc7331337f 100644 GIT binary patch literal 50754 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 F{|_Mu7lQx* delta 4985 zcmY*cOOG5^6|TBz6^f-TlL3( zFVt84+y5-CD)GN+NtkKhyS(}9=NmUaI-KA9vL6?TpCm~TdVUs!CQs5tho17XAksff zs!4?aM3Gr@NCAz`914IQJ{2__H2Omk`Gic*A*h-D!Xc=cvu{GJpz9!u^?M>y(2Gsm zA(D0H4nZ$IT!OLEUZ$&F!?M>zv1(dGLtFMWy+JAjDp^p|i95OL% z!Aj??J35V611Zq9KUWgS9yr9}1sPe>Ly=K7 z*GfH#I*+{M-9aL1v+1T4)8sy~Iuv-Dqm}t%*0L)FKIDl|)5;yHtK%g;c7ahwZ zAR1H6aRY$fRA>N^EUk+kC3M=#EDfFME$@J7P|UV$`)REMRQN~x>R7Vxg z&D@F$qC>Lu?9E$wsZYU~_cvD<_U5x4I7hCcvv&1e=PCtjx7wDw&_r3VS5gIp z>AjMwuL=syUdff4Zm;BO+iqdp)!cZCk}5EoX3^>iHDH)qZ%~X(%9=J3>QY3>LaV(LI(90D#_csYmV{O9wy(y<_PBJP~K!CDRCT>I|hvV>Kii z=h6WHFn2B;0EC0*(g8p)w>(E#u^wa>ntML9G%kb&5XsWa1`Y^%7v0{+;peYjzJ7U> z{3^Qr(|b35@sEHWF3)TEiAnrw7Ih4T#Sw_e;^z*3Fj6F_KZ{r zo)dcFoQQG}OAsI;S=tvP6ohzfU2X$IJdy5AqW(!rJ+hgk?oCqAOYkO9%sm3B=J^Z& zwX{^Dv6iOJB+c%0FtlEy;p!A*>;sxe)j9PAMm%!g(0mp5T0^`B!buvz^xSfrH5yGz z!!}t{vu6OH(G(ioP|egmDCBjsmzSykXPk6jKmYi%7tg+Uefzyi4hvZaLNAt-fl_Fu$=Iy}ks5BcrZ4XO^xk5+`N@l)JpaXKuQ$t`0K6jQ zUV3sTih0m;5+iiRwzUM2QT@%_Svov8JbCu~lj7BjFP^_TEe(>RrZh;AG%-7hWW!N| zF+`LR@6@ZU;+;y7*>G3YmJ2E7NmVghh%ip7l0FW8waCZrD6CgEKm^wDqBgt^Tc=WK za7$?WqBa#b9lxkeA4K$MkcE4haEcqg&ksC@IxdH-bjgD0Da=fj&ne>y`B zr~q&Z&bLZ?|WC5u|@-;=b?7ouG=#sqQL_qS5N3-84!8+=77it_nizk=im75?bXkJ zwxNErar=ia-!<`Hnp~ETZ~puHkEjsorpT+WZi-B{y2+?-IcuP5LR%JeFM?sZcbp72 znd1*c4F_ZOzKHCw2yJAbMk6rpxb4_4pcfxL7Co9B>N}!mhec>313ij;lHYYQs3qs$ z7BzrWxnLW+NOqW8aprxUjAEB9ti6j#R!Cxt*^Cq z3)R5`6QWF&GuRfRn+O#0>}w62p@CTq4BN`SAh=o;=*_m;Cc*@v*Phv&5(549;;w7_ zo;O@Iz7!+R*<87yNbGaz32yZ~;%yNK=5lO{M8nS6zUn3gUCe{FFD=M}wl8lp5cK-; zG~3UHfp`so>jo;a?18|3z@K3-N$dwQwlG-M-HuGB)@4J0|9B=wWp5JwQ8@`H_#+8E zPnMAc-(EEq8azP8r5Xi)EWw8iG{zEqTf`IPwtr$@OoTpU(3==Hl!*SMR=3fzzDy-) z0La-?as~vospJd@YE#J>z6`d!Ip>T{ZE?wI`(?1rIm6OxOKw||#-9e;k~ID_*p{T( zFM}D9MmggzgPC~EUk0=Ci-NqSEuC^5Q0wl<*aU#rJK}Vp>_^WI<_+5#ZoKc^XZAEB_T{t8?#2hwFcs7v>>Wt( zc{x6i;PY~PAm3M5Cy%@{wnt0u(VI)|u|$KH++&FbFS*AO4PJ6-XC|+(x@N(N>a+j6 z|Kt=)}G1qJKno&&*0QMSe?i%L*}mJ%L4%Pu4I1$ z!oe%~@&JOlt6{;P3WlgZkS`Aa=shU)C>jss%L6j#JwO!6tvJ&Uy~j)=#;J#9W@*Gk z_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 = ''