Compare commits

...

21 Commits

Author SHA1 Message Date
Priec
9b6e594d2f common build.rs migration bug fixed 2026-06-10 19:49:41 +02:00
Priec
617f18f331 working upgraded 2026-06-10 18:08:19 +02:00
Priec
5481d1cb13 moving towards upgraded crates 2026-06-10 17:22:42 +02:00
Priec
40ad0db13f bumping up versions safely 2026-06-10 13:44:51 +02:00
Priec
ed1d4be61b flake update and resolver update 2026-06-10 13:39:19 +02:00
Priec
394cd863e4 edition 2024 2026-06-10 13:26:51 +02:00
Priec
f73548649f tui pages canvas 2026-06-10 13:04:44 +02:00
Priec
bd204895d5 we are all at v0.7.5 welcome to bulk post, tui-pages support for canvas crate, moved validation-core crate 2026-06-07 17:51:51 +02:00
Priec
a05f1f2a1e removed formatter for rules 2026-05-29 12:18:13 +02:00
Priec
f11c6060ea tui pages is not prod ready yet 2026-05-26 21:52:53 +02:00
Priec
6d8fa0de63 cant change logic once data are in that column 2026-05-19 14:30:56 +02:00
Priec
dc273506b7 working v12 2026-05-17 13:10:44 +02:00
Priec
6a87750329 v0.6.9 nice 2026-05-10 17:14:11 +02:00
Priec
819058ad5c rule page in the validation client2 2026-05-10 09:24:04 +02:00
Priec
def75c00b4 rule page in the validation client 2026-05-10 09:23:33 +02:00
Priec
17a13569d8 cargo fmt 2026-05-06 20:33:53 +02:00
Priec
14f88e6a40 validation docs 2026-05-06 20:29:05 +02:00
Priec
3373e00dfc validation core as a dependency2 2026-05-06 19:50:09 +02:00
Priec
f094346e1b validation core as a dependency 2026-05-06 19:03:26 +02:00
Priec
3b0133640f more advancements 2026-05-03 23:34:03 +02:00
Priec
0600d3deaa table validation for the client from the server 2026-05-03 10:34:59 +02:00
33 changed files with 4655 additions and 1900 deletions

3
.gitignore vendored
View File

