Compare commits

..

30 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
Priec
90f8aedc3b better new functionality of column aliases 2026-05-02 13:56:45 +02:00
Priec
2a811b1f8c rename the column aliases 2026-05-02 00:38:54 +02:00
Priec
1f9c29411e multiple requests to the structure of a tables at once(batching) 2026-04-30 11:48:03 +02:00
Priec
b928004c76 search with multiquery redesigned 2026-04-29 19:56:17 +02:00
Priec
fb4769301c column name indexing 2026-04-29 01:33:48 +02:00
Priec
036e12f345 indexing done by the profile and not table 2026-04-29 01:08:59 +02:00
Priec
1ceab57f3b exact search endpoint 2026-04-29 00:40:36 +02:00
Priec
5de1cd7623 refactoring search based on the profile 2026-04-29 00:38:42 +02:00
Priec
1867de513d get profile details with scripts and tables columns is now working 2026-04-27 22:01:17 +02:00
36 changed files with 5983 additions and 2180 deletions

4
.gitignore vendored
View File

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

5
.gitmodules vendored
View File

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

3679
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

1
canvas

Submodule canvas deleted from 812ac2a428

2
client

Submodule client updated: 2494066140...e800ead957

View File

@@ -7,14 +7,16 @@ license.workspace = true
[dependencies] [dependencies]
prost-types = { workspace = true } prost-types = { workspace = true }
tonic = "0.13.0" tonic = "0.14.6"
prost = "0.13.5" prost = "0.14.4"
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
# Search # Search
tantivy = { workspace = true } tantivy = { workspace = true }
serde_json.workspace = true serde_json.workspace = true
tonic-prost = "0.14.6"
[build-dependencies] [build-dependencies]
tonic-build = { version = "0.13.0", features = ["prost-build"] } tonic-build = { version = "0.14.6" }
prost-build = "0.14.1" 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>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::configure() tonic_prost_build::configure()
.build_server(true) .build_server(true)
.file_descriptor_set_path("src/proto/descriptor.bin") .file_descriptor_set_path("src/proto/descriptor.bin")
.out_dir("src/proto") .out_dir("src/proto")
@@ -8,6 +8,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
".komp_ac.table_validation.FieldValidation", ".komp_ac.table_validation.FieldValidation",
"#[derive(serde::Serialize, serde::Deserialize)]", "#[derive(serde::Serialize, serde::Deserialize)]",
) )
.field_attribute(
".komp_ac.table_validation.FieldValidation.locked",
"#[serde(default)]",
)
.type_attribute( .type_attribute(
".komp_ac.table_validation.CharacterLimits", ".komp_ac.table_validation.CharacterLimits",
"#[derive(serde::Serialize, serde::Deserialize)]", "#[derive(serde::Serialize, serde::Deserialize)]",
@@ -24,12 +28,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
".komp_ac.table_validation.PatternRule", ".komp_ac.table_validation.PatternRule",
"#[derive(serde::Serialize, serde::Deserialize)]", "#[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( .type_attribute(
".komp_ac.table_validation.PatternRules", ".komp_ac.table_validation.PatternRules",
"#[derive(serde::Serialize, serde::Deserialize)]", "#[derive(serde::Serialize, serde::Deserialize)]",
) )
.type_attribute( .type_attribute(
".komp_ac.table_validation.CustomFormatter", ".komp_ac.table_validation.AllowedValues",
"#[derive(serde::Serialize, serde::Deserialize)]", "#[derive(serde::Serialize, serde::Deserialize)]",
) )
.type_attribute( .type_attribute(
@@ -40,11 +52,107 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
".komp_ac.table_validation.UpdateFieldValidationResponse", ".komp_ac.table_validation.UpdateFieldValidationResponse",
"#[derive(serde::Serialize, serde::Deserialize)]", "#[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") // Enum -> readable strings in JSON ("BYTES", "DISPLAY_WIDTH")
.type_attribute( .type_attribute(
".komp_ac.table_validation.CountMode", ".komp_ac.table_validation.CountMode",
"#[derive(serde::Serialize, serde::Deserialize)] #[serde(rename_all = \"SCREAMING_SNAKE_CASE\")]", "#[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( .type_attribute(
".komp_ac.table_definition.ColumnDefinition", ".komp_ac.table_definition.ColumnDefinition",
"#[derive(serde::Serialize, serde::Deserialize)]", "#[derive(serde::Serialize, serde::Deserialize)]",
@@ -57,10 +165,34 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
".komp_ac.table_definition.PostTableDefinitionRequest", ".komp_ac.table_definition.PostTableDefinitionRequest",
"#[derive(serde::Serialize, serde::Deserialize)]", "#[derive(serde::Serialize, serde::Deserialize)]",
) )
.type_attribute(
".komp_ac.table_definition.AddTableColumnsRequest",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute( .type_attribute(
".komp_ac.table_definition.TableDefinitionResponse", ".komp_ac.table_definition.TableDefinitionResponse",
"#[derive(serde::Serialize, serde::Deserialize)]" "#[derive(serde::Serialize, serde::Deserialize)]"
) )
.type_attribute(
".komp_ac.table_definition.GetColumnAliasRenameHistoryRequest",
"#[derive(serde::Serialize, serde::Deserialize)]"
)
.type_attribute(
".komp_ac.table_definition.ColumnAliasRenameHistoryEntry",
"#[derive(serde::Serialize, serde::Deserialize)]"
)
.type_attribute(
".komp_ac.table_definition.GetColumnAliasRenameHistoryResponse",
"#[derive(serde::Serialize, serde::Deserialize)]"
)
.type_attribute(
".komp_ac.table_definition.RenameColumnAliasRequest",
"#[derive(serde::Serialize, serde::Deserialize)]"
)
.type_attribute(
".komp_ac.table_definition.RenameColumnAliasResponse",
"#[derive(serde::Serialize, serde::Deserialize)]"
)
.type_attribute( .type_attribute(
".komp_ac.table_script.PostTableScriptRequest", ".komp_ac.table_script.PostTableScriptRequest",
"#[derive(serde::Serialize, serde::Deserialize)]", "#[derive(serde::Serialize, serde::Deserialize)]",
@@ -85,5 +217,29 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
], ],
&["proto"], &["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(()) Ok(())
} }

View File

@@ -3,18 +3,34 @@ syntax = "proto3";
package komp_ac.search; package komp_ac.search;
service Searcher { service Searcher {
rpc SearchTable(SearchRequest) returns (SearchResponse); rpc Search(SearchRequest) returns (SearchResponse);
}
enum MatchMode {
MATCH_MODE_UNSPECIFIED = 0;
MATCH_MODE_FUZZY = 1;
MATCH_MODE_EXACT = 2;
}
message ColumnConstraint {
string column = 1;
string query = 2;
MatchMode mode = 3;
} }
message SearchRequest { message SearchRequest {
string table_name = 1; string profile_name = 1;
string query = 2; optional string table_name = 2;
string free_query = 3;
repeated ColumnConstraint must = 4;
optional uint32 limit = 5;
} }
message SearchResponse { message SearchResponse {
message Hit { message Hit {
int64 id = 1; // PostgreSQL row ID int64 id = 1; // PostgreSQL row ID
float score = 2; float score = 2;
string content_json = 3; string content_json = 3;
string table_name = 4;
} }
repeated Hit hits = 1; repeated Hit hits = 1;
} }

View File

@@ -14,10 +14,24 @@ service TableDefinition {
// Also inserts metadata and default validation rules. Entirely transactional. // Also inserts metadata and default validation rules. Entirely transactional.
rpc PostTableDefinition(PostTableDefinitionRequest) returns (TableDefinitionResponse); 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. // Lists all profiles (schemas) and their tables with declared dependencies.
// This provides a tree-like overview of table relationships. // This provides a tree-like overview of table relationships.
rpc GetProfileTree(komp_ac.common.Empty) returns (ProfileTreeResponse); rpc GetProfileTree(komp_ac.common.Empty) returns (ProfileTreeResponse);
// Fetches all tables with their columns and scripts for a specific profile.
// Pure data retrieval - no business logic.
rpc GetProfileDetails(GetProfileDetailsRequest) returns (GetProfileDetailsResponse);
// Returns the stored rename history for column aliases in one profile.
rpc GetColumnAliasRenameHistory(GetColumnAliasRenameHistoryRequest) returns (GetColumnAliasRenameHistoryResponse);
// Renames a user-visible column alias while keeping the physical column unchanged.
rpc RenameColumnAlias(RenameColumnAliasRequest) returns (RenameColumnAliasResponse);
// Drops a table and its metadata, then deletes the profile if it becomes empty. // Drops a table and its metadata, then deletes the profile if it becomes empty.
rpc DeleteTable(DeleteTableRequest) returns (DeleteTableResponse); rpc DeleteTable(DeleteTableRequest) returns (DeleteTableResponse);
} }
@@ -62,6 +76,21 @@ message PostTableDefinitionRequest {
string profile_name = 5; 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. // Describes one user-defined column for a table.
message ColumnDefinition { message ColumnDefinition {
// Column name that follows the same validation rules as table_name. // Column name that follows the same validation rules as table_name.
@@ -119,6 +148,74 @@ message ProfileTreeResponse {
repeated Profile profiles = 1; repeated Profile profiles = 1;
} }
// Request to fetch all tables, columns and scripts for a profile.
message GetProfileDetailsRequest {
// Profile (schema) name to fetch details for.
string profile_name = 1;
}
// Response with all tables, columns and scripts for a profile.
message GetProfileDetailsResponse {
string profile_name = 1;
repeated TableDetail tables = 2;
}
// Request to fetch recorded column alias rename history for one profile.
message GetColumnAliasRenameHistoryRequest {
string profile_name = 1;
// Optional filter. When omitted, returns all tables in the profile.
optional int64 table_definition_id = 2;
}
// One recorded column alias rename.
message ColumnAliasRenameHistoryEntry {
int64 id = 1;
string profile_name = 2;
int64 table_definition_id = 3;
string table_name = 4;
string old_column_name = 5;
string new_column_name = 6;
string created_at = 7;
}
// Response with stored column alias rename history rows.
message GetColumnAliasRenameHistoryResponse {
string profile_name = 1;
repeated ColumnAliasRenameHistoryEntry entries = 2;
}
// Describes a table with its columns and associated scripts.
message TableDetail {
string name = 1;
int64 id = 2;
repeated ColumnDefinition columns = 3;
repeated ScriptInfo scripts = 4;
}
// A script that targets a specific column in a table.
message ScriptInfo {
int64 script_id = 1;
string target_column = 2;
string target_column_type = 3;
string script = 4;
string description = 5;
}
// Request to rename one user-visible column alias in a table.
message RenameColumnAliasRequest {
string profile_name = 1;
string table_name = 2;
string old_column_name = 3;
string new_column_name = 4;
}
// Response after renaming one column alias.
message RenameColumnAliasResponse {
bool success = 1;
string message = 2;
}
// Request to delete one table definition entirely. // Request to delete one table definition entirely.
message DeleteTableRequest { message DeleteTableRequest {
// Profile (schema) name owning the table (must exist). // Profile (schema) name owning the table (must exist).

View File

@@ -4,40 +4,45 @@ package komp_ac.table_structure;
import "common.proto"; import "common.proto";
// Introspects the physical PostgreSQL table for a given logical table // Introspects the physical PostgreSQL tables for one or more logical tables
// (defined in table_definitions) and returns its column structure. // (defined in table_definitions) and returns their column structures.
// The server validates that: // The server validates that:
// - The profile (schema) exists in `schemas` // - The profile (schema) exists in `schemas`
// - The table is defined for that profile in `table_definitions` // - Every table is defined for that profile in `table_definitions`
// It then queries information_schema for the physical table and returns // It then queries information_schema for the physical tables and returns
// normalized column metadata. If the physical table is missing despite // normalized column metadata.
// a definition, the response may contain an empty `columns` list.
service TableStructureService { service TableStructureService {
// Return the physical column list (name, normalized data_type, // Return the physical column list (name, normalized data_type,
// nullability, primary key flag) for a table in a profile. // nullability, primary key flag) for one or more tables in a profile.
// //
// Behavior: // Behavior:
// - NOT_FOUND if profile doesn't exist in `schemas` // - NOT_FOUND if profile doesn't exist in `schemas`
// - NOT_FOUND if table not defined for that profile in `table_definitions` // - NOT_FOUND if any table is not defined for that profile in `table_definitions`
// - Queries information_schema.columns ordered by ordinal position // - Queries information_schema.columns ordered by ordinal position
// - Normalizes data_type text (details under TableColumn.data_type) // - Normalizes data_type text (details under TableColumn.data_type)
// - Returns an empty list if the table is validated but has no visible // - Returns an error if any validated table has no visible columns in
// columns in information_schema (e.g., physical table missing) // information_schema (e.g., physical table missing)
rpc GetTableStructure(GetTableStructureRequest) returns (TableStructureResponse); rpc GetTableStructure(GetTableStructureRequest) returns (GetTableStructureResponse);
} }
// Request identifying the profile (schema) and table to inspect. // Request identifying the profile (schema) and tables to inspect.
message GetTableStructureRequest { message GetTableStructureRequest {
// Required. Profile (PostgreSQL schema) name. Must exist in `schemas`. // Required. Profile (PostgreSQL schema) name. Must exist in `schemas`.
string profile_name = 1; string profile_name = 1;
// Required. Table name within the profile. Must exist in `table_definitions` // Required. Table names within the profile. Each must exist in
// for the given profile. The physical table is then introspected via // `table_definitions` for the given profile. The physical tables are then
// information_schema. // introspected via information_schema.
string table_name = 2; repeated string table_names = 2;
} }
// Response with the ordered list of columns (by ordinal position). // Batched response keyed by table name.
message GetTableStructureResponse {
// Per-table physical column lists keyed by requested table name.
map<string, TableStructureResponse> table_structures = 1;
}
// Response with the ordered list of columns (by ordinal position) for one table.
message TableStructureResponse { message TableStructureResponse {
// Columns of the physical table, including system columns (id, deleted, // Columns of the physical table, including system columns (id, deleted,
// created_at), user-defined columns, and any foreign-key columns such as // created_at), user-defined columns, and any foreign-key columns such as

View File

@@ -2,32 +2,56 @@
syntax = "proto3"; syntax = "proto3";
package komp_ac.table_validation; 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 // Request validation rules for a table
message GetTableValidationRequest { message GetTableValidationRequest {
string profileName = 1; string profileName = 1;
string tableName = 2; string tableName = 2;
} }
// Response with field-level validations; if a field is omitted, // Response with field-level validations for the whole table.
// no validation is applied (default unspecified). // If a field is omitted, no validation configuration exists for that field.
message TableValidationResponse { message TableValidationResponse {
repeated FieldValidation fields = 1; 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 { message FieldValidation {
// MUST match your frontend FormState.dataKey for the column // MUST match your frontend FormState.dataKey for the column
string dataKey = 1; string dataKey = 1;
// Current: only CharacterLimits. More rules can be added later. // Validation 1: length and counting rules.
CharacterLimits limits = 10; CharacterLimits limits = 10;
// Future expansion:
PatternRules pattern = 11; // Validation 2 // Validation 2: position-based character constraints.
optional CustomFormatter formatter = 14; // Validation 4 custom formatting logic 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; 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; bool required = 4;
// Once locked, this field's validation config cannot be changed.
bool locked = 15;
} }
// Character length counting mode // Character length counting mode
@@ -38,7 +62,8 @@ enum CountMode {
DISPLAY_WIDTH = 3; DISPLAY_WIDTH = 3;
} }
// Character limit validation (Validation 1) // Character limit validation (Validation 1).
// These rules map directly to canvas CharacterLimits.
message CharacterLimits { message CharacterLimits {
// When zero, the field is considered "not set". If both min/max are zero, // When zero, the field is considered "not set". If both min/max are zero,
// the server should avoid sending this FieldValidation (no validation). // the server should avoid sending this FieldValidation (no validation).
@@ -51,39 +76,75 @@ message CharacterLimits {
CountMode countMode = 4; // defaults to CHARS if unspecified 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 { message DisplayMask {
string pattern = 1; // e.g., "(###) ###-####" or "####-##-##" string pattern = 1; // e.g., "(###) ###-####" or "####-##-##"
string input_char = 2; // e.g., "#" string input_char = 2; // e.g., "#"
optional string template_char = 3; // 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 { message PatternRule {
// Range descriptor: how far the rule applies PatternPosition position = 1;
// Examples: CharacterConstraint constraint = 2;
// - "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;
} }
message CustomFormatter { // Exact-value whitelist configuration.
// Formatter type identifier; handled clientside. // This maps to canvas AllowedValues semantics.
// Examples: "PSCFormatter", "PhoneFormatter", "CreditCardFormatter", "DateFormatter" message AllowedValues {
string type = 1; repeated string values = 1;
bool allow_empty = 2;
// Optional freetext note or parameters (e.g. locale, pattern) bool case_insensitive = 3;
optional string description = 2;
} }
// Collection of pattern rules for one field // Collection of pattern rules for one field.
message PatternRules { message PatternRules {
// All rules that make up the validation logic // All rules that make up the validation logic
repeated PatternRule rules = 1; repeated PatternRule rules = 1;
@@ -92,11 +153,31 @@ message PatternRules {
optional string description = 2; optional string description = 2;
} }
// Service to fetch validations for a table // Service for storing and fetching field-validation definitions.
service TableValidationService { service TableValidationService {
rpc GetTableValidation(GetTableValidationRequest) returns (TableValidationResponse); rpc GetTableValidation(GetTableValidationRequest) returns (TableValidationResponse);
// Upsert a single field validation definition.
rpc UpdateFieldValidation(UpdateFieldValidationRequest) returns (UpdateFieldValidationResponse); 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 { message UpdateFieldValidationRequest {
@@ -110,3 +191,130 @@ message UpdateFieldValidationResponse {
bool success = 1; bool success = 1;
string message = 2; 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 // - If the physical table is missing but the definition exists, returns INTERNAL
rpc PostTableData(PostTableDataRequest) returns (PostTableDataResponse); 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. // Update existing row data with strict type binding and script validation.
// //
// Behavior: // Behavior:
@@ -124,6 +135,36 @@ message PostTableDataResponse {
int64 inserted_id = 3; 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. // Update an existing row.
message PutTableDataRequest { message PutTableDataRequest {
// Required. Profile (schema) name. // Required. Profile (schema) name.

Binary file not shown.

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,25 @@
// This file is @generated by prost-build. // This file is @generated by prost-build.
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct ColumnConstraint {
#[prost(string, tag = "1")]
pub column: ::prost::alloc::string::String,
#[prost(string, tag = "2")]
pub query: ::prost::alloc::string::String,
#[prost(enumeration = "MatchMode", tag = "3")]
pub mode: i32,
}
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct SearchRequest { pub struct SearchRequest {
#[prost(string, tag = "1")] #[prost(string, tag = "1")]
pub table_name: ::prost::alloc::string::String, pub profile_name: ::prost::alloc::string::String,
#[prost(string, tag = "2")] #[prost(string, optional, tag = "2")]
pub query: ::prost::alloc::string::String, pub table_name: ::core::option::Option<::prost::alloc::string::String>,
#[prost(string, tag = "3")]
pub free_query: ::prost::alloc::string::String,
#[prost(message, repeated, tag = "4")]
pub must: ::prost::alloc::vec::Vec<ColumnConstraint>,
#[prost(uint32, optional, tag = "5")]
pub limit: ::core::option::Option<u32>,
} }
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct SearchResponse { pub struct SearchResponse {
@@ -22,6 +37,37 @@ pub mod search_response {
pub score: f32, pub score: f32,
#[prost(string, tag = "3")] #[prost(string, tag = "3")]
pub content_json: ::prost::alloc::string::String, pub content_json: ::prost::alloc::string::String,
#[prost(string, tag = "4")]
pub table_name: ::prost::alloc::string::String,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
#[repr(i32)]
pub enum MatchMode {
Unspecified = 0,
Fuzzy = 1,
Exact = 2,
}
impl MatchMode {
/// String value of the enum field names used in the ProtoBuf definition.
///
/// The values are not transformed in any way and thus are considered stable
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
pub fn as_str_name(&self) -> &'static str {
match self {
Self::Unspecified => "MATCH_MODE_UNSPECIFIED",
Self::Fuzzy => "MATCH_MODE_FUZZY",
Self::Exact => "MATCH_MODE_EXACT",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
match value {
"MATCH_MODE_UNSPECIFIED" => Some(Self::Unspecified),
"MATCH_MODE_FUZZY" => Some(Self::Fuzzy),
"MATCH_MODE_EXACT" => Some(Self::Exact),
_ => None,
}
} }
} }
/// Generated client implementations. /// Generated client implementations.
@@ -115,7 +161,7 @@ pub mod searcher_client {
self.inner = self.inner.max_encoding_message_size(limit); self.inner = self.inner.max_encoding_message_size(limit);
self self
} }
pub async fn search_table( pub async fn search(
&mut self, &mut self,
request: impl tonic::IntoRequest<super::SearchRequest>, request: impl tonic::IntoRequest<super::SearchRequest>,
) -> std::result::Result<tonic::Response<super::SearchResponse>, tonic::Status> { ) -> std::result::Result<tonic::Response<super::SearchResponse>, tonic::Status> {
@@ -127,13 +173,13 @@ pub mod searcher_client {
format!("Service was not ready: {}", e.into()), 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( let path = http::uri::PathAndQuery::from_static(
"/komp_ac.search.Searcher/SearchTable", "/komp_ac.search.Searcher/Search",
); );
let mut req = request.into_request(); let mut req = request.into_request();
req.extensions_mut() req.extensions_mut()
.insert(GrpcMethod::new("komp_ac.search.Searcher", "SearchTable")); .insert(GrpcMethod::new("komp_ac.search.Searcher", "Search"));
self.inner.unary(req, path, codec).await self.inner.unary(req, path, codec).await
} }
} }
@@ -151,7 +197,7 @@ pub mod searcher_server {
/// Generated trait containing gRPC methods that should be implemented for use with SearcherServer. /// Generated trait containing gRPC methods that should be implemented for use with SearcherServer.
#[async_trait] #[async_trait]
pub trait Searcher: std::marker::Send + std::marker::Sync + 'static { pub trait Searcher: std::marker::Send + std::marker::Sync + 'static {
async fn search_table( async fn search(
&self, &self,
request: tonic::Request<super::SearchRequest>, request: tonic::Request<super::SearchRequest>,
) -> std::result::Result<tonic::Response<super::SearchResponse>, tonic::Status>; ) -> std::result::Result<tonic::Response<super::SearchResponse>, tonic::Status>;
@@ -232,11 +278,11 @@ pub mod searcher_server {
} }
fn call(&mut self, req: http::Request<B>) -> Self::Future { fn call(&mut self, req: http::Request<B>) -> Self::Future {
match req.uri().path() { match req.uri().path() {
"/komp_ac.search.Searcher/SearchTable" => { "/komp_ac.search.Searcher/Search" => {
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
struct SearchTableSvc<T: Searcher>(pub Arc<T>); struct SearchSvc<T: Searcher>(pub Arc<T>);
impl<T: Searcher> tonic::server::UnaryService<super::SearchRequest> impl<T: Searcher> tonic::server::UnaryService<super::SearchRequest>
for SearchTableSvc<T> { for SearchSvc<T> {
type Response = super::SearchResponse; type Response = super::SearchResponse;
type Future = BoxFuture< type Future = BoxFuture<
tonic::Response<Self::Response>, tonic::Response<Self::Response>,
@@ -248,7 +294,7 @@ pub mod searcher_server {
) -> Self::Future { ) -> Self::Future {
let inner = Arc::clone(&self.0); let inner = Arc::clone(&self.0);
let fut = async move { let fut = async move {
<T as Searcher>::search_table(&inner, request).await <T as Searcher>::search(&inner, request).await
}; };
Box::pin(fut) Box::pin(fut)
} }
@@ -259,8 +305,8 @@ pub mod searcher_server {
let max_encoding_message_size = self.max_encoding_message_size; let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone(); let inner = self.inner.clone();
let fut = async move { let fut = async move {
let method = SearchTableSvc(inner); let method = SearchSvc(inner);
let codec = tonic::codec::ProstCodec::default(); let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec) let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config( .apply_compression_config(
accept_compression_encodings, accept_compression_encodings,

View File

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

View File

@@ -1,10 +1,10 @@
// This file is @generated by prost-build. // This file is @generated by prost-build.
/// A single link to another table within the same profile (schema). /// A single link to another table within the same profile (schema).
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct TableLink { pub struct TableLink {
/// Name of an existing table within the same profile to link to. /// 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. /// That column references "<linked>"(id) and adds an index automatically.
#[prost(string, tag = "1")] #[prost(string, tag = "1")]
pub linked_table_name: ::prost::alloc::string::String, pub linked_table_name: ::prost::alloc::string::String,
@@ -20,12 +20,12 @@ pub struct TableLink {
pub struct PostTableDefinitionRequest { pub struct PostTableDefinitionRequest {
/// Table name to create inside the target profile. /// Table name to create inside the target profile.
/// Must be lowercase, alphanumeric with underscores, /// Must be lowercase, alphanumeric with underscores,
/// start with a letter, and be <= 63 chars. /// start with a letter, and be \<= 63 chars.
/// Forbidden names: "id", "deleted", "created_at", or ending in "_id". /// Forbidden names: "id", "deleted", "created_at", or ending in "\_id".
#[prost(string, tag = "1")] #[prost(string, tag = "1")]
pub table_name: ::prost::alloc::string::String, pub table_name: ::prost::alloc::string::String,
/// List of links (foreign keys) to existing tables in the same profile. /// 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")] #[prost(message, repeated, tag = "2")]
pub links: ::prost::alloc::vec::Vec<TableLink>, pub links: ::prost::alloc::vec::Vec<TableLink>,
/// List of user-defined columns (adds to system/id/fk columns). /// 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>, pub columns: ::prost::alloc::vec::Vec<ColumnDefinition>,
/// List of column names to be indexed (must match existing user-defined columns). /// List of column names to be indexed (must match existing user-defined columns).
/// Indexes can target only user-defined columns; system columns ("id", "deleted", /// 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. /// have indexes. Requests trying to index those columns are rejected.
#[prost(string, repeated, tag = "4")] #[prost(string, repeated, tag = "4")]
pub indexes: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, pub indexes: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
/// Name of profile (Postgres schema) where the table will be created. /// Name of profile (Postgres schema) where the table will be created.
/// Same naming rules as table_name; cannot collide with reserved schemas /// 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")] #[prost(string, tag = "5")]
pub profile_name: ::prost::alloc::string::String, 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(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)] #[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 { pub struct ColumnDefinition {
/// Column name that follows the same validation rules as table_name. /// Column name that follows the same validation rules as table_name.
/// Must be lowercase, start with a letter, no uppercase characters, /// 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")] #[prost(string, tag = "1")]
pub name: ::prost::alloc::string::String, pub name: ::prost::alloc::string::String,
/// Logical column type. Supported values (case-insensitive): /// Logical column type. Supported values (case-insensitive):
@@ -68,7 +85,7 @@ pub struct ColumnDefinition {
} }
/// Response after table creation (success + DDL preview). /// Response after table creation (success + DDL preview).
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct TableDefinitionResponse { pub struct TableDefinitionResponse {
/// True if all DB changes and metadata inserts succeeded. /// True if all DB changes and metadata inserts succeeded.
#[prost(bool, tag = "1")] #[prost(bool, tag = "1")]
@@ -87,7 +104,7 @@ pub struct ProfileTreeResponse {
/// Nested message and enum types in `ProfileTreeResponse`. /// Nested message and enum types in `ProfileTreeResponse`.
pub mod profile_tree_response { pub mod profile_tree_response {
/// Table entry in a profile. /// Table entry in a profile.
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct Table { pub struct Table {
/// Internal ID from table_definitions.id (metadata record). /// Internal ID from table_definitions.id (metadata record).
#[prost(int64, tag = "1")] #[prost(int64, tag = "1")]
@@ -110,8 +127,109 @@ pub mod profile_tree_response {
pub tables: ::prost::alloc::vec::Vec<Table>, pub tables: ::prost::alloc::vec::Vec<Table>,
} }
} }
/// Request to delete one table definition entirely. /// Request to fetch all tables, columns and scripts for a profile.
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct GetProfileDetailsRequest {
/// Profile (schema) name to fetch details for.
#[prost(string, tag = "1")]
pub profile_name: ::prost::alloc::string::String,
}
/// Response with all tables, columns and scripts for a profile.
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct GetProfileDetailsResponse {
#[prost(string, tag = "1")]
pub profile_name: ::prost::alloc::string::String,
#[prost(message, repeated, tag = "2")]
pub tables: ::prost::alloc::vec::Vec<TableDetail>,
}
/// Request to fetch recorded column alias rename history for one profile.
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct GetColumnAliasRenameHistoryRequest {
#[prost(string, tag = "1")]
pub profile_name: ::prost::alloc::string::String,
/// Optional filter. When omitted, returns all tables in the profile.
#[prost(int64, optional, tag = "2")]
pub table_definition_id: ::core::option::Option<i64>,
}
/// One recorded column alias rename.
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct ColumnAliasRenameHistoryEntry {
#[prost(int64, tag = "1")]
pub id: i64,
#[prost(string, tag = "2")]
pub profile_name: ::prost::alloc::string::String,
#[prost(int64, tag = "3")]
pub table_definition_id: i64,
#[prost(string, tag = "4")]
pub table_name: ::prost::alloc::string::String,
#[prost(string, tag = "5")]
pub old_column_name: ::prost::alloc::string::String,
#[prost(string, tag = "6")]
pub new_column_name: ::prost::alloc::string::String,
#[prost(string, tag = "7")]
pub created_at: ::prost::alloc::string::String,
}
/// Response with stored column alias rename history rows.
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GetColumnAliasRenameHistoryResponse {
#[prost(string, tag = "1")]
pub profile_name: ::prost::alloc::string::String,
#[prost(message, repeated, tag = "2")]
pub entries: ::prost::alloc::vec::Vec<ColumnAliasRenameHistoryEntry>,
}
/// Describes a table with its columns and associated scripts.
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct TableDetail {
#[prost(string, tag = "1")]
pub name: ::prost::alloc::string::String,
#[prost(int64, tag = "2")]
pub id: i64,
#[prost(message, repeated, tag = "3")]
pub columns: ::prost::alloc::vec::Vec<ColumnDefinition>,
#[prost(message, repeated, tag = "4")]
pub scripts: ::prost::alloc::vec::Vec<ScriptInfo>,
}
/// A script that targets a specific column in a table.
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct ScriptInfo {
#[prost(int64, tag = "1")]
pub script_id: i64,
#[prost(string, tag = "2")]
pub target_column: ::prost::alloc::string::String,
#[prost(string, tag = "3")]
pub target_column_type: ::prost::alloc::string::String,
#[prost(string, tag = "4")]
pub script: ::prost::alloc::string::String,
#[prost(string, tag = "5")]
pub description: ::prost::alloc::string::String,
}
/// Request to rename one user-visible column alias in a table.
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct RenameColumnAliasRequest {
#[prost(string, tag = "1")]
pub profile_name: ::prost::alloc::string::String,
#[prost(string, tag = "2")]
pub table_name: ::prost::alloc::string::String,
#[prost(string, tag = "3")]
pub old_column_name: ::prost::alloc::string::String,
#[prost(string, tag = "4")]
pub new_column_name: ::prost::alloc::string::String,
}
/// Response after renaming one column alias.
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct RenameColumnAliasResponse {
#[prost(bool, tag = "1")]
pub success: bool,
#[prost(string, tag = "2")]
pub message: ::prost::alloc::string::String,
}
/// Request to delete one table definition entirely.
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct DeleteTableRequest { pub struct DeleteTableRequest {
/// Profile (schema) name owning the table (must exist). /// Profile (schema) name owning the table (must exist).
#[prost(string, tag = "1")] #[prost(string, tag = "1")]
@@ -122,7 +240,7 @@ pub struct DeleteTableRequest {
pub table_name: ::prost::alloc::string::String, pub table_name: ::prost::alloc::string::String,
} }
/// Response after table deletion. /// Response after table deletion.
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct DeleteTableResponse { pub struct DeleteTableResponse {
/// True if table and metadata were successfully deleted in one transaction. /// True if table and metadata were successfully deleted in one transaction.
#[prost(bool, tag = "1")] #[prost(bool, tag = "1")]
@@ -244,7 +362,7 @@ pub mod table_definition_client {
format!("Service was not ready: {}", e.into()), 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( let path = http::uri::PathAndQuery::from_static(
"/komp_ac.table_definition.TableDefinition/PostTableDefinition", "/komp_ac.table_definition.TableDefinition/PostTableDefinition",
); );
@@ -258,6 +376,37 @@ pub mod table_definition_client {
); );
self.inner.unary(req, path, codec).await 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. /// Lists all profiles (schemas) and their tables with declared dependencies.
/// This provides a tree-like overview of table relationships. /// This provides a tree-like overview of table relationships.
pub async fn get_profile_tree( pub async fn get_profile_tree(
@@ -275,7 +424,7 @@ pub mod table_definition_client {
format!("Service was not ready: {}", e.into()), 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( let path = http::uri::PathAndQuery::from_static(
"/komp_ac.table_definition.TableDefinition/GetProfileTree", "/komp_ac.table_definition.TableDefinition/GetProfileTree",
); );
@@ -289,6 +438,97 @@ pub mod table_definition_client {
); );
self.inner.unary(req, path, codec).await self.inner.unary(req, path, codec).await
} }
/// Fetches all tables with their columns and scripts for a specific profile.
/// Pure data retrieval - no business logic.
pub async fn get_profile_details(
&mut self,
request: impl tonic::IntoRequest<super::GetProfileDetailsRequest>,
) -> std::result::Result<
tonic::Response<super::GetProfileDetailsResponse>,
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/GetProfileDetails",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"komp_ac.table_definition.TableDefinition",
"GetProfileDetails",
),
);
self.inner.unary(req, path, codec).await
}
/// Returns the stored rename history for column aliases in one profile.
pub async fn get_column_alias_rename_history(
&mut self,
request: impl tonic::IntoRequest<super::GetColumnAliasRenameHistoryRequest>,
) -> std::result::Result<
tonic::Response<super::GetColumnAliasRenameHistoryResponse>,
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/GetColumnAliasRenameHistory",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"komp_ac.table_definition.TableDefinition",
"GetColumnAliasRenameHistory",
),
);
self.inner.unary(req, path, codec).await
}
/// Renames a user-visible column alias while keeping the physical column unchanged.
pub async fn rename_column_alias(
&mut self,
request: impl tonic::IntoRequest<super::RenameColumnAliasRequest>,
) -> std::result::Result<
tonic::Response<super::RenameColumnAliasResponse>,
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/RenameColumnAlias",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"komp_ac.table_definition.TableDefinition",
"RenameColumnAlias",
),
);
self.inner.unary(req, path, codec).await
}
/// Drops a table and its metadata, then deletes the profile if it becomes empty. /// Drops a table and its metadata, then deletes the profile if it becomes empty.
pub async fn delete_table( pub async fn delete_table(
&mut self, &mut self,
@@ -305,7 +545,7 @@ pub mod table_definition_client {
format!("Service was not ready: {}", e.into()), 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( let path = http::uri::PathAndQuery::from_static(
"/komp_ac.table_definition.TableDefinition/DeleteTable", "/komp_ac.table_definition.TableDefinition/DeleteTable",
); );
@@ -344,6 +584,15 @@ pub mod table_definition_server {
tonic::Response<super::TableDefinitionResponse>, tonic::Response<super::TableDefinitionResponse>,
tonic::Status, 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. /// Lists all profiles (schemas) and their tables with declared dependencies.
/// This provides a tree-like overview of table relationships. /// This provides a tree-like overview of table relationships.
async fn get_profile_tree( async fn get_profile_tree(
@@ -353,6 +602,31 @@ pub mod table_definition_server {
tonic::Response<super::ProfileTreeResponse>, tonic::Response<super::ProfileTreeResponse>,
tonic::Status, tonic::Status,
>; >;
/// Fetches all tables with their columns and scripts for a specific profile.
/// Pure data retrieval - no business logic.
async fn get_profile_details(
&self,
request: tonic::Request<super::GetProfileDetailsRequest>,
) -> std::result::Result<
tonic::Response<super::GetProfileDetailsResponse>,
tonic::Status,
>;
/// Returns the stored rename history for column aliases in one profile.
async fn get_column_alias_rename_history(
&self,
request: tonic::Request<super::GetColumnAliasRenameHistoryRequest>,
) -> std::result::Result<
tonic::Response<super::GetColumnAliasRenameHistoryResponse>,
tonic::Status,
>;
/// Renames a user-visible column alias while keeping the physical column unchanged.
async fn rename_column_alias(
&self,
request: tonic::Request<super::RenameColumnAliasRequest>,
) -> std::result::Result<
tonic::Response<super::RenameColumnAliasResponse>,
tonic::Status,
>;
/// Drops a table and its metadata, then deletes the profile if it becomes empty. /// Drops a table and its metadata, then deletes the profile if it becomes empty.
async fn delete_table( async fn delete_table(
&self, &self,
@@ -476,7 +750,53 @@ pub mod table_definition_server {
let inner = self.inner.clone(); let inner = self.inner.clone();
let fut = async move { let fut = async move {
let method = PostTableDefinitionSvc(inner); 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) let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config( .apply_compression_config(
accept_compression_encodings, accept_compression_encodings,
@@ -522,7 +842,153 @@ pub mod table_definition_server {
let inner = self.inner.clone(); let inner = self.inner.clone();
let fut = async move { let fut = async move {
let method = GetProfileTreeSvc(inner); 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,
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/GetProfileDetails" => {
#[allow(non_camel_case_types)]
struct GetProfileDetailsSvc<T: TableDefinition>(pub Arc<T>);
impl<
T: TableDefinition,
> tonic::server::UnaryService<super::GetProfileDetailsRequest>
for GetProfileDetailsSvc<T> {
type Response = super::GetProfileDetailsResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::GetProfileDetailsRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as TableDefinition>::get_profile_details(&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 = GetProfileDetailsSvc(inner);
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/GetColumnAliasRenameHistory" => {
#[allow(non_camel_case_types)]
struct GetColumnAliasRenameHistorySvc<T: TableDefinition>(
pub Arc<T>,
);
impl<
T: TableDefinition,
> tonic::server::UnaryService<
super::GetColumnAliasRenameHistoryRequest,
> for GetColumnAliasRenameHistorySvc<T> {
type Response = super::GetColumnAliasRenameHistoryResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<
super::GetColumnAliasRenameHistoryRequest,
>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as TableDefinition>::get_column_alias_rename_history(
&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 = GetColumnAliasRenameHistorySvc(inner);
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/RenameColumnAlias" => {
#[allow(non_camel_case_types)]
struct RenameColumnAliasSvc<T: TableDefinition>(pub Arc<T>);
impl<
T: TableDefinition,
> tonic::server::UnaryService<super::RenameColumnAliasRequest>
for RenameColumnAliasSvc<T> {
type Response = super::RenameColumnAliasResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::RenameColumnAliasRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as TableDefinition>::rename_column_alias(&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 = RenameColumnAliasSvc(inner);
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec) let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config( .apply_compression_config(
accept_compression_encodings, accept_compression_encodings,
@@ -567,7 +1033,7 @@ pub mod table_definition_server {
let inner = self.inner.clone(); let inner = self.inner.clone();
let fut = async move { let fut = async move {
let method = DeleteTableSvc(inner); let method = DeleteTableSvc(inner);
let codec = tonic::codec::ProstCodec::default(); let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec) let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config( .apply_compression_config(
accept_compression_encodings, accept_compression_encodings,

View File

@@ -1,7 +1,7 @@
// This file is @generated by prost-build. // This file is @generated by prost-build.
/// Request to create or update a script bound to a specific table and column. /// Request to create or update a script bound to a specific table and column.
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct PostTableScriptRequest { pub struct PostTableScriptRequest {
/// Required. The metadata ID from table_definitions.id that identifies the /// Required. The metadata ID from table_definitions.id that identifies the
/// table this script belongs to. The table must exist; its schema determines /// 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, pub target_column: ::prost::alloc::string::String,
/// Required. The script in the Steel DSL (S-expression style). /// Required. The script in the Steel DSL (S-expression style).
/// Syntax requirements: /// Syntax requirements:
/// - Non-empty, must start with '(' ///
/// - Balanced parentheses /// * Non-empty, must start with '('
/// * Balanced parentheses
/// ///
/// Referencing data: /// 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 "table_name" "column_name")
/// (steel_get_column_with_index "table_name" index "column_name") /// (steel_get_column_with_index "table_name" index "column_name")
/// • index must be a non-negative integer literal /// • index must be a non-negative integer literal
/// • self-references are allowed without links /// • self-references are allowed without links
/// • other tables require an explicit link from the source table /// • other tables require an explicit link from the source table
/// (table_definition_links) or the request fails /// (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 ...") /// (steel_query_sql "SELECT ...")
/// • Basic checks disallow operations that imply prohibited types, /// • Basic checks disallow operations that imply prohibited types,
/// e.g., EXTRACT(…), DATE_PART(…), ::DATE, ::TIMESTAMPTZ, ::BIGINT, CAST(…) /// 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 /// (get-var "column_name") is treated as referencing the current table
/// ///
/// Math operations: /// 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 /// 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 /// BIGINT, TEXT, BOOLEAN, DATE, TIMESTAMPTZ
/// ///
/// Dependency tracking and cycles: /// 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 /// 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")] #[prost(string, tag = "3")]
pub script: ::prost::alloc::string::String, pub script: ::prost::alloc::string::String,
/// Optional. Free-text description stored alongside the script (no functional effect). /// 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. /// Response after creating or updating a script.
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct TableScriptResponse { pub struct TableScriptResponse {
/// The ID of the script record in table_scripts (new or existing on upsert). /// The ID of the script record in table_scripts (new or existing on upsert).
#[prost(int64, tag = "1")] #[prost(int64, tag = "1")]
pub id: i64, pub id: i64,
/// Human-readable warnings concatenated into a single string. Possible messages: /// 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 /// * Warning if the script references itself (may affect first population)
/// - Info about number of structured linked-table accesses /// * Count of raw SQL queries present
/// - Warning if many dependencies may affect performance /// * Info about number of structured linked-table accesses
/// * Warning if many dependencies may affect performance
#[prost(string, tag = "2")] #[prost(string, tag = "2")]
pub warnings: ::prost::alloc::string::String, pub warnings: ::prost::alloc::string::String,
} }
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct GetTableScriptsRequest { pub struct GetTableScriptsRequest {
/// Required. Profile (schema) name. /// Required. Profile (schema) name.
#[prost(string, tag = "1")] #[prost(string, tag = "1")]
@@ -97,7 +102,7 @@ pub struct StoredTableScript {
#[prost(message, repeated, tag = "6")] #[prost(message, repeated, tag = "6")]
pub dependencies: ::prost::alloc::vec::Vec<ScriptDependency>, pub dependencies: ::prost::alloc::vec::Vec<ScriptDependency>,
} }
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct ScriptDependency { pub struct ScriptDependency {
#[prost(string, tag = "1")] #[prost(string, tag = "1")]
pub target_table: ::prost::alloc::string::String, pub target_table: ::prost::alloc::string::String,
@@ -124,14 +129,15 @@ pub mod table_script_client {
/// Manages column-computation scripts for user-defined tables. /// Manages column-computation scripts for user-defined tables.
/// Each script belongs to a single table (table_definition_id) and populates /// Each script belongs to a single table (table_definition_id) and populates
/// exactly one target column in that table. The server: /// 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 script syntax (non-empty, balanced parentheses, starts with '(')
/// - Validates column/type usage inside math expressions /// * Validates the target column (exists, not a system column, allowed type)
/// - Validates referenced tables/columns against the schema /// * Validates column/type usage inside math expressions
/// - Enforces link constraints for structured access (see notes below) /// * Validates referenced tables/columns against the schema
/// - Analyzes dependencies and prevents cycles across the schema /// * Enforces link constraints for structured access (see notes below)
/// - Transforms the script to decimal-safe math (steel_decimal) /// * Analyzes dependencies and prevents cycles across the schema
/// - Upserts into table_scripts and records dependencies in script_dependencies /// * Transforms the script to decimal-safe math (steel_decimal)
/// * Upserts into table_scripts and records dependencies in script_dependencies
/// The whole operation is transactional. /// The whole operation is transactional.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TableScriptClient<T> { 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. /// Create or update a script for a specific table and target column.
/// ///
/// Behavior: /// 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) /// and all referenced tables/columns (must exist in same schema)
/// - Validates math operations: prohibits using certain data types in math /// * Validates math operations: prohibits using certain data types in math
/// - Enforces link constraints for structured table access: /// * Enforces link constraints for structured table access:
/// • Allowed always: self-references (same table) /// • Allowed always: self-references (same table)
/// • Structured access via steel_get_column / steel_get_column_with_index /// • Structured access via steel_get_column / steel_get_column_with_index
/// requires an explicit link in table_definition_links /// requires an explicit link in table_definition_links
/// • Raw SQL access via steel_query_sql is permitted (still validated) /// • 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) /// (self-references are allowed and not treated as cycles)
/// - Transforms the script to decimal-safe operations (steel_decimal) /// * Transforms the script to decimal-safe operations (steel_decimal)
/// - UPSERTS into table_scripts on (table_definitions_id, target_column) /// * UPSERTS into table_scripts on (table_definitions_id, target_column)
/// and saves a normalized dependency list into script_dependencies /// and saves a normalized dependency list into script_dependencies
pub async fn post_table_script( pub async fn post_table_script(
&mut self, &mut self,
@@ -245,7 +252,7 @@ pub mod table_script_client {
format!("Service was not ready: {}", e.into()), 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( let path = http::uri::PathAndQuery::from_static(
"/komp_ac.table_script.TableScript/PostTableScript", "/komp_ac.table_script.TableScript/PostTableScript",
); );
@@ -262,10 +269,11 @@ pub mod table_script_client {
/// Fetch all stored scripts for a specific table. /// Fetch all stored scripts for a specific table.
/// ///
/// Behavior: /// Behavior:
/// - Resolves the table from (profile_name, table_name) ///
/// - Returns the stored, transformed script from table_scripts /// * Resolves the table from (profile_name, table_name)
/// - Includes normalized dependency metadata from script_dependencies /// * Returns the stored, transformed script from table_scripts
/// - Returns an empty scripts list when the table has no 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( pub async fn get_table_scripts(
&mut self, &mut self,
request: impl tonic::IntoRequest<super::GetTableScriptsRequest>, request: impl tonic::IntoRequest<super::GetTableScriptsRequest>,
@@ -281,7 +289,7 @@ pub mod table_script_client {
format!("Service was not ready: {}", e.into()), 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( let path = http::uri::PathAndQuery::from_static(
"/komp_ac.table_script.TableScript/GetTableScripts", "/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. /// Create or update a script for a specific table and target column.
/// ///
/// Behavior: /// 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) /// and all referenced tables/columns (must exist in same schema)
/// - Validates math operations: prohibits using certain data types in math /// * Validates math operations: prohibits using certain data types in math
/// - Enforces link constraints for structured table access: /// * Enforces link constraints for structured table access:
/// • Allowed always: self-references (same table) /// • Allowed always: self-references (same table)
/// • Structured access via steel_get_column / steel_get_column_with_index /// • Structured access via steel_get_column / steel_get_column_with_index
/// requires an explicit link in table_definition_links /// requires an explicit link in table_definition_links
/// • Raw SQL access via steel_query_sql is permitted (still validated) /// • 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) /// (self-references are allowed and not treated as cycles)
/// - Transforms the script to decimal-safe operations (steel_decimal) /// * Transforms the script to decimal-safe operations (steel_decimal)
/// - UPSERTS into table_scripts on (table_definitions_id, target_column) /// * UPSERTS into table_scripts on (table_definitions_id, target_column)
/// and saves a normalized dependency list into script_dependencies /// and saves a normalized dependency list into script_dependencies
async fn post_table_script( async fn post_table_script(
&self, &self,
@@ -337,10 +346,11 @@ pub mod table_script_server {
/// Fetch all stored scripts for a specific table. /// Fetch all stored scripts for a specific table.
/// ///
/// Behavior: /// Behavior:
/// - Resolves the table from (profile_name, table_name) ///
/// - Returns the stored, transformed script from table_scripts /// * Resolves the table from (profile_name, table_name)
/// - Includes normalized dependency metadata from script_dependencies /// * Returns the stored, transformed script from table_scripts
/// - Returns an empty scripts list when the table has no scripts /// * Includes normalized dependency metadata from script_dependencies
/// * Returns an empty scripts list when the table has no scripts
async fn get_table_scripts( async fn get_table_scripts(
&self, &self,
request: tonic::Request<super::GetTableScriptsRequest>, request: tonic::Request<super::GetTableScriptsRequest>,
@@ -352,14 +362,15 @@ pub mod table_script_server {
/// Manages column-computation scripts for user-defined tables. /// Manages column-computation scripts for user-defined tables.
/// Each script belongs to a single table (table_definition_id) and populates /// Each script belongs to a single table (table_definition_id) and populates
/// exactly one target column in that table. The server: /// 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 script syntax (non-empty, balanced parentheses, starts with '(')
/// - Validates column/type usage inside math expressions /// * Validates the target column (exists, not a system column, allowed type)
/// - Validates referenced tables/columns against the schema /// * Validates column/type usage inside math expressions
/// - Enforces link constraints for structured access (see notes below) /// * Validates referenced tables/columns against the schema
/// - Analyzes dependencies and prevents cycles across the schema /// * Enforces link constraints for structured access (see notes below)
/// - Transforms the script to decimal-safe math (steel_decimal) /// * Analyzes dependencies and prevents cycles across the schema
/// - Upserts into table_scripts and records dependencies in script_dependencies /// * Transforms the script to decimal-safe math (steel_decimal)
/// * Upserts into table_scripts and records dependencies in script_dependencies
/// The whole operation is transactional. /// The whole operation is transactional.
#[derive(Debug)] #[derive(Debug)]
pub struct TableScriptServer<T> { pub struct TableScriptServer<T> {
@@ -467,7 +478,7 @@ pub mod table_script_server {
let inner = self.inner.clone(); let inner = self.inner.clone();
let fut = async move { let fut = async move {
let method = PostTableScriptSvc(inner); let method = PostTableScriptSvc(inner);
let codec = tonic::codec::ProstCodec::default(); let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec) let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config( .apply_compression_config(
accept_compression_encodings, accept_compression_encodings,
@@ -512,7 +523,7 @@ pub mod table_script_server {
let inner = self.inner.clone(); let inner = self.inner.clone();
let fut = async move { let fut = async move {
let method = GetTableScriptsSvc(inner); let method = GetTableScriptsSvc(inner);
let codec = tonic::codec::ProstCodec::default(); let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec) let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config( .apply_compression_config(
accept_compression_encodings, accept_compression_encodings,

View File

@@ -1,38 +1,49 @@
// This file is @generated by prost-build. // This file is @generated by prost-build.
/// Request identifying the profile (schema) and table to inspect. /// Request identifying the profile (schema) and tables to inspect.
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct GetTableStructureRequest { pub struct GetTableStructureRequest {
/// Required. Profile (PostgreSQL schema) name. Must exist in `schemas`. /// Required. Profile (PostgreSQL schema) name. Must exist in `schemas`.
#[prost(string, tag = "1")] #[prost(string, tag = "1")]
pub profile_name: ::prost::alloc::string::String, pub profile_name: ::prost::alloc::string::String,
/// Required. Table name within the profile. Must exist in `table_definitions` /// Required. Table names within the profile. Each must exist in
/// for the given profile. The physical table is then introspected via /// `table_definitions` for the given profile. The physical tables are then
/// information_schema. /// introspected via information_schema.
#[prost(string, tag = "2")] #[prost(string, repeated, tag = "2")]
pub table_name: ::prost::alloc::string::String, pub table_names: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
} }
/// Response with the ordered list of columns (by ordinal position). /// Batched response keyed by table name.
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GetTableStructureResponse {
/// Per-table physical column lists keyed by requested table name.
#[prost(map = "string, message", tag = "1")]
pub table_structures: ::std::collections::HashMap<
::prost::alloc::string::String,
TableStructureResponse,
>,
}
/// Response with the ordered list of columns (by ordinal position) for one table.
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct TableStructureResponse { pub struct TableStructureResponse {
/// Columns of the physical table, including system columns (id, deleted, /// Columns of the physical table, including system columns (id, deleted,
/// created_at), user-defined columns, and any foreign-key columns such as /// 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")] #[prost(message, repeated, tag = "1")]
pub columns: ::prost::alloc::vec::Vec<TableColumn>, pub columns: ::prost::alloc::vec::Vec<TableColumn>,
} }
/// One physical column entry as reported by information_schema. /// One physical column entry as reported by information_schema.
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct TableColumn { pub struct TableColumn {
/// Column name exactly as defined in PostgreSQL. /// Column name exactly as defined in PostgreSQL.
#[prost(string, tag = "1")] #[prost(string, tag = "1")]
pub name: ::prost::alloc::string::String, pub name: ::prost::alloc::string::String,
/// Normalized data type string derived from information_schema: /// 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 /// * VARCHAR(n) when udt_name='varchar' with character_maximum_length
/// - NUMERIC(p,s) when udt_name='numeric' with precision and scale /// * CHAR(n) when udt_name='bpchar' with character_maximum_length
/// - NUMERIC(p) when udt_name='numeric' with precision only /// * NUMERIC(p,s) when udt_name='numeric' with precision and scale
/// - <TYPE>\[\] for array types (udt_name starting with '_', e.g., INT\[\] ) /// * NUMERIC(p) when udt_name='numeric' with precision only
/// - Otherwise UPPER(udt_name), e.g., TEXT, BIGINT, TIMESTAMPTZ /// * <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)" /// Examples: "TEXT", "BIGINT", "VARCHAR(255)", "TIMESTAMPTZ", "NUMERIC(14,4)"
#[prost(string, tag = "2")] #[prost(string, tag = "2")]
pub data_type: ::prost::alloc::string::String, pub data_type: ::prost::alloc::string::String,
@@ -55,14 +66,14 @@ pub mod table_structure_service_client {
)] )]
use tonic::codegen::*; use tonic::codegen::*;
use tonic::codegen::http::Uri; use tonic::codegen::http::Uri;
/// Introspects the physical PostgreSQL table for a given logical table /// Introspects the physical PostgreSQL tables for one or more logical tables
/// (defined in table_definitions) and returns its column structure. /// (defined in table_definitions) and returns their column structures.
/// The server validates that: /// The server validates that:
/// - The profile (schema) exists in `schemas` ///
/// - The table is defined for that profile in `table_definitions` /// * The profile (schema) exists in `schemas`
/// It then queries information_schema for the physical table and returns /// * Every table is defined for that profile in `table_definitions`
/// normalized column metadata. If the physical table is missing despite /// It then queries information_schema for the physical tables and returns
/// a definition, the response may contain an empty `columns` list. /// normalized column metadata.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TableStructureServiceClient<T> { pub struct TableStructureServiceClient<T> {
inner: tonic::client::Grpc<T>, inner: tonic::client::Grpc<T>,
@@ -144,20 +155,21 @@ pub mod table_structure_service_client {
self self
} }
/// Return the physical column list (name, normalized data_type, /// Return the physical column list (name, normalized data_type,
/// nullability, primary key flag) for a table in a profile. /// nullability, primary key flag) for one or more tables in a profile.
/// ///
/// Behavior: /// Behavior:
/// - NOT_FOUND if profile doesn't exist in `schemas` ///
/// - NOT_FOUND if table not defined for that profile in `table_definitions` /// * NOT_FOUND if profile doesn't exist in `schemas`
/// - Queries information_schema.columns ordered by ordinal position /// * NOT_FOUND if any table is not defined for that profile in `table_definitions`
/// - Normalizes data_type text (details under TableColumn.data_type) /// * Queries information_schema.columns ordered by ordinal position
/// - Returns an empty list if the table is validated but has no visible /// * Normalizes data_type text (details under TableColumn.data_type)
/// columns in information_schema (e.g., physical table missing) /// * 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( pub async fn get_table_structure(
&mut self, &mut self,
request: impl tonic::IntoRequest<super::GetTableStructureRequest>, request: impl tonic::IntoRequest<super::GetTableStructureRequest>,
) -> std::result::Result< ) -> std::result::Result<
tonic::Response<super::TableStructureResponse>, tonic::Response<super::GetTableStructureResponse>,
tonic::Status, tonic::Status,
> { > {
self.inner self.inner
@@ -168,7 +180,7 @@ pub mod table_structure_service_client {
format!("Service was not ready: {}", e.into()), 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( let path = http::uri::PathAndQuery::from_static(
"/komp_ac.table_structure.TableStructureService/GetTableStructure", "/komp_ac.table_structure.TableStructureService/GetTableStructure",
); );
@@ -198,31 +210,32 @@ pub mod table_structure_service_server {
#[async_trait] #[async_trait]
pub trait TableStructureService: std::marker::Send + std::marker::Sync + 'static { pub trait TableStructureService: std::marker::Send + std::marker::Sync + 'static {
/// Return the physical column list (name, normalized data_type, /// Return the physical column list (name, normalized data_type,
/// nullability, primary key flag) for a table in a profile. /// nullability, primary key flag) for one or more tables in a profile.
/// ///
/// Behavior: /// Behavior:
/// - NOT_FOUND if profile doesn't exist in `schemas` ///
/// - NOT_FOUND if table not defined for that profile in `table_definitions` /// * NOT_FOUND if profile doesn't exist in `schemas`
/// - Queries information_schema.columns ordered by ordinal position /// * NOT_FOUND if any table is not defined for that profile in `table_definitions`
/// - Normalizes data_type text (details under TableColumn.data_type) /// * Queries information_schema.columns ordered by ordinal position
/// - Returns an empty list if the table is validated but has no visible /// * Normalizes data_type text (details under TableColumn.data_type)
/// columns in information_schema (e.g., physical table missing) /// * Returns an error if any validated table has no visible columns in
/// information_schema (e.g., physical table missing)
async fn get_table_structure( async fn get_table_structure(
&self, &self,
request: tonic::Request<super::GetTableStructureRequest>, request: tonic::Request<super::GetTableStructureRequest>,
) -> std::result::Result< ) -> std::result::Result<
tonic::Response<super::TableStructureResponse>, tonic::Response<super::GetTableStructureResponse>,
tonic::Status, tonic::Status,
>; >;
} }
/// Introspects the physical PostgreSQL table for a given logical table /// Introspects the physical PostgreSQL tables for one or more logical tables
/// (defined in table_definitions) and returns its column structure. /// (defined in table_definitions) and returns their column structures.
/// The server validates that: /// The server validates that:
/// - The profile (schema) exists in `schemas` ///
/// - The table is defined for that profile in `table_definitions` /// * The profile (schema) exists in `schemas`
/// It then queries information_schema for the physical table and returns /// * Every table is defined for that profile in `table_definitions`
/// normalized column metadata. If the physical table is missing despite /// It then queries information_schema for the physical tables and returns
/// a definition, the response may contain an empty `columns` list. /// normalized column metadata.
#[derive(Debug)] #[derive(Debug)]
pub struct TableStructureServiceServer<T> { pub struct TableStructureServiceServer<T> {
inner: Arc<T>, inner: Arc<T>,
@@ -307,7 +320,7 @@ pub mod table_structure_service_server {
T: TableStructureService, T: TableStructureService,
> tonic::server::UnaryService<super::GetTableStructureRequest> > tonic::server::UnaryService<super::GetTableStructureRequest>
for GetTableStructureSvc<T> { for GetTableStructureSvc<T> {
type Response = super::TableStructureResponse; type Response = super::GetTableStructureResponse;
type Future = BoxFuture< type Future = BoxFuture<
tonic::Response<Self::Response>, tonic::Response<Self::Response>,
tonic::Status, tonic::Status,
@@ -334,7 +347,7 @@ pub mod table_structure_service_server {
let inner = self.inner.clone(); let inner = self.inner.clone();
let fut = async move { let fut = async move {
let method = GetTableStructureSvc(inner); let method = GetTableStructureSvc(inner);
let codec = tonic::codec::ProstCodec::default(); let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec) let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config( .apply_compression_config(
accept_compression_encodings, 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. /// Required. Key-value data for columns to insert.
/// ///
/// Allowed keys: /// 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 /// • "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: /// Type expectations by SQL type:
/// - TEXT: string value; empty string is treated as NULL ///
/// - BOOLEAN: bool value /// * TEXT: string value; empty string is treated as NULL
/// - TIMESTAMPTZ: ISO 8601/RFC 3339 string (parsed to TIMESTAMPTZ) /// * BOOLEAN: bool value
/// - INTEGER: number with no fractional part and within i32 range /// * TIMESTAMPTZ: ISO 8601/RFC 3339 string (parsed to TIMESTAMPTZ)
/// - BIGINT: number with no fractional part and within i64 range /// * INTEGER: number with no fractional part and within i32 range
/// - NUMERIC(p,s): string representation only; empty string becomes NULL /// * 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) /// (numbers for NUMERIC are rejected to avoid precision loss)
/// ///
/// Script validation rules: /// 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 /// and its provided value MUST equal the scripts computed value (type-aware
/// comparison, e.g., decimals are compared numerically). /// comparison, e.g., decimals are compared numerically).
/// ///
/// Notes: /// 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") /// certain fields like "telefon")
#[prost(map = "string, message", tag = "3")] #[prost(map = "string, message", tag = "3")]
pub data: ::std::collections::HashMap< pub data: ::std::collections::HashMap<
@@ -43,7 +47,7 @@ pub struct PostTableDataRequest {
>, >,
} }
/// Insert response. /// Insert response.
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct PostTableDataResponse { pub struct PostTableDataResponse {
/// True if the insert succeeded. /// True if the insert succeeded.
#[prost(bool, tag = "1")] #[prost(bool, tag = "1")]
@@ -55,6 +59,42 @@ pub struct PostTableDataResponse {
#[prost(int64, tag = "3")] #[prost(int64, tag = "3")]
pub inserted_id: i64, 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. /// Update an existing row.
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct PutTableDataRequest { pub struct PutTableDataRequest {
@@ -70,9 +110,10 @@ pub struct PutTableDataRequest {
/// Required. Columns to update (same typing rules as PostTableDataRequest.data). /// Required. Columns to update (same typing rules as PostTableDataRequest.data).
/// ///
/// Special script rules: /// 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). /// 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 /// to change compared to the current stored value, the update is rejected with
/// FAILED_PRECONDITION, instructing the caller to include X explicitly. /// FAILED_PRECONDITION, instructing the caller to include X explicitly.
/// ///
@@ -84,7 +125,7 @@ pub struct PutTableDataRequest {
>, >,
} }
/// Update response. /// Update response.
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct PutTableDataResponse { pub struct PutTableDataResponse {
/// True if the update succeeded (or no-op on empty data). /// True if the update succeeded (or no-op on empty data).
#[prost(bool, tag = "1")] #[prost(bool, tag = "1")]
@@ -97,7 +138,7 @@ pub struct PutTableDataResponse {
pub updated_id: i64, pub updated_id: i64,
} }
/// Soft-delete a single row. /// Soft-delete a single row.
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct DeleteTableDataRequest { pub struct DeleteTableDataRequest {
/// Required. Profile (schema) name. /// Required. Profile (schema) name.
#[prost(string, tag = "1")] #[prost(string, tag = "1")]
@@ -110,14 +151,14 @@ pub struct DeleteTableDataRequest {
pub record_id: i64, pub record_id: i64,
} }
/// Soft-delete response. /// Soft-delete response.
#[derive(Clone, Copy, PartialEq, ::prost::Message)] #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
pub struct DeleteTableDataResponse { pub struct DeleteTableDataResponse {
/// True if a row was marked deleted (id existed and was not already deleted). /// True if a row was marked deleted (id existed and was not already deleted).
#[prost(bool, tag = "1")] #[prost(bool, tag = "1")]
pub success: bool, pub success: bool,
} }
/// Fetch a single non-deleted row by id. /// Fetch a single non-deleted row by id.
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct GetTableDataRequest { pub struct GetTableDataRequest {
/// Required. Profile (schema) name. /// Required. Profile (schema) name.
#[prost(string, tag = "1")] #[prost(string, tag = "1")]
@@ -133,9 +174,10 @@ pub struct GetTableDataRequest {
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct GetTableDataResponse { pub struct GetTableDataResponse {
/// Map of column_name → stringified value for: /// Map of column_name → stringified value for:
/// - id, deleted ///
/// - all user-defined columns from the table definition /// * id, deleted
/// - FK columns named "<linked_table>_id" for each table link /// * 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 /// All values are returned as TEXT via col::TEXT and COALESCEed to empty string
/// (NULL becomes ""). The row is returned only if deleted = FALSE. /// (NULL becomes ""). The row is returned only if deleted = FALSE.
@@ -146,7 +188,7 @@ pub struct GetTableDataResponse {
>, >,
} }
/// Count non-deleted rows. /// Count non-deleted rows.
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct GetTableDataCountRequest { pub struct GetTableDataCountRequest {
/// Required. Profile (schema) name. /// Required. Profile (schema) name.
#[prost(string, tag = "1")] #[prost(string, tag = "1")]
@@ -156,7 +198,7 @@ pub struct GetTableDataCountRequest {
pub table_name: ::prost::alloc::string::String, pub table_name: ::prost::alloc::string::String,
} }
/// Fetch by ordinal position among non-deleted rows (1-based). /// 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 { pub struct GetTableDataByPositionRequest {
/// Required. Profile (schema) name. /// Required. Profile (schema) name.
#[prost(string, tag = "1")] #[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. /// Insert a new row into a table with strict type binding and script validation.
/// ///
/// Behavior: /// Behavior:
/// - Validates that profile (schema) exists and table is defined for it ///
/// - Validates provided columns exist (user-defined or allowed system/FK columns) /// * Validates that profile (schema) exists and table is defined for it
/// - For columns targeted by scripts in this table, the client MUST provide the /// * 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) /// value, and it MUST equal the scripts calculated value (compared type-safely)
/// - Binds values with correct SQL types, rejects invalid formats/ranges /// * Binds values with correct SQL types, rejects invalid formats/ranges
/// - Inserts the row and returns the new id; queues search indexing (best effort) /// * 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 /// * If the physical table is missing but the definition exists, returns INTERNAL
pub async fn post_table_data( pub async fn post_table_data(
&mut self, &mut self,
request: impl tonic::IntoRequest<super::PostTableDataRequest>, request: impl tonic::IntoRequest<super::PostTableDataRequest>,
@@ -289,7 +332,7 @@ pub mod tables_data_client {
format!("Service was not ready: {}", e.into()), 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( let path = http::uri::PathAndQuery::from_static(
"/komp_ac.tables_data.TablesData/PostTableData", "/komp_ac.tables_data.TablesData/PostTableData",
); );
@@ -300,17 +343,57 @@ pub mod tables_data_client {
); );
self.inner.unary(req, path, codec).await 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. /// Update existing row data with strict type binding and script validation.
/// ///
/// Behavior: /// Behavior:
/// - Validates profile and table, and that the record exists ///
/// - If request data is empty, returns success without changing the row /// * Validates profile and table, and that the record exists
/// - For columns targeted by scripts: /// * 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 included in update, provided value must equal the script result
/// • If not included, update must not cause the script result to differ /// • If not included, update must not cause the script result to differ
/// from the current stored value; otherwise FAILED_PRECONDITION is returned /// from the current stored value; otherwise FAILED_PRECONDITION is returned
/// - Binds values with correct SQL types; rejects invalid formats/ranges /// * Binds values with correct SQL types; rejects invalid formats/ranges
/// - Updates the row and returns the id; queues search indexing (best effort) /// * Updates the row and returns the id; queues search indexing (best effort)
pub async fn put_table_data( pub async fn put_table_data(
&mut self, &mut self,
request: impl tonic::IntoRequest<super::PutTableDataRequest>, request: impl tonic::IntoRequest<super::PutTableDataRequest>,
@@ -326,7 +409,7 @@ pub mod tables_data_client {
format!("Service was not ready: {}", e.into()), 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( let path = http::uri::PathAndQuery::from_static(
"/komp_ac.tables_data.TablesData/PutTableData", "/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. /// Soft-delete a single record (sets deleted = true) if it exists and is not already deleted.
/// ///
/// Behavior: /// Behavior:
/// - Validates profile and table definition ///
/// - Updates only rows with deleted = false /// * Validates profile and table definition
/// - success = true means a row was actually changed; false means nothing to delete /// * Updates only rows with deleted = false
/// - If the physical table is missing but the definition exists, returns INTERNAL /// * 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( pub async fn delete_table_data(
&mut self, &mut self,
request: impl tonic::IntoRequest<super::DeleteTableDataRequest>, request: impl tonic::IntoRequest<super::DeleteTableDataRequest>,
@@ -359,7 +443,7 @@ pub mod tables_data_client {
format!("Service was not ready: {}", e.into()), 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( let path = http::uri::PathAndQuery::from_static(
"/komp_ac.tables_data.TablesData/DeleteTableData", "/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. /// Fetch a single non-deleted row by id as textified values.
/// ///
/// Behavior: /// 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 /// including: id, deleted, all user-defined columns, and FK columns
/// named "<linked_table>_id" for each table link /// named "\<linked_table>\_id" for each table link
/// - Fails with NOT_FOUND if record does not exist or is soft-deleted /// * Fails with NOT_FOUND if record does not exist or is soft-deleted
/// - If the physical table is missing but the definition exists, returns INTERNAL /// * If the physical table is missing but the definition exists, returns INTERNAL
pub async fn get_table_data( pub async fn get_table_data(
&mut self, &mut self,
request: impl tonic::IntoRequest<super::GetTableDataRequest>, request: impl tonic::IntoRequest<super::GetTableDataRequest>,
@@ -394,7 +479,7 @@ pub mod tables_data_client {
format!("Service was not ready: {}", e.into()), 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( let path = http::uri::PathAndQuery::from_static(
"/komp_ac.tables_data.TablesData/GetTableData", "/komp_ac.tables_data.TablesData/GetTableData",
); );
@@ -408,9 +493,10 @@ pub mod tables_data_client {
/// Count non-deleted rows in a table. /// Count non-deleted rows in a table.
/// ///
/// Behavior: /// Behavior:
/// - Validates profile and table definition ///
/// - Returns komp_ac.common.CountResponse.count with rows where deleted = FALSE /// * Validates profile and table definition
/// - If the physical table is missing but the definition exists, returns INTERNAL /// * 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( pub async fn get_table_data_count(
&mut self, &mut self,
request: impl tonic::IntoRequest<super::GetTableDataCountRequest>, request: impl tonic::IntoRequest<super::GetTableDataCountRequest>,
@@ -426,7 +512,7 @@ pub mod tables_data_client {
format!("Service was not ready: {}", e.into()), 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( let path = http::uri::PathAndQuery::from_static(
"/komp_ac.tables_data.TablesData/GetTableDataCount", "/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. /// Fetch the N-th non-deleted row by id order (1-based), then return its full data.
/// ///
/// Behavior: /// Behavior:
/// - position is 1-based (position = 1 → first row by id ASC with deleted = FALSE) ///
/// - Returns NOT_FOUND if position is out of bounds /// * position is 1-based (position = 1 → first row by id ASC with deleted = FALSE)
/// - Otherwise identical to GetTableData for the selected id /// * 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( pub async fn get_table_data_by_position(
&mut self, &mut self,
request: impl tonic::IntoRequest<super::GetTableDataByPositionRequest>, request: impl tonic::IntoRequest<super::GetTableDataByPositionRequest>,
@@ -461,7 +548,7 @@ pub mod tables_data_client {
format!("Service was not ready: {}", e.into()), 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( let path = http::uri::PathAndQuery::from_static(
"/komp_ac.tables_data.TablesData/GetTableDataByPosition", "/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. /// Insert a new row into a table with strict type binding and script validation.
/// ///
/// Behavior: /// Behavior:
/// - Validates that profile (schema) exists and table is defined for it ///
/// - Validates provided columns exist (user-defined or allowed system/FK columns) /// * Validates that profile (schema) exists and table is defined for it
/// - For columns targeted by scripts in this table, the client MUST provide the /// * 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) /// value, and it MUST equal the scripts calculated value (compared type-safely)
/// - Binds values with correct SQL types, rejects invalid formats/ranges /// * Binds values with correct SQL types, rejects invalid formats/ranges
/// - Inserts the row and returns the new id; queues search indexing (best effort) /// * 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 /// * If the physical table is missing but the definition exists, returns INTERNAL
async fn post_table_data( async fn post_table_data(
&self, &self,
request: tonic::Request<super::PostTableDataRequest>, request: tonic::Request<super::PostTableDataRequest>,
@@ -507,17 +595,35 @@ pub mod tables_data_server {
tonic::Response<super::PostTableDataResponse>, tonic::Response<super::PostTableDataResponse>,
tonic::Status, 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. /// Update existing row data with strict type binding and script validation.
/// ///
/// Behavior: /// Behavior:
/// - Validates profile and table, and that the record exists ///
/// - If request data is empty, returns success without changing the row /// * Validates profile and table, and that the record exists
/// - For columns targeted by scripts: /// * 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 included in update, provided value must equal the script result
/// • If not included, update must not cause the script result to differ /// • If not included, update must not cause the script result to differ
/// from the current stored value; otherwise FAILED_PRECONDITION is returned /// from the current stored value; otherwise FAILED_PRECONDITION is returned
/// - Binds values with correct SQL types; rejects invalid formats/ranges /// * Binds values with correct SQL types; rejects invalid formats/ranges
/// - Updates the row and returns the id; queues search indexing (best effort) /// * Updates the row and returns the id; queues search indexing (best effort)
async fn put_table_data( async fn put_table_data(
&self, &self,
request: tonic::Request<super::PutTableDataRequest>, 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. /// Soft-delete a single record (sets deleted = true) if it exists and is not already deleted.
/// ///
/// Behavior: /// Behavior:
/// - Validates profile and table definition ///
/// - Updates only rows with deleted = false /// * Validates profile and table definition
/// - success = true means a row was actually changed; false means nothing to delete /// * Updates only rows with deleted = false
/// - If the physical table is missing but the definition exists, returns INTERNAL /// * 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( async fn delete_table_data(
&self, &self,
request: tonic::Request<super::DeleteTableDataRequest>, 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. /// Fetch a single non-deleted row by id as textified values.
/// ///
/// Behavior: /// 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 /// including: id, deleted, all user-defined columns, and FK columns
/// named "<linked_table>_id" for each table link /// named "\<linked_table>\_id" for each table link
/// - Fails with NOT_FOUND if record does not exist or is soft-deleted /// * Fails with NOT_FOUND if record does not exist or is soft-deleted
/// - If the physical table is missing but the definition exists, returns INTERNAL /// * If the physical table is missing but the definition exists, returns INTERNAL
async fn get_table_data( async fn get_table_data(
&self, &self,
request: tonic::Request<super::GetTableDataRequest>, request: tonic::Request<super::GetTableDataRequest>,
@@ -558,9 +666,10 @@ pub mod tables_data_server {
/// Count non-deleted rows in a table. /// Count non-deleted rows in a table.
/// ///
/// Behavior: /// Behavior:
/// - Validates profile and table definition ///
/// - Returns komp_ac.common.CountResponse.count with rows where deleted = FALSE /// * Validates profile and table definition
/// - If the physical table is missing but the definition exists, returns INTERNAL /// * 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( async fn get_table_data_count(
&self, &self,
request: tonic::Request<super::GetTableDataCountRequest>, 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. /// Fetch the N-th non-deleted row by id order (1-based), then return its full data.
/// ///
/// Behavior: /// Behavior:
/// - position is 1-based (position = 1 → first row by id ASC with deleted = FALSE) ///
/// - Returns NOT_FOUND if position is out of bounds /// * position is 1-based (position = 1 → first row by id ASC with deleted = FALSE)
/// - Otherwise identical to GetTableData for the selected id /// * Returns NOT_FOUND if position is out of bounds
/// * Otherwise identical to GetTableData for the selected id
async fn get_table_data_by_position( async fn get_table_data_by_position(
&self, &self,
request: tonic::Request<super::GetTableDataByPositionRequest>, request: tonic::Request<super::GetTableDataByPositionRequest>,
@@ -693,7 +803,53 @@ pub mod tables_data_server {
let inner = self.inner.clone(); let inner = self.inner.clone();
let fut = async move { let fut = async move {
let method = PostTableDataSvc(inner); 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) let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config( .apply_compression_config(
accept_compression_encodings, accept_compression_encodings,
@@ -738,7 +894,7 @@ pub mod tables_data_server {
let inner = self.inner.clone(); let inner = self.inner.clone();
let fut = async move { let fut = async move {
let method = PutTableDataSvc(inner); let method = PutTableDataSvc(inner);
let codec = tonic::codec::ProstCodec::default(); let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec) let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config( .apply_compression_config(
accept_compression_encodings, accept_compression_encodings,
@@ -783,7 +939,7 @@ pub mod tables_data_server {
let inner = self.inner.clone(); let inner = self.inner.clone();
let fut = async move { let fut = async move {
let method = DeleteTableDataSvc(inner); let method = DeleteTableDataSvc(inner);
let codec = tonic::codec::ProstCodec::default(); let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec) let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config( .apply_compression_config(
accept_compression_encodings, accept_compression_encodings,
@@ -828,7 +984,7 @@ pub mod tables_data_server {
let inner = self.inner.clone(); let inner = self.inner.clone();
let fut = async move { let fut = async move {
let method = GetTableDataSvc(inner); let method = GetTableDataSvc(inner);
let codec = tonic::codec::ProstCodec::default(); let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec) let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config( .apply_compression_config(
accept_compression_encodings, accept_compression_encodings,
@@ -874,7 +1030,7 @@ pub mod tables_data_server {
let inner = self.inner.clone(); let inner = self.inner.clone();
let fut = async move { let fut = async move {
let method = GetTableDataCountSvc(inner); let method = GetTableDataCountSvc(inner);
let codec = tonic::codec::ProstCodec::default(); let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec) let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config( .apply_compression_config(
accept_compression_encodings, accept_compression_encodings,
@@ -923,7 +1079,7 @@ pub mod tables_data_server {
let inner = self.inner.clone(); let inner = self.inner.clone();
let fut = async move { let fut = async move {
let method = GetTableDataByPositionSvc(inner); let method = GetTableDataByPositionSvc(inner);
let codec = tonic::codec::ProstCodec::default(); let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec) let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config( .apply_compression_config(
accept_compression_encodings, accept_compression_encodings,

View File

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

View File

@@ -1,75 +1,190 @@
// common/src/search.rs use std::path::{Path, PathBuf};
use tantivy::schema::*; use tantivy::schema::{
use tantivy::tokenizer::*; Field, IndexRecordOption, JsonObjectOptions, Schema, Term, TextFieldIndexing, TextOptions,
INDEXED, STORED, STRING,
};
use tantivy::tokenizer::{
AsciiFoldingFilter, LowerCaser, NgramTokenizer, RawTokenizer, RemoveLongFilter,
SimpleTokenizer, TextAnalyzer, TokenStream,
};
use tantivy::Index; use tantivy::Index;
/// Creates a hybrid Slovak search schema with optimized prefix fields. 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";
pub const TOK_WORD: &str = "kw_word";
pub const TOK_NGRAM: &str = "kw_ngram";
pub const TOK_EXACT: &str = "kw_exact";
/// Returns the on-disk path for a profile search index.
pub fn search_index_path(root: &Path, profile_name: &str) -> PathBuf {
root.join(profile_name)
}
/// Returns the unique index key for one table row inside a profile index.
pub fn search_row_key(table_name: &str, row_id: i64) -> String {
format!("{}:{}", table_name, row_id)
}
/// Normalizes user-entered values for exact-mode terms.
pub fn normalize_exact(input: &str) -> String {
let trimmed = input.trim();
if trimmed.is_empty() {
return String::new();
}
let mut analyzer = exact_analyzer();
let mut stream = analyzer.token_stream(trimmed);
let mut out = String::with_capacity(trimmed.len());
while let Some(token) = stream.next() {
out.push_str(&token.text);
}
out
}
/// Normalizes a column name to the JSON-key form used at index time.
pub fn normalize_column_name(column: &str) -> String {
column.to_ascii_lowercase()
}
/// Creates the column-aware search schema.
pub fn create_search_schema() -> Schema { pub fn create_search_schema() -> Schema {
let mut schema_builder = Schema::builder(); let mut schema_builder = Schema::builder();
schema_builder.add_u64_field("pg_id", INDEXED | STORED); 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));
// FIELD 1: For prefixes (1-4 chars). schema_builder.add_json_field(F_DATA_WORD, json_options(TOK_WORD, true, false));
let short_prefix_indexing = TextFieldIndexing::default() schema_builder.add_json_field(F_DATA_NGRAM, json_options(TOK_NGRAM, true, false));
.set_tokenizer("slovak_prefix_edge") schema_builder.add_json_field(F_DATA_EXACT, json_options(TOK_EXACT, false, false));
.set_index_option(IndexRecordOption::WithFreqsAndPositions);
let short_prefix_options = TextOptions::default()
.set_indexing_options(short_prefix_indexing)
.set_stored();
schema_builder.add_text_field("prefix_edge", short_prefix_options);
// FIELD 2: For the full word.
let full_word_indexing = TextFieldIndexing::default()
.set_tokenizer("slovak_prefix_full")
.set_index_option(IndexRecordOption::WithFreqsAndPositions);
let full_word_options = TextOptions::default()
.set_indexing_options(full_word_indexing)
.set_stored();
schema_builder.add_text_field("prefix_full", full_word_options);
// NGRAM FIELD: For substring matching.
let ngram_field_indexing = TextFieldIndexing::default()
.set_tokenizer("slovak_ngram")
.set_index_option(IndexRecordOption::WithFreqsAndPositions);
let ngram_options = TextOptions::default()
.set_indexing_options(ngram_field_indexing)
.set_stored();
schema_builder.add_text_field("text_ngram", ngram_options);
schema_builder.build() schema_builder.build()
} }
/// Registers all necessary Slovak tokenizers with the index. fn text_options(tokenizer_name: &str) -> TextOptions {
/// let indexing = TextFieldIndexing::default()
/// This must be called by ANY process that opens the index .set_tokenizer(tokenizer_name)
/// to ensure the tokenizers are loaded into memory. .set_index_option(IndexRecordOption::WithFreqsAndPositions);
pub fn register_slovak_tokenizers(index: &Index) -> tantivy::Result<()> {
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 {
IndexRecordOption::Basic
};
let indexing = TextFieldIndexing::default()
.set_tokenizer(tokenizer_name)
.set_index_option(index_option);
let mut options = JsonObjectOptions::default().set_indexing_options(indexing);
if stored {
options = options.set_stored();
}
options
}
/// Registers all required tokenizers with the index.
pub fn register_tokenizers(index: &Index) -> tantivy::Result<()> {
let tokenizer_manager = index.tokenizers(); let tokenizer_manager = index.tokenizers();
// TOKENIZER for `prefix_edge`: Edge N-gram (1-4 chars) tokenizer_manager.register(TOK_WORD, word_analyzer());
let edge_tokenizer = TextAnalyzer::builder(NgramTokenizer::new(1, 4, true)?) tokenizer_manager.register(TOK_NGRAM, ngram_analyzer()?);
.filter(RemoveLongFilter::limit(40)) tokenizer_manager.register(TOK_EXACT, exact_analyzer());
.filter(LowerCaser)
.filter(AsciiFoldingFilter)
.build();
tokenizer_manager.register("slovak_prefix_edge", edge_tokenizer);
// TOKENIZER for `prefix_full`: Simple word tokenizer
let full_tokenizer = TextAnalyzer::builder(SimpleTokenizer::default())
.filter(RemoveLongFilter::limit(40))
.filter(LowerCaser)
.filter(AsciiFoldingFilter)
.build();
tokenizer_manager.register("slovak_prefix_full", full_tokenizer);
// NGRAM TOKENIZER: For substring matching.
let ngram_tokenizer = TextAnalyzer::builder(NgramTokenizer::new(3, 3, false)?)
.filter(RemoveLongFilter::limit(40))
.filter(LowerCaser)
.filter(AsciiFoldingFilter)
.build();
tokenizer_manager.register("slovak_ngram", ngram_tokenizer);
Ok(()) Ok(())
} }
fn word_analyzer() -> TextAnalyzer {
TextAnalyzer::builder(SimpleTokenizer::default())
.filter(RemoveLongFilter::limit(80))
.filter(LowerCaser)
.filter(AsciiFoldingFilter)
.build()
}
fn ngram_analyzer() -> tantivy::Result<TextAnalyzer> {
Ok(TextAnalyzer::builder(NgramTokenizer::new(3, 3, false)?)
.filter(RemoveLongFilter::limit(80))
.filter(LowerCaser)
.filter(AsciiFoldingFilter)
.build())
}
fn exact_analyzer() -> TextAnalyzer {
TextAnalyzer::builder(RawTokenizer::default())
.filter(LowerCaser)
.filter(AsciiFoldingFilter)
.build()
}
/// Tokenizes text the same way `data_word` is indexed.
pub fn tokenize_word(text: &str) -> Vec<String> {
tokenize_with(word_analyzer(), text)
}
/// Tokenizes text the same way `data_ngram` is indexed.
pub fn tokenize_ngram(text: &str) -> Vec<String> {
match ngram_analyzer() {
Ok(analyzer) => tokenize_with(analyzer, text),
Err(_) => Vec::new(),
}
}
fn tokenize_with(mut analyzer: TextAnalyzer, text: &str) -> Vec<String> {
let mut stream = analyzer.token_stream(text);
let mut out = Vec::new();
while let Some(token) = stream.next() {
out.push(token.text.clone());
}
out
}
/// Builds a term scoped to a specific JSON path within a JSON field.
pub fn json_path_term(field: Field, column: &str, text: &str) -> Term {
let mut term = Term::from_field_json_path(field, column, false);
term.append_type_and_str(text);
term
}
/// Returns all required schema fields or fails loudly on mismatch.
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,
}
impl SchemaFields {
pub fn from(schema: &Schema) -> tantivy::Result<Self> {
Ok(Self {
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)?,
})
}
}
fn get_field(schema: &Schema, name: &str) -> tantivy::Result<Field> {
schema.get_field(name).map_err(|e| {
tantivy::TantivyError::SchemaError(format!("schema is missing field '{name}': {e}"))
})
}

6
flake.lock generated
View File

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

1
search/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.codex

View File

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

View File

@@ -1,279 +1,447 @@
// src/lib.rs mod query_builder;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::Path;
use tantivy::collector::TopDocs; use std::sync::{Arc, Mutex};
use tantivy::query::{
BooleanQuery, BoostQuery, FuzzyTermQuery, Occur, Query, QueryParser, TermQuery,
};
use tantivy::schema::{IndexRecordOption, Value};
use tantivy::{Index, TantivyDocument, Term};
use tonic::{Request, Response, Status};
use common::proto::komp_ac::search::searcher_server::Searcher; use common::proto::komp_ac::search::searcher_server::Searcher;
pub use common::proto::komp_ac::search::searcher_server::SearcherServer; pub use common::proto::komp_ac::search::searcher_server::SearcherServer;
use common::proto::komp_ac::search::{search_response::Hit, SearchRequest, SearchResponse}; use common::proto::komp_ac::search::{SearchRequest, SearchResponse, search_response::Hit};
use common::search::register_slovak_tokenizers; use common::search::{SchemaFields, register_tokenizers, search_index_path};
use sqlx::{PgPool, Row}; 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};
use tonic::{Request, Response, Status};
use tracing::info; use tracing::info;
// We need to hold the database pool in our service struct. const INDEX_ROOT: &str = "./tantivy_indexes";
const DEFAULT_RESULT_LIMIT: usize = 25;
const HARD_RESULT_LIMIT: usize = 200;
const DEFAULT_LIST_LIMIT: usize = 5;
pub struct SearcherService { pub struct SearcherService {
pub pool: PgPool, pub pool: PgPool,
profiles: Mutex<HashMap<String, Arc<ProfileIndex>>>,
} }
// normalize_slovak_text function remains unchanged... impl SearcherService {
fn normalize_slovak_text(text: &str) -> String { pub fn new(pool: PgPool) -> Self {
// ... function content is unchanged ... Self {
text.chars() pool,
.map(|c| match c { profiles: Mutex::new(HashMap::new()),
'á' | 'à' | 'â' | 'ä' | 'ă' | 'ā' => 'a', }
'Á' | 'À' | 'Â' | 'Ä' | 'Ă' | 'Ā' => 'A', }
'é' | 'è' | 'ê' | 'ë' | 'ě' | 'ē' => 'e',
'É' | 'È' | 'Ê' | 'Ë' | 'Ě' | 'Ē' => 'E',
'í' | 'ì' | 'î' | 'ï' | 'ī' => 'i',
'Í' | 'Ì' | 'Î' | 'Ï' | 'Ī' => 'I',
'ó' | 'ò' | 'ô' | 'ö' | 'ō' | 'ő' => 'o',
'Ó' | 'Ò' | 'Ô' | 'Ö' | 'Ō' | 'Ő' => 'O',
'ú' | 'ù' | 'û' | 'ü' | 'ū' | 'ű' => 'u',
'Ú' | 'Ù' | 'Û' | 'Ü' | 'Ū' | 'Ű' => 'U',
'ý' | 'ỳ' | 'ŷ' | 'ÿ' => 'y',
'Ý' | 'Ỳ' | 'Ŷ' | 'Ÿ' => 'Y',
'č' => 'c',
'Č' => 'C',
'ď' => 'd',
'Ď' => 'D',
'ľ' => 'l',
'Ľ' => 'L',
'ň' => 'n',
'Ň' => 'N',
'ř' => 'r',
'Ř' => 'R',
'š' => 's',
'Š' => 'S',
'ť' => 't',
'Ť' => 'T',
'ž' => 'z',
'Ž' => 'Z',
_ => c,
})
.collect()
}
#[tonic::async_trait] async fn run_rpc(
impl Searcher for SearcherService {
async fn search_table(
&self, &self,
request: Request<SearchRequest>, request: Request<SearchRequest>,
) -> Result<Response<SearchResponse>, Status> { ) -> Result<Response<SearchResponse>, Status> {
let req = request.into_inner(); let req = request.into_inner();
let table_name = req.table_name; let normalized = normalize_request(req)?;
let query_str = req.query;
if !profile_exists(&self.pool, &normalized.profile_name).await? {
return Err(Status::not_found(format!(
"Profile '{}' was not found",
normalized.profile_name
)));
}
if let Some(table_name) = normalized.table_name.as_deref() {
if !table_exists(&self.pool, &normalized.profile_name, table_name).await? {
return Err(Status::not_found(format!(
"Table '{}' was not found in profile '{}'",
table_name, normalized.profile_name
)));
}
}
if !normalized.has_input() {
let Some(table_name) = normalized.table_name.as_deref() else {
return Err(Status::invalid_argument(
"table_name is required when query is empty",
));
};
let hits = fetch_latest_rows(
&self.pool,
&normalized.profile_name,
table_name,
normalized.limit.unwrap_or(DEFAULT_LIST_LIMIT),
)
.await?;
return Ok(Response::new(SearchResponse { hits }));
}
let index_path = search_index_path(Path::new(INDEX_ROOT), &normalized.profile_name);
if !index_path.exists() {
return Err(Status::not_found(format!(
"No search index found for profile '{}'",
normalized.profile_name
)));
}
let profile = profile_index(&self.profiles, &normalized.profile_name, &index_path)?;
let mut hits = run_search(
&self.pool,
&profile,
&normalized.profile_name,
normalized.table_name.as_deref(),
&normalized.free_query,
&normalized.must,
normalized.limit.unwrap_or(DEFAULT_RESULT_LIMIT),
)
.await?;
hits.sort_by(|left, right| right.score.total_cmp(&left.score));
if let Some(limit) = normalized.limit {
if hits.len() > limit {
hits.truncate(limit);
}
}
// --- MODIFIED LOGIC ---
// If the query is empty, fetch the 5 most recent records.
if query_str.trim().is_empty() {
info!( info!(
"Empty query for table '{}'. Fetching default results.", "search: profile={} table={:?} free='{}' constraints={} hits={}",
table_name normalized.profile_name,
); normalized.table_name,
let qualified_table = format!("gen.\"{}\"", table_name); normalized.free_query,
let sql = format!( normalized.must.len(),
"SELECT id, to_jsonb(t) AS data FROM {} t ORDER BY id DESC LIMIT 5", hits.len()
qualified_table
); );
let rows = sqlx::query(&sql).fetch_all(&self.pool).await.map_err(|e| { Ok(Response::new(SearchResponse { hits }))
Status::internal(format!("DB query for default results failed: {}", e)) }
}
struct ProfileIndex {
index: Index,
reader: IndexReader,
fields: SchemaFields,
}
impl ProfileIndex {
fn open(path: &Path) -> Result<Self, Status> {
let index = Index::open_in_dir(path)
.map_err(|e| Status::internal(format!("Failed to open index: {}", e)))?;
register_tokenizers(&index)
.map_err(|e| Status::internal(format!("Failed to register tokenizers: {}", e)))?;
let reader = index
.reader_builder()
.reload_policy(ReloadPolicy::OnCommitWithDelay)
.try_into()
.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. Delete the stale index and create it again: {}",
e
))
})?; })?;
let hits: Vec<Hit> = rows Ok(Self {
index,
reader,
fields,
})
}
}
#[derive(Debug)]
struct NormalizedSearchRequest {
profile_name: String,
table_name: Option<String>,
free_query: String,
must: Vec<SearchConstraint>,
limit: Option<usize>,
}
impl NormalizedSearchRequest {
fn has_input(&self) -> bool {
!self.free_query.is_empty() || !self.must.is_empty()
}
}
fn profile_index(
cache: &Mutex<HashMap<String, Arc<ProfileIndex>>>,
profile_name: &str,
path: &Path,
) -> Result<Arc<ProfileIndex>, Status> {
{
let cache_guard = cache
.lock()
.map_err(|_| Status::internal("Profile index cache lock poisoned"))?;
if let Some(index) = cache_guard.get(profile_name) {
return Ok(index.clone());
}
}
let opened = Arc::new(ProfileIndex::open(path)?);
let mut cache_guard = cache
.lock()
.map_err(|_| Status::internal("Profile index cache lock poisoned"))?;
if let Some(index) = cache_guard.get(profile_name) {
return Ok(index.clone());
}
cache_guard.insert(profile_name.to_string(), opened.clone());
Ok(opened)
}
fn validate_identifier(value: &str, field_name: &str) -> Result<(), Status> {
let mut chars = value.chars();
let Some(first) = chars.next() else {
return Err(Status::invalid_argument(format!(
"{field_name} must not be empty"
)));
};
if !(first.is_ascii_alphabetic() || first == '_')
|| !chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
{
return Err(Status::invalid_argument(format!(
"{field_name} contains invalid characters"
)));
}
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)
}
async fn profile_exists(pool: &PgPool, profile_name: &str) -> Result<bool, Status> {
let exists =
sqlx::query_scalar::<_, bool>("SELECT EXISTS(SELECT 1 FROM schemas WHERE name = $1)")
.bind(profile_name)
.fetch_one(pool)
.await
.map_err(|e| Status::internal(format!("Profile lookup failed: {}", e)))?;
Ok(exists)
}
async fn table_exists(pool: &PgPool, profile_name: &str, table_name: &str) -> Result<bool, Status> {
let exists = sqlx::query_scalar::<_, bool>(
r#"
SELECT EXISTS(
SELECT 1
FROM table_definitions td
JOIN schemas s ON td.schema_id = s.id
WHERE s.name = $1 AND td.table_name = $2
)
"#,
)
.bind(profile_name)
.bind(table_name)
.fetch_one(pool)
.await
.map_err(|e| Status::internal(format!("Table lookup failed: {}", e)))?;
Ok(exists)
}
fn normalize_request(req: SearchRequest) -> Result<NormalizedSearchRequest, Status> {
let profile_name = req.profile_name.trim();
if profile_name.is_empty() {
return Err(Status::invalid_argument("profile_name is required"));
}
validate_identifier(profile_name, "profile_name")?;
let table_name = match req.table_name.as_deref().map(str::trim) {
Some(table_name) if !table_name.is_empty() => {
validate_identifier(table_name, "table_name")?;
Some(table_name.to_string())
}
_ => None,
};
let free_query = req.free_query.trim().to_string();
let mut must = Vec::new();
for constraint in req.must {
let column = constraint.column.trim();
validate_search_column(column)?;
let query = constraint.query.trim();
if query.is_empty() {
return Err(Status::invalid_argument(
"constraint.query must not be empty",
));
}
must.push(SearchConstraint {
column: column.to_string(),
query: query.to_string(),
mode: constraint_mode_from_proto(constraint.mode),
});
}
let limit = req
.limit
.map(|value| (value as usize).min(HARD_RESULT_LIMIT));
Ok(NormalizedSearchRequest {
profile_name: profile_name.to_string(),
table_name,
free_query,
must,
limit,
})
}
fn constraint_mode_from_proto(raw_mode: i32) -> ConstraintMode {
match raw_mode {
2 => ConstraintMode::Exact,
_ => ConstraintMode::Fuzzy,
}
}
async fn fetch_latest_rows(
pool: &PgPool,
profile_name: &str,
table_name: &str,
limit: usize,
) -> Result<Vec<Hit>, Status> {
let sql = format!(
"SELECT id, to_jsonb(t) AS data FROM {} t WHERE deleted = FALSE ORDER BY id DESC LIMIT $1",
qualify_profile_table(profile_name, table_name)
);
let rows = sqlx::query(AssertSqlSafe(sql))
.bind(limit as i64)
.fetch_all(pool)
.await
.map_err(|e| Status::internal(format!("DB query for default results failed: {}", e)))?;
Ok(rows
.into_iter() .into_iter()
.map(|row| { .map(|row| {
let id: i64 = row.try_get("id").unwrap_or_default(); let id: i64 = row.try_get("id").unwrap_or_default();
let json_data: serde_json::Value = row.try_get("data").unwrap_or_default(); let json_data: serde_json::Value = row.try_get("data").unwrap_or_default();
Hit { Hit {
id, id,
// Score is 0.0 as this is not a relevance-ranked search
score: 0.0, score: 0.0,
content_json: json_data.to_string(), content_json: json_data.to_string(),
table_name: table_name.to_string(),
} }
}) })
.collect(); .collect())
}
info!( async fn run_search(
"--- SERVER: Successfully processed empty query. Returning {} default hits. ---", pool: &PgPool,
hits.len() profile: &ProfileIndex,
); profile_name: &str,
return Ok(Response::new(SearchResponse { hits })); table_filter: Option<&str>,
} free_query: &str,
// --- END OF MODIFIED LOGIC --- must: &[SearchConstraint],
limit: usize,
let index_path = Path::new("./tantivy_indexes").join(&table_name); ) -> Result<Vec<Hit>, Status> {
if !index_path.exists() { let master_query = build_master_query(
return Err(Status::not_found(format!( &profile.index,
"No search index found for table '{}'", &profile.fields,
table_name free_query,
))); must,
} table_filter,
)?;
let index = Index::open_in_dir(&index_path)
.map_err(|e| Status::internal(format!("Failed to open index: {}", e)))?;
register_slovak_tokenizers(&index).map_err(|e| {
Status::internal(format!("Failed to register Slovak tokenizers: {}", e))
})?;
let reader = index
.reader()
.map_err(|e| Status::internal(format!("Failed to create index reader: {}", e)))?;
let searcher = reader.searcher();
let schema = index.schema();
let pg_id_field = schema
.get_field("pg_id")
.map_err(|_| Status::internal("Schema is missing the 'pg_id' field."))?;
// --- Query Building Logic (no changes here) ---
let prefix_edge_field = schema.get_field("prefix_edge").unwrap();
let prefix_full_field = schema.get_field("prefix_full").unwrap();
let text_ngram_field = schema.get_field("text_ngram").unwrap();
let normalized_query = normalize_slovak_text(&query_str);
let words: Vec<&str> = normalized_query.split_whitespace().collect();
if words.is_empty() {
return Ok(Response::new(SearchResponse { hits: vec![] }));
}
let mut query_layers: Vec<(Occur, Box<dyn Query>)> = Vec::new();
// ... all your query building layers remain exactly the same ...
// ===============================
// LAYER 1: PREFIX MATCHING (HIGHEST PRIORITY, Boost: 4.0)
// ===============================
{
let mut must_clauses: Vec<(Occur, Box<dyn Query>)> = Vec::new();
for word in &words {
let edge_term = Term::from_field_text(prefix_edge_field, word);
let full_term = Term::from_field_text(prefix_full_field, word);
let per_word_query = BooleanQuery::new(vec![
(
Occur::Should,
Box::new(TermQuery::new(edge_term, IndexRecordOption::Basic)),
),
(
Occur::Should,
Box::new(TermQuery::new(full_term, IndexRecordOption::Basic)),
),
]);
must_clauses.push((Occur::Must, Box::new(per_word_query) as Box<dyn Query>));
}
if !must_clauses.is_empty() {
let prefix_query = BooleanQuery::new(must_clauses);
let boosted_query = BoostQuery::new(Box::new(prefix_query), 4.0);
query_layers.push((Occur::Should, Box::new(boosted_query)));
}
}
// ===============================
// LAYER 2: FUZZY MATCHING (HIGH PRIORITY, Boost: 3.0)
// ===============================
{
let last_word = words.last().unwrap();
let fuzzy_term = Term::from_field_text(prefix_full_field, last_word);
let fuzzy_query = FuzzyTermQuery::new(fuzzy_term, 2, true);
let boosted_query = BoostQuery::new(Box::new(fuzzy_query), 3.0);
query_layers.push((Occur::Should, Box::new(boosted_query)));
}
// ===============================
// LAYER 3: PHRASE MATCHING WITH SLOP (MEDIUM PRIORITY, Boost: 2.0)
// ===============================
if words.len() > 1 {
let slop_parser = QueryParser::for_index(&index, vec![prefix_full_field]);
let slop_query_str = format!("\"{}\"~3", normalized_query);
if let Ok(slop_query) = slop_parser.parse_query(&slop_query_str) {
let boosted_query = BoostQuery::new(slop_query, 2.0);
query_layers.push((Occur::Should, Box::new(boosted_query)));
}
}
// ===============================
// LAYER 4: NGRAM SUBSTRING MATCHING (LOWEST PRIORITY, Boost: 1.0)
// ===============================
{
let ngram_parser = QueryParser::for_index(&index, vec![text_ngram_field]);
if let Ok(ngram_query) = ngram_parser.parse_query(&normalized_query) {
let boosted_query = BoostQuery::new(ngram_query, 1.0);
query_layers.push((Occur::Should, Box::new(boosted_query)));
}
}
let master_query = BooleanQuery::new(query_layers);
// --- End of Query Building Logic ---
let searcher = profile.reader.searcher();
let top_docs = searcher let top_docs = searcher
.search(&master_query, &TopDocs::with_limit(100)) .search(&*master_query, &TopDocs::with_limit(limit).order_by_score())
.map_err(|e| Status::internal(format!("Search failed: {}", e)))?; .map_err(|e| Status::internal(format!("Search failed: {}", e)))?;
if top_docs.is_empty() { if top_docs.is_empty() {
return Ok(Response::new(SearchResponse { hits: vec![] })); return Ok(vec![]);
} }
// --- NEW LOGIC: Fetch from DB and combine results --- let mut candidates: Vec<(f32, i64, String)> = Vec::with_capacity(top_docs.len());
// Step 1: Extract (score, pg_id) from Tantivy results.
let mut scored_ids: Vec<(f32, u64)> = Vec::new();
for (score, doc_address) in top_docs { for (score, doc_address) in top_docs {
let doc: TantivyDocument = searcher let doc: TantivyDocument = searcher
.doc(doc_address) .doc(doc_address)
.map_err(|e| Status::internal(format!("Failed to retrieve document: {}", e)))?; .map_err(|e| Status::internal(format!("Failed to retrieve document: {}", e)))?;
if let Some(pg_id_value) = doc.get_first(pg_id_field) { let Some(pg_id) = doc
if let Some(pg_id) = pg_id_value.as_u64() { .get_first(profile.fields.pg_id)
scored_ids.push((score, pg_id)); .and_then(|value| value.as_u64())
} else {
} continue;
};
let Some(table_name) = doc
.get_first(profile.fields.table_name)
.and_then(|value| value.as_str())
else {
continue;
};
candidates.push((score, pg_id as i64, table_name.to_string()));
} }
// Step 2: Fetch all corresponding rows from Postgres in a single query. if candidates.is_empty() {
let pg_ids: Vec<i64> = scored_ids.iter().map(|(_, id)| *id as i64).collect(); return Ok(vec![]);
let qualified_table = format!("gen.\"{}\"", table_name); }
let query_str = format!(
"SELECT id, to_jsonb(t) AS data FROM {} t WHERE id = ANY($1)", let mut ids_by_table: HashMap<String, Vec<i64>> = HashMap::new();
qualified_table for (_, pg_id, table_name) in &candidates {
ids_by_table
.entry(table_name.clone())
.or_default()
.push(*pg_id);
}
let mut content_map: HashMap<(String, i64), String> = HashMap::new();
for (table_name, pg_ids) in ids_by_table {
validate_identifier(&table_name, "table_name")?;
let sql = format!(
"SELECT id, to_jsonb(t) AS data FROM {} t WHERE deleted = FALSE AND id = ANY($1)",
qualify_profile_table(profile_name, &table_name)
); );
let rows = sqlx::query(&query_str) let rows = sqlx::query(AssertSqlSafe(sql))
.bind(&pg_ids) .bind(&pg_ids)
.fetch_all(&self.pool) .fetch_all(pool)
.await .await
.map_err(|e| Status::internal(format!("Database query failed: {}", e)))?; .map_err(|e| Status::internal(format!("Database query failed: {}", e)))?;
// Step 3: Map the database results by ID for quick lookup.
let mut content_map: HashMap<i64, String> = HashMap::new();
for row in rows { for row in rows {
let id: i64 = row.try_get("id").unwrap_or(0); let id: i64 = row.try_get("id").unwrap_or_default();
let json_data: serde_json::Value = let json_data: serde_json::Value = row.try_get("data").unwrap_or_default();
row.try_get("data").unwrap_or(serde_json::Value::Null); content_map.insert((table_name.clone(), id), json_data.to_string());
content_map.insert(id, json_data.to_string()); }
} }
// Step 4: Build the final response, combining Tantivy scores with PG content. Ok(candidates
let hits: Vec<Hit> = scored_ids
.into_iter() .into_iter()
.filter_map(|(score, pg_id)| { .filter_map(|(score, pg_id, table_name)| {
content_map.get(&(pg_id as i64)).map(|content_json| Hit { content_map
id: pg_id as i64, .get(&(table_name.clone(), pg_id))
.map(|content_json| Hit {
id: pg_id,
score, score,
content_json: content_json.clone(), content_json: content_json.clone(),
table_name,
}) })
}) })
.collect(); .collect())
}
info!( #[tonic::async_trait]
"--- SERVER: Successfully processed search. Returning {} hits. ---", impl Searcher for SearcherService {
hits.len() async fn search(
); &self,
request: Request<SearchRequest>,
let response = SearchResponse { hits }; ) -> Result<Response<SearchResponse>, Status> {
Ok(Response::new(response)) self.run_rpc(request).await
} }
} }

251
search/src/query_builder.rs Normal file
View File

@@ -0,0 +1,251 @@
use common::search::{
json_path_term, normalize_column_name, normalize_exact, tokenize_ngram, tokenize_word,
SchemaFields,
};
use tantivy::query::{
BooleanQuery, BoostQuery, EmptyQuery, FuzzyTermQuery, Occur, PhraseQuery, Query, QueryParser,
TermQuery,
};
use tantivy::schema::{IndexRecordOption, Term};
use tantivy::Index;
use tonic::Status;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ConstraintMode {
Fuzzy,
Exact,
}
#[derive(Clone, Debug)]
pub struct SearchConstraint {
pub column: String,
pub query: String,
pub mode: ConstraintMode,
}
pub fn build_master_query(
index: &Index,
fields: &SchemaFields,
free_query: &str,
must: &[SearchConstraint],
table_filter: Option<&str>,
) -> Result<Box<dyn Query>, Status> {
let mut clauses: Vec<(Occur, Box<dyn Query>)> = Vec::new();
let mut has_search_clause = false;
for constraint in must {
let predicate = match constraint.mode {
ConstraintMode::Exact => {
exact_predicate(fields, &constraint.column, &constraint.query)?
}
ConstraintMode::Fuzzy => {
fuzzy_predicate_scoped(fields, &constraint.column, &constraint.query)?
}
};
clauses.push((Occur::Must, predicate));
has_search_clause = true;
}
let free_words = tokenize_word(free_query);
if !free_words.is_empty() {
let predicate = fuzzy_predicate_unscoped(index, fields, &free_words)?;
clauses.push((Occur::Must, predicate));
has_search_clause = true;
}
if let Some(table_name) = table_filter {
let term = Term::from_field_text(fields.table_name, table_name);
clauses.push((
Occur::Must,
Box::new(TermQuery::new(term, IndexRecordOption::Basic)),
));
}
if !has_search_clause {
return Ok(Box::new(EmptyQuery));
}
Ok(Box::new(BooleanQuery::new(clauses)))
}
fn exact_predicate(
fields: &SchemaFields,
column: &str,
query: &str,
) -> Result<Box<dyn Query>, Status> {
let normalized_value = normalize_exact(query);
if normalized_value.is_empty() {
return Err(Status::invalid_argument(
"exact query is empty after normalization",
));
}
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)))
}
fn fuzzy_predicate_scoped(
fields: &SchemaFields,
column: &str,
query: &str,
) -> Result<Box<dyn Query>, Status> {
let words = tokenize_word(query);
if words.is_empty() {
return Err(Status::invalid_argument(
"fuzzy query has no searchable tokens",
));
}
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 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.clone(), distance, true)),
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 phrase_terms: Vec<(usize, Term)> = words
.iter()
.enumerate()
.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((
Occur::Should,
Box::new(BoostQuery::new(Box::new(phrase), 2.0)),
));
}
let ngrams = tokenize_ngram(query);
if !ngrams.is_empty() {
let ngram_clauses: Vec<(Occur, Box<dyn Query>)> = ngrams
.into_iter()
.map(|gram| {
let term = json_path_term(fields.data_ngram, &column, &gram);
(
Occur::Must,
Box::new(TermQuery::new(term, IndexRecordOption::Basic)) as Box<dyn Query>,
)
})
.collect();
layers.push((
Occur::Should,
Box::new(BoostQuery::new(
Box::new(BooleanQuery::new(ngram_clauses)),
1.0,
)),
));
}
Ok(Box::new(BooleanQuery::new(layers)))
}
fn fuzzy_predicate_unscoped(
index: &Index,
fields: &SchemaFields,
words: &[String],
) -> Result<Box<dyn Query>, Status> {
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 = 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,
)),
));
}
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.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))));
}
}
{
let parser = QueryParser::for_index(index, vec![fields.all_text]);
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, 1.0))));
}
}
if layers.is_empty() {
return Ok(Box::new(EmptyQuery));
}
Ok(Box::new(BooleanQuery::new(layers)))
}
fn fuzzy_distance(word_len: usize) -> Option<u8> {
match word_len {
0..=3 => None,
4..=6 => Some(1),
_ => Some(2),
}
}

2
server

Submodule server updated: 6b0c3e63b4...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.