@@ -7,3 +7,6 @@ steel_decimal/tests/property_tests.proptest-regressions
canvas/*.toml
.aider*
.codex
TODO.md
tui-pages-cli/
tui-canvas-validation-core/

5
.gitmodules vendored
View File

@@ -2,8 +2,11 @@
path = client
url = git@gitlab.com:filipriec/komp_ac_client.git
[submodule "canvas"]
path = canvas
path = tui-canvas
url = git@gitlab.com:filipriec/tui-canvas.git
[submodule "server"]
path = server
url = git@gitlab.com:filipriec/komp_ac_server.git
[submodule "tui-pages"]
path = tui-pages
url = git@gitlab.com:filipriec/tui-pages.git

3657
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,12 @@
[workspace]
members = ["client", "server", "common", "search", "canvas"]
resolver = "2"
members = ["client", "server", "common", "search", "tui-canvas", "tui-canvas/tui-canvas-validation-core", "tui-pages" ]
resolver = "3"
[workspace.package]
# TODO: idk how to do the name, fix later
# name = "komp_ac"
version = "0.6.3"
edition = "2021"
version = "0.8.2"
edition = "2024"
license = "GPL-3.0-or-later"
authors = ["Filip Priečinský <filippriec@gmail.com>"]
description = "Poriadny uctovnicky software."
@@ -20,36 +20,36 @@ categories = ["command-line-interface"]
[workspace.dependencies]
# Async and gRPC
tokio = { version = "1.44.2", features = ["full"] }
tonic = "0.13.0"
prost = "0.13.5"
async-trait = "0.1.88"
prost-types = "0.13.0"
tokio = { version = "1.52.3", features = ["full"] }
tonic = "0.14.6"
prost = "0.14.4"
async-trait = "0.1.89"
prost-types = "0.14.4"
# Data Handling & Serialization
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
time = "0.3.41"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.150"
time = "0.3.47"
# Utilities & Error Handling
anyhow = "1.0.98"
anyhow = "1.0.102"
dotenvy = "0.15.7"
lazy_static = "1.5.0"
tracing = "0.1.41"
tracing = "0.1.44"
# Search crate
tantivy = "0.24.1"
tantivy = "0.26.1"
# Steel_decimal crate
rust_decimal = { version = "1.37.2", features = ["maths", "serde"] }
rust_decimal_macros = "1.37.1"
thiserror = "2.0.12"
regex = "1.11.1"
rust_decimal = { version = "1.42.0", features = ["maths", "serde"] }
rust_decimal_macros = "1.40.0"
thiserror = "2.0.18"
regex = "1.12.4"
# Canvas crate
ratatui = { version = "0.29.0", features = ["crossterm"] }
crossterm = "0.28.1"
toml = "0.8.20"
unicode-width = "0.2.0"
ratatui = { version = "0.30.1", features = ["crossterm"] }
crossterm = "0.29.0"
toml = "1.1.2"
unicode-width = "0.2.2"
common = { path = "./common" }

1
canvas

Submodule canvas deleted from abbda5b7a9

2
client

Submodule client updated: cdce1fc5f4...e800ead957

View File

@@ -7,14 +7,16 @@ license.workspace = true
[dependencies]
prost-types = { workspace = true }
tonic = "0.13.0"
prost = "0.13.5"
serde = { version = "1.0.219", features = ["derive"] }
tonic = "0.14.6"
prost = "0.14.4"
serde = { version = "1.0.228", features = ["derive"] }
# Search
tantivy = { workspace = true }
serde_json.workspace = true
tonic-prost = "0.14.6"
[build-dependencies]
tonic-build = { version = "0.13.0", features = ["prost-build"] }
prost-build = "0.14.1"
tonic-build = { version = "0.14.6" }
prost-build = "0.14.4"
tonic-prost-build = "0.14.6"

View File

@@ -1,5 +1,5 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::configure()
tonic_prost_build::configure()
.build_server(true)
.file_descriptor_set_path("src/proto/descriptor.bin")
.out_dir("src/proto")
@@ -8,6 +8,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
".komp_ac.table_validation.FieldValidation",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.field_attribute(
".komp_ac.table_validation.FieldValidation.locked",
"#[serde(default)]",
)
.type_attribute(
".komp_ac.table_validation.CharacterLimits",
"#[derive(serde::Serialize, serde::Deserialize)]",
@@ -24,12 +28,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
".komp_ac.table_validation.PatternRule",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.PatternPosition",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.CharacterConstraint",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.PatternRules",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.CustomFormatter",
".komp_ac.table_validation.AllowedValues",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
@@ -40,11 +52,107 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
".komp_ac.table_validation.UpdateFieldValidationResponse",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.ReplaceTableValidationRequest",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.ReplaceTableValidationResponse",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.ValidationRuleDefinition",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.ValidationSetRuleItem",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.ValidationSetRuleItem.Source",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.ValidationSetDefinition",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.UpsertValidationRuleRequest",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.UpsertValidationRuleResponse",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.ListValidationRulesRequest",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.ListValidationRulesResponse",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.DeleteValidationRuleRequest",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.DeleteValidationRuleResponse",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.UpsertValidationSetRequest",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.UpsertValidationSetResponse",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.ListValidationSetsRequest",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.ListValidationSetsResponse",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.DeleteValidationSetRequest",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.DeleteValidationSetResponse",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.ApplyValidationSetRequest",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.ApplyValidationSetResponse",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.LockFieldValidationRequest",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.LockFieldValidationResponse",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
// Enum -> readable strings in JSON ("BYTES", "DISPLAY_WIDTH")
.type_attribute(
".komp_ac.table_validation.CountMode",
"#[derive(serde::Serialize, serde::Deserialize)] #[serde(rename_all = \"SCREAMING_SNAKE_CASE\")]",
)
.type_attribute(
".komp_ac.table_validation.PatternPositionKind",
"#[derive(serde::Serialize, serde::Deserialize)] #[serde(rename_all = \"SCREAMING_SNAKE_CASE\")]",
)
.type_attribute(
".komp_ac.table_validation.CharacterConstraintKind",
"#[derive(serde::Serialize, serde::Deserialize)] #[serde(rename_all = \"SCREAMING_SNAKE_CASE\")]",
)
.type_attribute(
".komp_ac.table_definition.ColumnDefinition",
"#[derive(serde::Serialize, serde::Deserialize)]",
@@ -57,6 +165,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
".komp_ac.table_definition.PostTableDefinitionRequest",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_definition.AddTableColumnsRequest",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_definition.TableDefinitionResponse",
"#[derive(serde::Serialize, serde::Deserialize)]"
@@ -105,5 +217,29 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
],
&["proto"],
)?;
// Scope build-script reruns to the actual inputs. Without this, the script
// emits no `rerun-if-changed` directives, so Cargo reruns it whenever any
// file in the package changes. Since codegen writes into `src/proto/`, that
// self-write retriggers the script on every build (an infinite loop under
// `cargo watch`). tonic_build 0.13 emitted these automatically; 0.14
// (tonic_prost_build) no longer does.
println!("cargo:rerun-if-changed=build.rs");
for proto in [
"proto/common.proto",
"proto/adresar.proto",
"proto/auth.proto",
"proto/search.proto",
"proto/search2.proto",
"proto/table_definition.proto",
"proto/table_script.proto",
"proto/table_structure.proto",
"proto/table_validation.proto",
"proto/tables_data.proto",
"proto/uctovnictvo.proto",
] {
println!("cargo:rerun-if-changed={proto}");
}
Ok(())
}

View File

@@ -14,6 +14,10 @@ service TableDefinition {
// Also inserts metadata and default validation rules. Entirely transactional.
rpc PostTableDefinition(PostTableDefinitionRequest) returns (TableDefinitionResponse);
// Appends new user-defined columns to an existing table.
// Existing columns, links, and table logic are never changed by this call.
rpc AddTableColumns(AddTableColumnsRequest) 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);
@@ -72,6 +76,21 @@ message PostTableDefinitionRequest {
string profile_name = 5;
}
// Defines append-only column additions for an existing table.
message AddTableColumnsRequest {
// Existing profile/schema name.
string profile_name = 1;
// Existing table name in the profile.
string table_name = 2;
// New user-defined columns only. Existing columns cannot be changed here.
repeated ColumnDefinition columns = 3;
// Optional indexes for the new columns only.
repeated string indexes = 4;
}
// Describes one user-defined column for a table.
message ColumnDefinition {
// Column name that follows the same validation rules as table_name.

View File

@@ -2,32 +2,56 @@
syntax = "proto3";
package komp_ac.table_validation;
// This proto is the canonical server-side storage and distribution contract for
// client validation configuration.
//
// Design goals:
// - The server stores the entire field validation definition in one structured payload.
// - Clients fetch the validation rules for a table in one batch and map them to
// their local validation/runtime system (for example canvas).
// - Common validation must be represented as typed data, not as string mini-languages.
//
// Important split:
// - limits / pattern / allowed_values / required are validation rules.
// - mask is presentation and input-shaping metadata for clients.
// Request validation rules for a table
message GetTableValidationRequest {
string profileName = 1;
string tableName = 2;
}
// Response with field-level validations; if a field is omitted,
// no validation is applied (default unspecified).
// Response with field-level validations for the whole table.
// If a field is omitted, no validation configuration exists for that field.
message TableValidationResponse {
repeated FieldValidation fields = 1;
}
// Field-level validation (extensible for future kinds)
// Field-level validation definition stored on the server and distributed to clients.
message FieldValidation {
// MUST match your frontend FormState.dataKey for the column
string dataKey = 1;
// Current: only CharacterLimits. More rules can be added later.
// Validation 1: length and counting rules.
CharacterLimits limits = 10;
// Future expansion:
PatternRules pattern = 11; // Validation 2
optional CustomFormatter formatter = 14; // Validation 4 custom formatting logic
// Validation 2: position-based character constraints.
PatternRules pattern = 11;
// Exact-value whitelist.
AllowedValues allowed_values = 12;
// Client-side hint that this field participates in external/asynchronous validation UI.
bool external_validation_enabled = 13;
// Client-side display mask metadata. The server stores raw data without mask literals.
DisplayMask mask = 3;
// ExternalValidation external = 13;
// CustomFormatter formatter = 14;
// Field must be provided / treated as required by clients and server enforcement layers.
bool required = 4;
// Once locked, this field's validation config cannot be changed.
bool locked = 15;
}
// Character length counting mode
@@ -38,7 +62,8 @@ enum CountMode {
DISPLAY_WIDTH = 3;
}
// Character limit validation (Validation 1)
// Character limit validation (Validation 1).
// These rules map directly to canvas CharacterLimits.
message CharacterLimits {
// When zero, the field is considered "not set". If both min/max are zero,
// the server should avoid sending this FieldValidation (no validation).
@@ -51,39 +76,75 @@ message CharacterLimits {
CountMode countMode = 4; // defaults to CHARS if unspecified
}
// Mask for pretty display
// Mask for pretty display only.
//
// This is not a validation rule by itself. It exists so clients can render and
// navigate masked input while still storing raw values server-side.
message DisplayMask {
string pattern = 1; // e.g., "(###) ###-####" or "####-##-##"
string input_char = 2; // e.g., "#"
optional string template_char = 3; // e.g., "_"
}
// One positionbased validation rule, similar to CharacterFilter + PositionRange
// Which positions a pattern rule applies to.
// This exists instead of a string syntax like "0-3" so the server can validate
// the structure directly and clients do not need to parse a DSL.
message PatternPosition {
PatternPositionKind kind = 1;
uint32 single = 2;
uint32 start = 3;
uint32 end = 4;
repeated uint32 positions = 5;
}
enum PatternPositionKind {
PATTERN_POSITION_KIND_UNSPECIFIED = 0;
PATTERN_POSITION_SINGLE = 1;
PATTERN_POSITION_RANGE = 2;
PATTERN_POSITION_FROM = 3;
PATTERN_POSITION_MULTIPLE = 4;
}
// What type of character constraint a pattern rule applies.
// This mirrors the typed character filters used by canvas.
message CharacterConstraint {
CharacterConstraintKind kind = 1;
// Used when kind == CHARACTER_CONSTRAINT_EXACT.
optional string exact = 2;
// Used when kind == CHARACTER_CONSTRAINT_ONE_OF.
repeated string one_of = 3;
// Used when kind == CHARACTER_CONSTRAINT_REGEX.
optional string regex = 4;
}
enum CharacterConstraintKind {
CHARACTER_CONSTRAINT_KIND_UNSPECIFIED = 0;
CHARACTER_CONSTRAINT_ALPHABETIC = 1;
CHARACTER_CONSTRAINT_NUMERIC = 2;
CHARACTER_CONSTRAINT_ALPHANUMERIC = 3;
CHARACTER_CONSTRAINT_EXACT = 4;
CHARACTER_CONSTRAINT_ONE_OF = 5;
CHARACTER_CONSTRAINT_REGEX = 6;
}
// One position-based validation rule, similar to canvas PositionFilter.
message PatternRule {
// Range descriptor: how far the rule applies
// Examples:
// - "0" → Single position 0
// - "0-3" → Range 0..3 inclusive
// - "from:5" → From position 5 onward
// - "0,2,5" → Multiple discrete positions
string range = 1;
// Character filter type, caseinsensitive keywords:
// "ALPHABETIC", "NUMERIC", "ALPHANUMERIC",
// "ONEOF(<chars>)", "EXACT(:)", "CUSTOM(<name>)"
string filter = 2;
PatternPosition position = 1;
CharacterConstraint constraint = 2;
}
message CustomFormatter {
// Formatter type identifier; handled clientside.
// Examples: "PSCFormatter", "PhoneFormatter", "CreditCardFormatter", "DateFormatter"
string type = 1;
// Optional freetext note or parameters (e.g. locale, pattern)
optional string description = 2;
// Exact-value whitelist configuration.
// This maps to canvas AllowedValues semantics.
message AllowedValues {
repeated string values = 1;
bool allow_empty = 2;
bool case_insensitive = 3;
}
// Collection of pattern rules for one field
// Collection of pattern rules for one field.
message PatternRules {
// All rules that make up the validation logic
repeated PatternRule rules = 1;
@@ -92,11 +153,31 @@ message PatternRules {
optional string description = 2;
}
// Service to fetch validations for a table
// Service for storing and fetching field-validation definitions.
service TableValidationService {
rpc GetTableValidation(GetTableValidationRequest) returns (TableValidationResponse);
// Upsert a single field validation definition.
rpc UpdateFieldValidation(UpdateFieldValidationRequest) returns (UpdateFieldValidationResponse);
// Replace the full validation definition set for a table in one transaction.
rpc ReplaceTableValidation(ReplaceTableValidationRequest) returns (ReplaceTableValidationResponse);
// Reusable named rule fragments.
rpc UpsertValidationRule(UpsertValidationRuleRequest) returns (UpsertValidationRuleResponse);
rpc ListValidationRules(ListValidationRulesRequest) returns (ListValidationRulesResponse);
rpc DeleteValidationRule(DeleteValidationRuleRequest) returns (DeleteValidationRuleResponse);
// Reusable named sets composed from rules.
rpc UpsertValidationSet(UpsertValidationSetRequest) returns (UpsertValidationSetResponse);
rpc ListValidationSets(ListValidationSetsRequest) returns (ListValidationSetsResponse);
rpc DeleteValidationSet(DeleteValidationSetRequest) returns (DeleteValidationSetResponse);
// Snapshot a reusable set onto a concrete table field.
rpc ApplyValidationSet(ApplyValidationSetRequest) returns (ApplyValidationSetResponse);
// Permanently lock one field's validation config.
rpc LockFieldValidation(LockFieldValidationRequest) returns (LockFieldValidationResponse);
}
message UpdateFieldValidationRequest {
@@ -110,3 +191,130 @@ message UpdateFieldValidationResponse {
bool success = 1;
string message = 2;
}
message ReplaceTableValidationRequest {
string profileName = 1;
string tableName = 2;
// Full replacement set. Fields omitted here are removed from the stored config.
repeated FieldValidation fields = 3;
}
message ReplaceTableValidationResponse {
bool success = 1;
string message = 2;
}
message ValidationRuleDefinition {
optional int64 id = 4;
string name = 1;
optional string description = 2;
// Reusable rule fragment. dataKey is ignored by the server for reusable rules.
FieldValidation validation = 3;
}
message ValidationSetRuleItem {
int32 position = 1;
optional string name = 2;
optional string description = 3;
oneof source {
string global_rule_name = 10;
FieldValidation inline_validation = 11;
int64 global_rule_id = 12;
}
}
message ValidationSetDefinition {
reserved 3;
string name = 1;
optional string description = 2;
// Ordered set items.
repeated ValidationSetRuleItem ruleItems = 5;
// Server-resolved snapshot of all set items in order.
FieldValidation resolvedValidation = 4;
}
message UpsertValidationRuleRequest {
string profileName = 1;
ValidationRuleDefinition rule = 2;
}
message UpsertValidationRuleResponse {
bool success = 1;
string message = 2;
}
message ListValidationRulesRequest {
string profileName = 1;
}
message ListValidationRulesResponse {
repeated ValidationRuleDefinition rules = 1;
}
message DeleteValidationRuleRequest {
string profileName = 1;
string name = 2;
}
message DeleteValidationRuleResponse {
bool success = 1;
string message = 2;
}
message UpsertValidationSetRequest {
string profileName = 1;
ValidationSetDefinition set = 2;
}
message UpsertValidationSetResponse {
bool success = 1;
string message = 2;
}
message ListValidationSetsRequest {
string profileName = 1;
}
message ListValidationSetsResponse {
repeated ValidationSetDefinition sets = 1;
}
message DeleteValidationSetRequest {
string profileName = 1;
string name = 2;
}
message DeleteValidationSetResponse {
bool success = 1;
string message = 2;
}
message ApplyValidationSetRequest {
string profileName = 1;
string tableName = 2;
string dataKey = 3;
string setName = 4;
}
message ApplyValidationSetResponse {
bool success = 1;
string message = 2;
FieldValidation validation = 3;
}
message LockFieldValidationRequest {
string profileName = 1;
string tableName = 2;
string dataKey = 3;
}
message LockFieldValidationResponse {
bool success = 1;
string message = 2;
}

View File

@@ -23,6 +23,17 @@ service TablesData {
// - If the physical table is missing but the definition exists, returns INTERNAL
rpc PostTableData(PostTableDataRequest) returns (PostTableDataResponse);
// Insert multiple rows by applying PostTableData behavior to each row.
//
// Behavior:
// - Accepts 1..10,000 rows in one gRPC request
// - Processes rows in request order
// - Each row is inserted through the same validation, script execution,
// typed binding, database insert, and indexing path as PostTableData
// - Stops at the first failing row and returns that row's gRPC error code
// with row index context; rows inserted before the failure remain inserted
rpc PostTableDataBulk(PostTableDataBulkRequest) returns (PostTableDataBulkResponse);
// Update existing row data with strict type binding and script validation.
//
// Behavior:
@@ -124,6 +135,36 @@ message PostTableDataResponse {
int64 inserted_id = 3;
}
// One row in a bulk insert request.
message PostTableDataBulkRow {
// Required. Same data payload as PostTableDataRequest.data.
map<string, google.protobuf.Value> data = 1;
}
// Bulk insert request.
message PostTableDataBulkRequest {
// Required. Profile (PostgreSQL schema) name that owns the table.
string profile_name = 1;
// Required. Logical table (definition) name within the profile.
string table_name = 2;
// Required. Rows to insert. Must contain at least 1 and at most 10,000 rows.
repeated PostTableDataBulkRow rows = 3;
}
// Bulk insert response.
message PostTableDataBulkResponse {
// True if all rows were inserted successfully.
bool success = 1;
// Human-readable message.
string message = 2;
// Per-row responses from the underlying PostTableData logic, in request order.
repeated PostTableDataResponse responses = 3;
}
// Update an existing row.
message PutTableDataRequest {
// Required. Profile (schema) name.

Binary file not shown.

View File

@@ -1,15 +1,15 @@
// This file is @generated by prost-build.
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
pub struct GetAdresarRequest {
#[prost(int64, tag = "1")]
pub id: i64,
}
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
pub struct DeleteAdresarRequest {
#[prost(int64, tag = "1")]
pub id: i64,
}
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct PostAdresarRequest {
#[prost(string, tag = "1")]
pub firma: ::prost::alloc::string::String,
@@ -42,7 +42,7 @@ pub struct PostAdresarRequest {
#[prost(string, tag = "15")]
pub fax: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct AdresarResponse {
#[prost(int64, tag = "1")]
pub id: i64,
@@ -77,7 +77,7 @@ pub struct AdresarResponse {
#[prost(string, tag = "16")]
pub fax: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct PutAdresarRequest {
#[prost(int64, tag = "1")]
pub id: i64,
@@ -112,7 +112,7 @@ pub struct PutAdresarRequest {
#[prost(string, tag = "16")]
pub fax: ::prost::alloc::string::String,
}
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
pub struct DeleteAdresarResponse {
#[prost(bool, tag = "1")]
pub success: bool,
@@ -223,7 +223,7 @@ pub mod adresar_client {
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.adresar.Adresar/PostAdresar",
);
@@ -247,7 +247,7 @@ pub mod adresar_client {
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.adresar.Adresar/GetAdresar",
);
@@ -271,7 +271,7 @@ pub mod adresar_client {
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.adresar.Adresar/PutAdresar",
);
@@ -295,7 +295,7 @@ pub mod adresar_client {
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.adresar.Adresar/DeleteAdresar",
);
@@ -319,7 +319,7 @@ pub mod adresar_client {
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.adresar.Adresar/GetAdresarCount",
);
@@ -343,7 +343,7 @@ pub mod adresar_client {
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.adresar.Adresar/GetAdresarByPosition",
);
@@ -506,7 +506,7 @@ pub mod adresar_server {
let inner = self.inner.clone();
let fut = async move {
let method = PostAdresarSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
@@ -551,7 +551,7 @@ pub mod adresar_server {
let inner = self.inner.clone();
let fut = async move {
let method = GetAdresarSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
@@ -596,7 +596,7 @@ pub mod adresar_server {
let inner = self.inner.clone();
let fut = async move {
let method = PutAdresarSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
@@ -641,7 +641,7 @@ pub mod adresar_server {
let inner = self.inner.clone();
let fut = async move {
let method = DeleteAdresarSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
@@ -686,7 +686,7 @@ pub mod adresar_server {
let inner = self.inner.clone();
let fut = async move {
let method = GetAdresarCountSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
@@ -734,7 +734,7 @@ pub mod adresar_server {
let inner = self.inner.clone();
let fut = async move {
let method = GetAdresarByPositionSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,

View File

@@ -1,5 +1,5 @@
// This file is @generated by prost-build.
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct RegisterRequest {
#[prost(string, tag = "1")]
pub username: ::prost::alloc::string::String,
@@ -12,7 +12,7 @@ pub struct RegisterRequest {
#[prost(string, tag = "5")]
pub role: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct AuthResponse {
/// UUID in string format
#[prost(string, tag = "1")]
@@ -27,7 +27,7 @@ pub struct AuthResponse {
#[prost(string, tag = "4")]
pub role: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct LoginRequest {
/// Can be username or email
#[prost(string, tag = "1")]
@@ -35,7 +35,7 @@ pub struct LoginRequest {
#[prost(string, tag = "2")]
pub password: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct LoginResponse {
/// JWT token
#[prost(string, tag = "1")]
@@ -158,7 +158,7 @@ pub mod auth_service_client {
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.auth.AuthService/Register",
);
@@ -179,7 +179,7 @@ pub mod auth_service_client {
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.auth.AuthService/Login",
);
@@ -318,7 +318,7 @@ pub mod auth_service_server {
let inner = self.inner.clone();
let fut = async move {
let method = RegisterSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
@@ -361,7 +361,7 @@ pub mod auth_service_server {
let inner = self.inner.clone();
let fut = async move {
let method = LoginSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,

View File

@@ -1,12 +1,12 @@
// This file is @generated by prost-build.
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
pub struct Empty {}
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
pub struct CountResponse {
#[prost(int64, tag = "1")]
pub count: i64,
}
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
pub struct PositionRequest {
#[prost(int64, tag = "1")]
pub position: i64,

View File

@@ -1,5 +1,5 @@
// This file is @generated by prost-build.
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct ColumnConstraint {
#[prost(string, tag = "1")]
pub column: ::prost::alloc::string::String,
@@ -173,7 +173,7 @@ pub mod searcher_client {
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.search.Searcher/Search",
);
@@ -306,7 +306,7 @@ pub mod searcher_server {
let inner = self.inner.clone();
let fut = async move {
let method = SearchSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,

View File

@@ -17,7 +17,7 @@ pub struct Search2Request {
#[prost(bool, optional, tag = "7")]
pub order_desc: ::core::option::Option<bool>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct ColumnFilter {
#[prost(string, tag = "1")]
pub column_name: ::prost::alloc::string::String,
@@ -39,7 +39,7 @@ pub struct Search2Response {
}
/// Nested message and enum types in `Search2Response`.
pub mod search2_response {
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct Hit {
#[prost(int64, tag = "1")]
pub id: i64,
@@ -204,7 +204,7 @@ pub mod search2_client {
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.search2.Search2/SearchTable",
);
@@ -337,7 +337,7 @@ pub mod search2_server {
let inner = self.inner.clone();
let fut = async move {
let method = SearchTableSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,

View File

@@ -1,10 +1,10 @@
// 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)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct TableLink {
/// Name of an existing table within the same profile to link to.
/// For each link, a "<linked>_id" column is created on the new table.
/// For each link, a "<linked>\_id" column is created on the new table.
/// That column references "<linked>"(id) and adds an index automatically.
#[prost(string, tag = "1")]
pub linked_table_name: ::prost::alloc::string::String,
@@ -20,12 +20,12 @@ pub struct TableLink {
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".
/// 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 "<linked>_id" column and an index.
/// Each will automatically get a "<linked>\_id" column and an index.
#[prost(message, repeated, tag = "2")]
pub links: ::prost::alloc::vec::Vec<TableLink>,
/// List of user-defined columns (adds to system/id/fk columns).
@@ -33,23 +33,40 @@ pub struct PostTableDefinitionRequest {
pub columns: ::prost::alloc::vec::Vec<ColumnDefinition>,
/// 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
/// "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_".
/// 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.
/// Defines append-only column additions for an existing table.
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct AddTableColumnsRequest {
/// Existing profile/schema name.
#[prost(string, tag = "1")]
pub profile_name: ::prost::alloc::string::String,
/// Existing table name in the profile.
#[prost(string, tag = "2")]
pub table_name: ::prost::alloc::string::String,
/// New user-defined columns only. Existing columns cannot be changed here.
#[prost(message, repeated, tag = "3")]
pub columns: ::prost::alloc::vec::Vec<ColumnDefinition>,
/// Optional indexes for the new columns only.
#[prost(string, repeated, tag = "4")]
pub indexes: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
}
/// Describes one user-defined column for a table.
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, Eq, Hash, ::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".
/// 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):
@@ -68,7 +85,7 @@ pub struct ColumnDefinition {
}
/// Response after table creation (success + DDL preview).
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct TableDefinitionResponse {
/// True if all DB changes and metadata inserts succeeded.
#[prost(bool, tag = "1")]
@@ -87,7 +104,7 @@ pub struct ProfileTreeResponse {
/// Nested message and enum types in `ProfileTreeResponse`.
pub mod profile_tree_response {
/// Table entry in a profile.
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct Table {
/// Internal ID from table_definitions.id (metadata record).
#[prost(int64, tag = "1")]
@@ -111,7 +128,7 @@ pub mod profile_tree_response {
}
}
/// Request to fetch all tables, columns and scripts for a profile.
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct GetProfileDetailsRequest {
/// Profile (schema) name to fetch details for.
#[prost(string, tag = "1")]
@@ -127,7 +144,7 @@ pub struct GetProfileDetailsResponse {
}
/// Request to fetch recorded column alias rename history for one profile.
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct GetColumnAliasRenameHistoryRequest {
#[prost(string, tag = "1")]
pub profile_name: ::prost::alloc::string::String,
@@ -137,7 +154,7 @@ pub struct GetColumnAliasRenameHistoryRequest {
}
/// One recorded column alias rename.
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct ColumnAliasRenameHistoryEntry {
#[prost(int64, tag = "1")]
pub id: i64,
@@ -176,7 +193,7 @@ pub struct TableDetail {
pub scripts: ::prost::alloc::vec::Vec<ScriptInfo>,
}
/// A script that targets a specific column in a table.
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct ScriptInfo {
#[prost(int64, tag = "1")]
pub script_id: i64,
@@ -191,7 +208,7 @@ pub struct ScriptInfo {
}
/// Request to rename one user-visible column alias in a table.
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct RenameColumnAliasRequest {
#[prost(string, tag = "1")]
pub profile_name: ::prost::alloc::string::String,
@@ -204,7 +221,7 @@ pub struct RenameColumnAliasRequest {
}
/// Response after renaming one column alias.
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct RenameColumnAliasResponse {
#[prost(bool, tag = "1")]
pub success: bool,
@@ -212,7 +229,7 @@ pub struct RenameColumnAliasResponse {
pub message: ::prost::alloc::string::String,
}
/// Request to delete one table definition entirely.
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct DeleteTableRequest {
/// Profile (schema) name owning the table (must exist).
#[prost(string, tag = "1")]
@@ -223,7 +240,7 @@ pub struct DeleteTableRequest {
pub table_name: ::prost::alloc::string::String,
}
/// Response after table deletion.
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct DeleteTableResponse {
/// True if table and metadata were successfully deleted in one transaction.
#[prost(bool, tag = "1")]
@@ -345,7 +362,7 @@ pub mod table_definition_client {
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.table_definition.TableDefinition/PostTableDefinition",
);
@@ -359,6 +376,37 @@ pub mod table_definition_client {
);
self.inner.unary(req, path, codec).await
}
/// Appends new user-defined columns to an existing table.
/// Existing columns, links, and table logic are never changed by this call.
pub async fn add_table_columns(
&mut self,
request: impl tonic::IntoRequest<super::AddTableColumnsRequest>,
) -> std::result::Result<
tonic::Response<super::TableDefinitionResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.table_definition.TableDefinition/AddTableColumns",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"komp_ac.table_definition.TableDefinition",
"AddTableColumns",
),
);
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(
@@ -376,7 +424,7 @@ pub mod table_definition_client {
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.table_definition.TableDefinition/GetProfileTree",
);
@@ -407,7 +455,7 @@ pub mod table_definition_client {
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.table_definition.TableDefinition/GetProfileDetails",
);
@@ -437,7 +485,7 @@ pub mod table_definition_client {
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.table_definition.TableDefinition/GetColumnAliasRenameHistory",
);
@@ -467,7 +515,7 @@ pub mod table_definition_client {
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.table_definition.TableDefinition/RenameColumnAlias",
);
@@ -497,7 +545,7 @@ pub mod table_definition_client {
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.table_definition.TableDefinition/DeleteTable",
);
@@ -536,6 +584,15 @@ pub mod table_definition_server {
tonic::Response<super::TableDefinitionResponse>,
tonic::Status,
>;
/// Appends new user-defined columns to an existing table.
/// Existing columns, links, and table logic are never changed by this call.
async fn add_table_columns(
&self,
request: tonic::Request<super::AddTableColumnsRequest>,
) -> std::result::Result<
tonic::Response<super::TableDefinitionResponse>,
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(
@@ -693,7 +750,53 @@ pub mod table_definition_server {
let inner = self.inner.clone();
let fut = async move {
let method = PostTableDefinitionSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/komp_ac.table_definition.TableDefinition/AddTableColumns" => {
#[allow(non_camel_case_types)]
struct AddTableColumnsSvc<T: TableDefinition>(pub Arc<T>);
impl<
T: TableDefinition,
> tonic::server::UnaryService<super::AddTableColumnsRequest>
for AddTableColumnsSvc<T> {
type Response = super::TableDefinitionResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::AddTableColumnsRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as TableDefinition>::add_table_columns(&inner, request)
.await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = AddTableColumnsSvc(inner);
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
@@ -739,7 +842,7 @@ pub mod table_definition_server {
let inner = self.inner.clone();
let fut = async move {
let method = GetProfileTreeSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
@@ -785,7 +888,7 @@ pub mod table_definition_server {
let inner = self.inner.clone();
let fut = async move {
let method = GetProfileDetailsSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
@@ -839,7 +942,7 @@ pub mod table_definition_server {
let inner = self.inner.clone();
let fut = async move {
let method = GetColumnAliasRenameHistorySvc(inner);
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
@@ -885,7 +988,7 @@ pub mod table_definition_server {
let inner = self.inner.clone();
let fut = async move {
let method = RenameColumnAliasSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
@@ -930,7 +1033,7 @@ pub mod table_definition_server {
let inner = self.inner.clone();
let fut = async move {
let method = DeleteTableSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,

View File

@@ -1,7 +1,7 @@
// 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)]
#[derive(Clone, PartialEq, Eq, Hash, ::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
@@ -18,35 +18,39 @@ pub struct PostTableScriptRequest {
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
///
/// * Non-empty, must start with '('
/// * Balanced parentheses
///
/// Referencing data:
/// - Structured table/column access (enforces link constraints):
///
/// * 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):
/// * 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:
/// * 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,
///
/// * 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:
/// * 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,
///
/// * 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)
/// * 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).
@@ -55,20 +59,21 @@ pub struct PostTableScriptRequest {
}
/// Response after creating or updating a script.
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::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
///
/// * 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,
}
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct GetTableScriptsRequest {
/// Required. Profile (schema) name.
#[prost(string, tag = "1")]
@@ -97,7 +102,7 @@ pub struct StoredTableScript {
#[prost(message, repeated, tag = "6")]
pub dependencies: ::prost::alloc::vec::Vec<ScriptDependency>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct ScriptDependency {
#[prost(string, tag = "1")]
pub target_table: ::prost::alloc::string::String,
@@ -124,14 +129,15 @@ pub mod table_script_client {
/// 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
///
/// * 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<T> {
@@ -216,19 +222,20 @@ pub mod table_script_client {
/// 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),
///
/// * 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:
/// * 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
/// * 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)
/// * 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,
@@ -245,7 +252,7 @@ pub mod table_script_client {
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.table_script.TableScript/PostTableScript",
);
@@ -262,10 +269,11 @@ pub mod table_script_client {
/// Fetch all stored scripts for a specific table.
///
/// Behavior:
/// - Resolves the table from (profile_name, table_name)
/// - Returns the stored, transformed script from table_scripts
/// - Includes normalized dependency metadata from script_dependencies
/// - Returns an empty scripts list when the table has no scripts
///
/// * Resolves the table from (profile_name, table_name)
/// * Returns the stored, transformed script from table_scripts
/// * Includes normalized dependency metadata from script_dependencies
/// * Returns an empty scripts list when the table has no scripts
pub async fn get_table_scripts(
&mut self,
request: impl tonic::IntoRequest<super::GetTableScriptsRequest>,
@@ -281,7 +289,7 @@ pub mod table_script_client {
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.table_script.TableScript/GetTableScripts",
);
@@ -313,19 +321,20 @@ pub mod table_script_server {
/// 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),
///
/// * 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:
/// * 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
/// * 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)
/// * 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,
@@ -337,10 +346,11 @@ pub mod table_script_server {
/// Fetch all stored scripts for a specific table.
///
/// Behavior:
/// - Resolves the table from (profile_name, table_name)
/// - Returns the stored, transformed script from table_scripts
/// - Includes normalized dependency metadata from script_dependencies
/// - Returns an empty scripts list when the table has no scripts
///
/// * Resolves the table from (profile_name, table_name)
/// * Returns the stored, transformed script from table_scripts
/// * Includes normalized dependency metadata from script_dependencies
/// * Returns an empty scripts list when the table has no scripts
async fn get_table_scripts(
&self,
request: tonic::Request<super::GetTableScriptsRequest>,
@@ -352,14 +362,15 @@ pub mod table_script_server {
/// 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
///
/// * 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<T> {
@@ -467,7 +478,7 @@ pub mod table_script_server {
let inner = self.inner.clone();
let fut = async move {
let method = PostTableScriptSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
@@ -512,7 +523,7 @@ pub mod table_script_server {
let inner = self.inner.clone();
let fut = async move {
let method = GetTableScriptsSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,

View File

@@ -1,6 +1,6 @@
// This file is @generated by prost-build.
/// Request identifying the profile (schema) and tables to inspect.
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct GetTableStructureRequest {
/// Required. Profile (PostgreSQL schema) name. Must exist in `schemas`.
#[prost(string, tag = "1")]
@@ -26,23 +26,24 @@ pub struct GetTableStructureResponse {
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
/// "<linked_table>_id". May be empty if the physical table is missing.
/// "\<linked_table>\_id". May be empty if the physical table is missing.
#[prost(message, repeated, tag = "1")]
pub columns: ::prost::alloc::vec::Vec<TableColumn>,
}
/// One physical column entry as reported by information_schema.
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct TableColumn {
/// Column name exactly as defined in PostgreSQL.
#[prost(string, tag = "1")]
pub name: ::prost::alloc::string::String,
/// 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
/// - <TYPE>\[\] for array types (udt_name starting with '_', e.g., INT\[\] )
/// - Otherwise UPPER(udt_name), e.g., TEXT, BIGINT, TIMESTAMPTZ
///
/// * 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
/// * <TYPE>\[\] 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,
@@ -68,8 +69,9 @@ pub mod table_structure_service_client {
/// Introspects the physical PostgreSQL tables for one or more logical tables
/// (defined in table_definitions) and returns their column structures.
/// The server validates that:
/// - The profile (schema) exists in `schemas`
/// - Every table is defined for that profile in `table_definitions`
///
/// * The profile (schema) exists in `schemas`
/// * Every table is defined for that profile in `table_definitions`
/// It then queries information_schema for the physical tables and returns
/// normalized column metadata.
#[derive(Debug, Clone)]
@@ -156,11 +158,12 @@ pub mod table_structure_service_client {
/// nullability, primary key flag) for one or more tables in a profile.
///
/// Behavior:
/// - NOT_FOUND if profile doesn't exist in `schemas`
/// - NOT_FOUND if any table is 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 error if any validated table has no visible columns in
///
/// * NOT_FOUND if profile doesn't exist in `schemas`
/// * NOT_FOUND if any table is 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 error if any validated table has no visible columns in
/// information_schema (e.g., physical table missing)
pub async fn get_table_structure(
&mut self,
@@ -177,7 +180,7 @@ pub mod table_structure_service_client {
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.table_structure.TableStructureService/GetTableStructure",
);
@@ -210,11 +213,12 @@ pub mod table_structure_service_server {
/// nullability, primary key flag) for one or more tables in a profile.
///
/// Behavior:
/// - NOT_FOUND if profile doesn't exist in `schemas`
/// - NOT_FOUND if any table is 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 error if any validated table has no visible columns in
///
/// * NOT_FOUND if profile doesn't exist in `schemas`
/// * NOT_FOUND if any table is 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 error if any validated table has no visible columns in
/// information_schema (e.g., physical table missing)
async fn get_table_structure(
&self,
@@ -227,8 +231,9 @@ pub mod table_structure_service_server {
/// Introspects the physical PostgreSQL tables for one or more logical tables
/// (defined in table_definitions) and returns their column structures.
/// The server validates that:
/// - The profile (schema) exists in `schemas`
/// - Every table is defined for that profile in `table_definitions`
///
/// * The profile (schema) exists in `schemas`
/// * Every table is defined for that profile in `table_definitions`
/// It then queries information_schema for the physical tables and returns
/// normalized column metadata.
#[derive(Debug)]
@@ -342,7 +347,7 @@ pub mod table_structure_service_server {
let inner = self.inner.clone();
let fut = async move {
let method = GetTableStructureSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,

File diff suppressed because it is too large Load Diff

View File

@@ -13,28 +13,32 @@ pub struct PostTableDataRequest {
/// Required. Key-value data for columns to insert.
///
/// Allowed keys:
/// - User-defined columns from the table definition
/// - System/FK columns:
///
/// * User-defined columns from the table definition
/// * System/FK columns:
/// • "deleted" (BOOLEAN), optional; default FALSE if not provided
/// • "<linked_table>_id" (BIGINT) for each table link
/// • "\<linked_table>\_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
///
/// * 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,
///
/// * If a script exists for a target column, that column MUST be present here,
/// and its provided value MUST equal the scripts 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
///
/// * 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<
@@ -43,7 +47,7 @@ pub struct PostTableDataRequest {
>,
}
/// Insert response.
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct PostTableDataResponse {
/// True if the insert succeeded.
#[prost(bool, tag = "1")]
@@ -55,6 +59,42 @@ pub struct PostTableDataResponse {
#[prost(int64, tag = "3")]
pub inserted_id: i64,
}
/// One row in a bulk insert request.
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PostTableDataBulkRow {
/// Required. Same data payload as PostTableDataRequest.data.
#[prost(map = "string, message", tag = "1")]
pub data: ::std::collections::HashMap<
::prost::alloc::string::String,
::prost_types::Value,
>,
}
/// Bulk insert request.
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PostTableDataBulkRequest {
/// Required. Profile (PostgreSQL schema) name that owns the table.
#[prost(string, tag = "1")]
pub profile_name: ::prost::alloc::string::String,
/// Required. Logical table (definition) name within the profile.
#[prost(string, tag = "2")]
pub table_name: ::prost::alloc::string::String,
/// Required. Rows to insert. Must contain at least 1 and at most 10,000 rows.
#[prost(message, repeated, tag = "3")]
pub rows: ::prost::alloc::vec::Vec<PostTableDataBulkRow>,
}
/// Bulk insert response.
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PostTableDataBulkResponse {
/// True if all rows were inserted successfully.
#[prost(bool, tag = "1")]
pub success: bool,
/// Human-readable message.
#[prost(string, tag = "2")]
pub message: ::prost::alloc::string::String,
/// Per-row responses from the underlying PostTableData logic, in request order.
#[prost(message, repeated, tag = "3")]
pub responses: ::prost::alloc::vec::Vec<PostTableDataResponse>,
}
/// Update an existing row.
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PutTableDataRequest {
@@ -70,9 +110,10 @@ pub struct PutTableDataRequest {
/// 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
///
/// * If a script targets column X and X is included here, the value for X must
/// equal the scripts result (type-aware).
/// - If X is not included here but the update would cause the scripts result
/// * If X is not included here but the update would cause the scripts result
/// to change compared to the current stored value, the update is rejected with
/// FAILED_PRECONDITION, instructing the caller to include X explicitly.
///
@@ -84,7 +125,7 @@ pub struct PutTableDataRequest {
>,
}
/// Update response.
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct PutTableDataResponse {
/// True if the update succeeded (or no-op on empty data).
#[prost(bool, tag = "1")]
@@ -97,7 +138,7 @@ pub struct PutTableDataResponse {
pub updated_id: i64,
}
/// Soft-delete a single row.
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct DeleteTableDataRequest {
/// Required. Profile (schema) name.
#[prost(string, tag = "1")]
@@ -110,14 +151,14 @@ pub struct DeleteTableDataRequest {
pub record_id: i64,
}
/// Soft-delete response.
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::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)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct GetTableDataRequest {
/// Required. Profile (schema) name.
#[prost(string, tag = "1")]
@@ -133,9 +174,10 @@ pub struct GetTableDataRequest {
#[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 "<linked_table>_id" for each table link
///
/// * id, deleted
/// * all user-defined columns from the table definition
/// * FK columns named "\<linked_table>\_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.
@@ -146,7 +188,7 @@ pub struct GetTableDataResponse {
>,
}
/// Count non-deleted rows.
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct GetTableDataCountRequest {
/// Required. Profile (schema) name.
#[prost(string, tag = "1")]
@@ -156,7 +198,7 @@ pub struct GetTableDataCountRequest {
pub table_name: ::prost::alloc::string::String,
}
/// Fetch by ordinal position among non-deleted rows (1-based).
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct GetTableDataByPositionRequest {
/// Required. Profile (schema) name.
#[prost(string, tag = "1")]
@@ -267,13 +309,14 @@ pub mod tables_data_client {
/// 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
///
/// * 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 scripts 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
/// * 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<super::PostTableDataRequest>,
@@ -289,7 +332,7 @@ pub mod tables_data_client {
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.tables_data.TablesData/PostTableData",
);
@@ -300,17 +343,57 @@ pub mod tables_data_client {
);
self.inner.unary(req, path, codec).await
}
/// Insert multiple rows by applying PostTableData behavior to each row.
///
/// Behavior:
///
/// * Accepts 1..10,000 rows in one gRPC request
/// * Processes rows in request order
/// * Each row is inserted through the same validation, script execution,
/// typed binding, database insert, and indexing path as PostTableData
/// * Stops at the first failing row and returns that row's gRPC error code
/// with row index context; rows inserted before the failure remain inserted
pub async fn post_table_data_bulk(
&mut self,
request: impl tonic::IntoRequest<super::PostTableDataBulkRequest>,
) -> std::result::Result<
tonic::Response<super::PostTableDataBulkResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.tables_data.TablesData/PostTableDataBulk",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"komp_ac.tables_data.TablesData",
"PostTableDataBulk",
),
);
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:
///
/// * 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)
/// * 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<super::PutTableDataRequest>,
@@ -326,7 +409,7 @@ pub mod tables_data_client {
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.tables_data.TablesData/PutTableData",
);
@@ -340,10 +423,11 @@ pub mod tables_data_client {
/// 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
///
/// * 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<super::DeleteTableDataRequest>,
@@ -359,7 +443,7 @@ pub mod tables_data_client {
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.tables_data.TablesData/DeleteTableData",
);
@@ -373,12 +457,13 @@ pub mod tables_data_client {
/// 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)
///
/// * 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 "<linked_table>_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
/// named "\<linked_table>\_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<super::GetTableDataRequest>,
@@ -394,7 +479,7 @@ pub mod tables_data_client {
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.tables_data.TablesData/GetTableData",
);
@@ -408,9 +493,10 @@ pub mod tables_data_client {
/// 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
///
/// * 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<super::GetTableDataCountRequest>,
@@ -426,7 +512,7 @@ pub mod tables_data_client {
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.tables_data.TablesData/GetTableDataCount",
);
@@ -443,9 +529,10 @@ pub mod tables_data_client {
/// 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
///
/// * 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<super::GetTableDataByPositionRequest>,
@@ -461,7 +548,7 @@ pub mod tables_data_client {
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.tables_data.TablesData/GetTableDataByPosition",
);
@@ -493,13 +580,14 @@ pub mod tables_data_server {
/// 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
///
/// * 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 scripts 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
/// * 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<super::PostTableDataRequest>,
@@ -507,17 +595,35 @@ pub mod tables_data_server {
tonic::Response<super::PostTableDataResponse>,
tonic::Status,
>;
/// Insert multiple rows by applying PostTableData behavior to each row.
///
/// Behavior:
///
/// * Accepts 1..10,000 rows in one gRPC request
/// * Processes rows in request order
/// * Each row is inserted through the same validation, script execution,
/// typed binding, database insert, and indexing path as PostTableData
/// * Stops at the first failing row and returns that row's gRPC error code
/// with row index context; rows inserted before the failure remain inserted
async fn post_table_data_bulk(
&self,
request: tonic::Request<super::PostTableDataBulkRequest>,
) -> std::result::Result<
tonic::Response<super::PostTableDataBulkResponse>,
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:
///
/// * 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)
/// * 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<super::PutTableDataRequest>,
@@ -528,10 +634,11 @@ pub mod tables_data_server {
/// 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
///
/// * 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<super::DeleteTableDataRequest>,
@@ -542,12 +649,13 @@ pub mod tables_data_server {
/// 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)
///
/// * 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 "<linked_table>_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
/// named "\<linked_table>\_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<super::GetTableDataRequest>,
@@ -558,9 +666,10 @@ pub mod tables_data_server {
/// 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
///
/// * 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<super::GetTableDataCountRequest>,
@@ -571,9 +680,10 @@ pub mod tables_data_server {
/// 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
///
/// * 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<super::GetTableDataByPositionRequest>,
@@ -693,7 +803,53 @@ pub mod tables_data_server {
let inner = self.inner.clone();
let fut = async move {
let method = PostTableDataSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/komp_ac.tables_data.TablesData/PostTableDataBulk" => {
#[allow(non_camel_case_types)]
struct PostTableDataBulkSvc<T: TablesData>(pub Arc<T>);
impl<
T: TablesData,
> tonic::server::UnaryService<super::PostTableDataBulkRequest>
for PostTableDataBulkSvc<T> {
type Response = super::PostTableDataBulkResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::PostTableDataBulkRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as TablesData>::post_table_data_bulk(&inner, request)
.await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = PostTableDataBulkSvc(inner);
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
@@ -738,7 +894,7 @@ pub mod tables_data_server {
let inner = self.inner.clone();
let fut = async move {
let method = PutTableDataSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
@@ -783,7 +939,7 @@ pub mod tables_data_server {
let inner = self.inner.clone();
let fut = async move {
let method = DeleteTableDataSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
@@ -828,7 +984,7 @@ pub mod tables_data_server {
let inner = self.inner.clone();
let fut = async move {
let method = GetTableDataSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
@@ -874,7 +1030,7 @@ pub mod tables_data_server {
let inner = self.inner.clone();
let fut = async move {
let method = GetTableDataCountSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
@@ -923,7 +1079,7 @@ pub mod tables_data_server {
let inner = self.inner.clone();
let fut = async move {
let method = GetTableDataByPositionSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,

View File

@@ -1,5 +1,5 @@
// This file is @generated by prost-build.
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct PostUctovnictvoRequest {
#[prost(int64, tag = "1")]
pub adresar_id: i64,
@@ -25,7 +25,7 @@ pub struct PostUctovnictvoRequest {
#[prost(string, tag = "11")]
pub firma: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct UctovnictvoResponse {
#[prost(int64, tag = "1")]
pub id: i64,
@@ -52,7 +52,7 @@ pub struct UctovnictvoResponse {
#[prost(string, tag = "12")]
pub firma: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct PutUctovnictvoRequest {
#[prost(int64, tag = "1")]
pub id: i64,
@@ -79,7 +79,7 @@ pub struct PutUctovnictvoRequest {
#[prost(string, tag = "12")]
pub firma: ::prost::alloc::string::String,
}
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
pub struct GetUctovnictvoRequest {
#[prost(int64, tag = "1")]
pub id: i64,
@@ -190,7 +190,7 @@ pub mod uctovnictvo_client {
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.uctovnictvo.Uctovnictvo/PostUctovnictvo",
);
@@ -216,7 +216,7 @@ pub mod uctovnictvo_client {
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvo",
);
@@ -242,7 +242,7 @@ pub mod uctovnictvo_client {
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvoCount",
);
@@ -271,7 +271,7 @@ pub mod uctovnictvo_client {
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvoByPosition",
);
@@ -300,7 +300,7 @@ pub mod uctovnictvo_client {
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.uctovnictvo.Uctovnictvo/PutUctovnictvo",
);
@@ -468,7 +468,7 @@ pub mod uctovnictvo_server {
let inner = self.inner.clone();
let fut = async move {
let method = PostUctovnictvoSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
@@ -513,7 +513,7 @@ pub mod uctovnictvo_server {
let inner = self.inner.clone();
let fut = async move {
let method = GetUctovnictvoSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
@@ -559,7 +559,7 @@ pub mod uctovnictvo_server {
let inner = self.inner.clone();
let fut = async move {
let method = GetUctovnictvoCountSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
@@ -610,7 +610,7 @@ pub mod uctovnictvo_server {
let inner = self.inner.clone();
let fut = async move {
let method = GetUctovnictvoByPositionSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
@@ -655,7 +655,7 @@ pub mod uctovnictvo_server {
let inner = self.inner.clone();
let fut = async move {
let method = PutUctovnictvoSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,

View File

@@ -1,8 +1,8 @@
use std::path::{Path, PathBuf};
use tantivy::schema::{
Field, IndexRecordOption, JsonObjectOptions, Schema, TextFieldIndexing, Term, INDEXED,
STORED, STRING,
Field, IndexRecordOption, JsonObjectOptions, Schema, Term, TextFieldIndexing, TextOptions,
INDEXED, STORED, STRING,
};
use tantivy::tokenizer::{
AsciiFoldingFilter, LowerCaser, NgramTokenizer, RawTokenizer, RemoveLongFilter,
@@ -13,6 +13,7 @@ use tantivy::Index;
pub const F_PG_ID: &str = "pg_id";
pub const F_TABLE_NAME: &str = "table_name";
pub const F_ROW_KEY: &str = "row_key";
pub const F_ALL_TEXT: &str = "all_text";
pub const F_DATA_WORD: &str = "data_word";
pub const F_DATA_NGRAM: &str = "data_ngram";
pub const F_DATA_EXACT: &str = "data_exact";
@@ -59,6 +60,7 @@ pub fn create_search_schema() -> Schema {
schema_builder.add_u64_field(F_PG_ID, INDEXED | STORED);
schema_builder.add_text_field(F_TABLE_NAME, STRING | STORED);
schema_builder.add_text_field(F_ROW_KEY, STRING | STORED);
schema_builder.add_text_field(F_ALL_TEXT, text_options(TOK_WORD));
schema_builder.add_json_field(F_DATA_WORD, json_options(TOK_WORD, true, false));
schema_builder.add_json_field(F_DATA_NGRAM, json_options(TOK_NGRAM, true, false));
@@ -67,11 +69,15 @@ pub fn create_search_schema() -> Schema {
schema_builder.build()
}
fn json_options(
tokenizer_name: &str,
with_positions: bool,
stored: bool,
) -> JsonObjectOptions {
fn text_options(tokenizer_name: &str) -> TextOptions {
let indexing = TextFieldIndexing::default()
.set_tokenizer(tokenizer_name)
.set_index_option(IndexRecordOption::WithFreqsAndPositions);
TextOptions::default().set_indexing_options(indexing)
}
fn json_options(tokenizer_name: &str, with_positions: bool, stored: bool) -> JsonObjectOptions {
let index_option = if with_positions {
IndexRecordOption::WithFreqsAndPositions
} else {
@@ -157,6 +163,7 @@ pub struct SchemaFields {
pub pg_id: Field,
pub table_name: Field,
pub row_key: Field,
pub all_text: Field,
pub data_word: Field,
pub data_ngram: Field,
pub data_exact: Field,
@@ -168,6 +175,7 @@ impl SchemaFields {
pg_id: get_field(schema, F_PG_ID)?,
table_name: get_field(schema, F_TABLE_NAME)?,
row_key: get_field(schema, F_ROW_KEY)?,
all_text: get_field(schema, F_ALL_TEXT)?,
data_word: get_field(schema, F_DATA_WORD)?,
data_ngram: get_field(schema, F_DATA_NGRAM)?,
data_exact: get_field(schema, F_DATA_EXACT)?,

6
flake.lock generated
View File

@@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1753549186,
"narHash": "sha256-Znl7rzuxKg/Mdm6AhimcKynM7V3YeNDIcLjBuoBcmNs=",
"lastModified": 1780749050,
"narHash": "sha256-3av0pIjlOWQ6rDbNOmpUSvbNnJkGORQKKjb4LtCZsIY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "17f6bd177404d6d43017595c5264756764444ab8",
"rev": "a799d3e3886da994fa307f817a6bc705ae538eeb",
"type": "github"
},
"original": {

View File

@@ -15,5 +15,5 @@ tracing = { workspace = true }
tantivy = { workspace = true }
common = { path = "../common" }
tonic-reflection = "0.13.1"
sqlx = { version = "0.8.6", features = ["postgres"] }
tonic-reflection = "0.14.6"
sqlx = { version = "0.9.0", features = ["postgres"] }

View File

@@ -5,11 +5,11 @@ use std::path::Path;
use std::sync::{Arc, Mutex};
use common::proto::komp_ac::search::searcher_server::Searcher;
use common::proto::komp_ac::search::{search_response::Hit, SearchRequest, SearchResponse};
pub use common::proto::komp_ac::search::searcher_server::SearcherServer;
use common::search::{register_tokenizers, search_index_path, SchemaFields};
use query_builder::{build_master_query, ConstraintMode, SearchConstraint};
use sqlx::{PgPool, Row};
use common::proto::komp_ac::search::{SearchRequest, SearchResponse, search_response::Hit};
use common::search::{SchemaFields, register_tokenizers, search_index_path};
use query_builder::{ConstraintMode, SearchConstraint, build_master_query};
use sqlx::{AssertSqlSafe, PgPool, Row};
use tantivy::collector::TopDocs;
use tantivy::schema::Value;
use tantivy::{Index, IndexReader, ReloadPolicy, TantivyDocument};
@@ -34,7 +34,10 @@ impl SearcherService {
}
}
async fn run_rpc(&self, request: Request<SearchRequest>) -> Result<Response<SearchResponse>, Status> {
async fn run_rpc(
&self,
request: Request<SearchRequest>,
) -> Result<Response<SearchResponse>, Status> {
let req = request.into_inner();
let normalized = normalize_request(req)?;
@@ -130,7 +133,7 @@ impl ProfileIndex {
.map_err(|e| Status::internal(format!("Failed to build index reader: {}", e)))?;
let fields = SchemaFields::from(&index.schema()).map_err(|e| {
Status::internal(format!(
"Search index schema mismatch. Reindex required: {}",
"Search index schema mismatch. Delete the stale index and create it again: {}",
e
))
})?;
@@ -202,6 +205,22 @@ fn validate_identifier(value: &str, field_name: &str) -> Result<(), Status> {
Ok(())
}
fn validate_search_column(value: &str) -> Result<(), Status> {
if value.is_empty() {
return Err(Status::invalid_argument(
"constraint.column must not be empty",
));
}
if value.chars().any(|ch| ch.is_control() || ch == '\0') {
return Err(Status::invalid_argument(
"constraint.column contains invalid characters",
));
}
Ok(())
}
fn qualify_profile_table(profile_name: &str, table_name: &str) -> String {
format!("\"{}\".\"{}\"", profile_name, table_name)
}
@@ -255,12 +274,7 @@ fn normalize_request(req: SearchRequest) -> Result<NormalizedSearchRequest, Stat
for constraint in req.must {
let column = constraint.column.trim();
if column.is_empty() {
return Err(Status::invalid_argument(
"constraint.column must not be empty",
));
}
validate_identifier(column, "constraint.column")?;
validate_search_column(column)?;
let query = constraint.query.trim();
if query.is_empty() {
@@ -276,7 +290,9 @@ fn normalize_request(req: SearchRequest) -> Result<NormalizedSearchRequest, Stat
});
}
let limit = req.limit.map(|value| (value as usize).min(HARD_RESULT_LIMIT));
let limit = req
.limit
.map(|value| (value as usize).min(HARD_RESULT_LIMIT));
Ok(NormalizedSearchRequest {
profile_name: profile_name.to_string(),
@@ -305,7 +321,7 @@ async fn fetch_latest_rows(
qualify_profile_table(profile_name, table_name)
);
let rows = sqlx::query(&sql)
let rows = sqlx::query(AssertSqlSafe(sql))
.bind(limit as i64)
.fetch_all(pool)
.await
@@ -335,12 +351,17 @@ async fn run_search(
must: &[SearchConstraint],
limit: usize,
) -> Result<Vec<Hit>, Status> {
let master_query =
build_master_query(&profile.index, &profile.fields, free_query, must, table_filter)?;
let master_query = build_master_query(
&profile.index,
&profile.fields,
free_query,
must,
table_filter,
)?;
let searcher = profile.reader.searcher();
let top_docs = searcher
.search(&*master_query, &TopDocs::with_limit(limit))
.search(&*master_query, &TopDocs::with_limit(limit).order_by_score())
.map_err(|e| Status::internal(format!("Search failed: {}", e)))?;
if top_docs.is_empty() {
@@ -387,7 +408,7 @@ async fn run_search(
qualify_profile_table(profile_name, &table_name)
);
let rows = sqlx::query(&sql)
let rows = sqlx::query(AssertSqlSafe(sql))
.bind(&pg_ids)
.fetch_all(pool)
.await

View File

@@ -1,5 +1,6 @@
use common::search::{
json_path_term, normalize_exact, tokenize_ngram, tokenize_word, SchemaFields,
json_path_term, normalize_column_name, normalize_exact, tokenize_ngram, tokenize_word,
SchemaFields,
};
use tantivy::query::{
BooleanQuery, BoostQuery, EmptyQuery, FuzzyTermQuery, Occur, PhraseQuery, Query, QueryParser,
@@ -34,7 +35,9 @@ pub fn build_master_query(
for constraint in must {
let predicate = match constraint.mode {
ConstraintMode::Exact => exact_predicate(fields, &constraint.column, &constraint.query)?,
ConstraintMode::Exact => {
exact_predicate(fields, &constraint.column, &constraint.query)?
}
ConstraintMode::Fuzzy => {
fuzzy_predicate_scoped(fields, &constraint.column, &constraint.query)?
}
@@ -46,7 +49,7 @@ pub fn build_master_query(
let free_words = tokenize_word(free_query);
if !free_words.is_empty() {
let predicate = fuzzy_predicate_unscoped(index, fields, &free_words)?;
clauses.push((Occur::Should, predicate));
clauses.push((Occur::Must, predicate));
has_search_clause = true;
}
@@ -77,7 +80,8 @@ fn exact_predicate(
));
}
let term = json_path_term(fields.data_exact, column, &normalized_value);
let column = normalize_column_name(column);
let term = json_path_term(fields.data_exact, &column, &normalized_value);
Ok(Box::new(TermQuery::new(term, IndexRecordOption::Basic)))
}
@@ -93,11 +97,13 @@ fn fuzzy_predicate_scoped(
));
}
let column = normalize_column_name(column);
let mut layers: Vec<(Occur, Box<dyn Query>)> = Vec::new();
let mut per_word_clauses: Vec<(Occur, Box<dyn Query>)> = Vec::new();
for word in &words {
let term = json_path_term(fields.data_word, column, word);
let term = json_path_term(fields.data_word, &column, word);
let mut alternates: Vec<(Occur, Box<dyn Query>)> = Vec::new();
alternates.push((
@@ -134,7 +140,7 @@ fn fuzzy_predicate_scoped(
let phrase_terms: Vec<(usize, Term)> = words
.iter()
.enumerate()
.map(|(offset, word)| (offset, json_path_term(fields.data_word, column, word)))
.map(|(offset, word)| (offset, json_path_term(fields.data_word, &column, word)))
.collect();
let phrase = PhraseQuery::new_with_offset_and_slop(phrase_terms, 3);
layers.push((
@@ -148,7 +154,7 @@ fn fuzzy_predicate_scoped(
let ngram_clauses: Vec<(Occur, Box<dyn Query>)> = ngrams
.into_iter()
.map(|gram| {
let term = json_path_term(fields.data_ngram, column, &gram);
let term = json_path_term(fields.data_ngram, &column, &gram);
(
Occur::Must,
Box::new(TermQuery::new(term, IndexRecordOption::Basic)) as Box<dyn Query>,
@@ -157,7 +163,10 @@ fn fuzzy_predicate_scoped(
.collect();
layers.push((
Occur::Should,
Box::new(BoostQuery::new(Box::new(BooleanQuery::new(ngram_clauses)), 1.0)),
Box::new(BoostQuery::new(
Box::new(BooleanQuery::new(ngram_clauses)),
1.0,
)),
));
}
@@ -171,35 +180,43 @@ fn fuzzy_predicate_unscoped(
) -> Result<Box<dyn Query>, Status> {
let mut layers: Vec<(Occur, Box<dyn Query>)> = Vec::new();
{
let parser = QueryParser::for_index(index, vec![fields.data_word]);
let query_string = words
.iter()
.map(|word| format!("+{}*", word))
.collect::<Vec<_>>()
.join(" ");
if let Ok(query) = parser.parse_query(&query_string) {
layers.push((Occur::Should, Box::new(BoostQuery::new(query, 4.0))));
}
let mut per_word_clauses: Vec<(Occur, Box<dyn Query>)> = Vec::new();
for word in words {
let term = Term::from_field_text(fields.all_text, word);
let mut alternates: Vec<(Occur, Box<dyn Query>)> = Vec::new();
alternates.push((
Occur::Should,
Box::new(BoostQuery::new(
Box::new(TermQuery::new(term.clone(), IndexRecordOption::WithFreqs)),
4.0,
)),
));
alternates.push((
Occur::Should,
Box::new(BoostQuery::new(
Box::new(FuzzyTermQuery::new_prefix(term.clone(), 0, false)),
3.0,
)),
));
if let Some(distance) = fuzzy_distance(word.chars().count()) {
alternates.push((
Occur::Should,
Box::new(BoostQuery::new(
Box::new(FuzzyTermQuery::new(term, distance, true)),
2.0,
)),
));
}
{
let parser = QueryParser::for_index(index, vec![fields.data_word]);
let query_string = words
.iter()
.map(|word| match fuzzy_distance(word.chars().count()) {
Some(distance) => format!("+{}~{}", word, distance),
None => format!("+{}", word),
})
.collect::<Vec<_>>()
.join(" ");
if let Ok(query) = parser.parse_query(&query_string) {
layers.push((Occur::Should, Box::new(BoostQuery::new(query, 2.0))));
}
per_word_clauses.push((Occur::Must, Box::new(BooleanQuery::new(alternates))));
}
layers.push((Occur::Should, Box::new(BooleanQuery::new(per_word_clauses))));
if words.len() > 1 {
let parser = QueryParser::for_index(index, vec![fields.data_word]);
let parser = QueryParser::for_index(index, vec![fields.all_text]);
let query_string = format!("\"{}\"~3", words.join(" "));
if let Ok(query) = parser.parse_query(&query_string) {
layers.push((Occur::Should, Box::new(BoostQuery::new(query, 2.0))));
@@ -207,10 +224,10 @@ fn fuzzy_predicate_unscoped(
}
{
let parser = QueryParser::for_index(index, vec![fields.data_ngram]);
let parser = QueryParser::for_index(index, vec![fields.all_text]);
let query_string = words
.iter()
.map(|word| format!("+{}", word))
.map(|word| format!("+{}*", word))
.collect::<Vec<_>>()
.join(" ");
if let Ok(query) = parser.parse_query(&query_string) {

2
server

Submodule server updated: 8dbe9cc14c...3c3a1d6698

34
tantivy_todo.md Normal file
View File

@@ -0,0 +1,34 @@
1. Add explicit reindex/backfill tooling.
Right now, only future PostTableData / PutTableData calls index rows. There should be an admin/dev command like:
ReindexProfile(profile_name)
ReindexTable(profile_name, table_name)
ReindexRow(profile_name, table_name, id)
This is the biggest missing piece.
2. Stop using relative ./tantivy_indexes.
Both writer and reader depend on the process working directory. Make it config/env-driven, e.g.
TANTIVY_INDEX_DIR.
3. Add index schema/version metadata.
If you change tokenizers/schema later, old indexes should fail with a clear “index version mismatch, reindex
required” instead of behaving strangely.
4. Batch index commits.
Current code opens a writer and commits per row. Fine for dev, not great for many inserts. A long-lived writer
task batching commits every N docs or every short interval would be more reliable and faster.
5. Make the indexing queue durable.
The current mpsc queue is in-memory. If the server crashes after DB insert but before indexing, search is stale.
For serious use, store pending index jobs in Postgres, process them, mark done.
6. Index only live rows intentionally.
handle_add_or_update currently fetches row by id without checking deleted = false, then search filters deleted
rows later. Id either skip indexing deleted rows or make delete/update semantics explicit.
7. Add typed fields for numbers/dates if you need range queries.
Right now numbers are converted to strings. Good for text search, bad for real numeric filtering/sorting. Tantivy
can do numeric/date fields, but JSON text fields are not enough for robust range search.
8. Decide column-name strategy.
Indexing lowercases raw DB JSON keys. If UI uses display names/aliases, column constraints can miss unless the
frontend sends exactly what the index expects. Id centralize display-name to physical-name mapping before
search.
9. Add delete hooks for table/profile deletion.
When a table or profile is deleted, the matching Tantivy docs/index directory should be cleaned by code, not
manually.

1
tui-canvas Submodule

Submodule tui-canvas added at cea9ced406

1
tui-pages Submodule

Submodule tui-pages added at 4a06774f0e

View File

@@ -0,0 +1,33 @@
1. Action System
tui-pages intentionally does not provide ActionResolution like client/src/action_engine/action_decider/
handler.rs:23. Replace it in the client adapter/handler, not in tui-pages.
Best path: keep the routing logic as app code inside TuiActionHandler::handle_action, or keep a private helper
equivalent to ActionDecider::resolve. ctx.current_view and ctx.focus from tui-pages/src/runtime/mod.rs:181 give
you enough data to route page/canvas/global actions. This is a client philosophy change: routing becomes part of
the app handler, while tui-pages only provides focus/view/input context.
2. Overlay Types
No tui-pages change needed. Your old OverlayKind maps naturally to the generic O parameter.
Define something like client-side enum ClientOverlay { CommandBar, SearchPalette, FindFilePalette, Sidebar,
Picker }, then use FocusTarget::Overlay(ClientOverlay::CommandBar) etc. DialogButton(usize) should probably
become FocusTarget::ModalItem(usize) if you use the modal path, because tui-pages separates simple named overlays
from modal item focus in tui-pages/src/focus/target.rs:19.
3. String vs Type-Based Modes
This is not a real incompatibility. ModeId is just a typed wrapper over strings and implements AsRef<str>/
From<String> in tui-pages/src/runtime/mod.rs:13. Your current KeyMode::as_str() values already match the tui-
pages::modes constants.
The actual migration is moving mode calculation from FocusTarget::mode_hint() in client/src/focus_manager/
target.rs:50 into PageSpec::modes(...). Since page_spec(view, state, focus) receives focus, you can preserve the
same behavior there. Watch one behavior: the old client converts plain insert-mode chars into
CanvasAction::InsertChar in client/src/input_pipeline/pipeline.rs:90; tui-pages returns PipelineResponse::Type
instead. Preserve that in TuiActionHandler::handle_text.
4. Page Identification
PageIdentifier should likely disappear. It duplicates AppView. tui-pages already gives ctx.current_view, so
checks like PageIdentifier::Form(_) become matches!(ctx.current_view, AppView::Form(_)).
This is a natural cleanup, not a tui-pages gap. The only case to keep a separate identifier is if you
intentionally want to collapse several AppView variants into one routing bucket. Even then, make it a client-
local helper derived from AppView, not a runtime concept.