diff --git a/proto/camino/api.proto b/proto/camino/api.proto index a673e18..d43315d 100644 --- a/proto/camino/api.proto +++ b/proto/camino/api.proto @@ -242,6 +242,8 @@ message QueryPendingField { optional uint32 residual_window = 7; uint32 residual_row = 8; string residual_field = 9; + optional uint32 relational_capture = 10; + uint32 captured_object = 11; } message QueryStats { uint32 sql_count = 1; @@ -251,6 +253,9 @@ message QueryStats { uint32 result_bytes = 5; double preparation_ms = 6; double total_ms = 7; + uint32 relational_stages = 8; + uint32 captured_candidates = 9; + double relational_ms = 10; } message QueryResponse { Value value = 1; @@ -264,6 +269,35 @@ message QueryResponse { repeated QueryResidualWindow residual_windows = 8; // Native reads share one database snapshot; package enrichment does not. string consistency = 9; // native-snapshot | mixed + repeated QueryRelationalCapture relational_captures = 10; +} + +// Private coordinator input, removed before publishing a result. Memberships +// and native facts share one snapshot; package reads are sampled afterwards. +message QueryCapturedMember { + string object_id = 1; + string entry_id = 2; + Value map_key = 3; +} +message QueryCapturedMembers { + repeated QueryCapturedMember entries = 1; +} +message QueryCapturedObject { + string object_id = 1; + map fields = 2; + map field_types = 3; + map relationships = 4; +} +message QueryRelationalCapture { + repeated QueryPathPart path = 1; + QueryRelationalPlan plan = 2; + string root_object_id = 3; + repeated QueryCapturedObject objects = 4; + string variables_json = 5; + uint32 row_limit = 6; + uint32 candidate_limit = 7; + optional uint32 residual_window = 8; + repeated string result_path = 9; } message QueryResidualRow { @@ -283,6 +317,8 @@ message QueryResidualWindow { bool bounded_all = 6; repeated QuerySelection selection = 7; map field_types = 8; + bool relational = 9; + repeated string matched_entries = 10; } message QueryFieldFailure { diff --git a/proto/camino/schema.proto b/proto/camino/schema.proto index 9565d82..3997ef5 100644 --- a/proto/camino/schema.proto +++ b/proto/camino/schema.proto @@ -102,6 +102,171 @@ message QuerySelection { repeated QueryCondition conditions = 6; map arguments = 7; repeated QuerySelection selection = 8; + QueryRelationalPlan relational = 9; + QueryPredicate predicate = 10; +} +// Resolved IDs, not GraphQL names, determine execution. Response selections +// remain separate so aliases and fragments cannot change relational semantics. +message QueryPathStep { + oneof step { + bool source = 1; + bool target = 2; + QueryRelationPath relation = 3; + } +} +message QueryRelationPath { + string interface_revision_id = 1; + string member_id = 2; + string target_interface_revision_id = 3; + bool optional = 4; +} +message QueryFieldOperand { + string interface_revision_id = 1; + string member_id = 2; +} +message QueryExpression { + repeated QueryPathStep path = 1; + oneof leaf { + QueryFieldOperand field = 2; + string ref = 3; + bool entry = 4; + bool map_key = 5; + } + string value_type_json = 6; +} +enum QueryAggregateOperator { + QUERY_AGGREGATE_OPERATOR_UNSPECIFIED = 0; + QUERY_AGGREGATE_OPERATOR_COUNT = 1; + QUERY_AGGREGATE_OPERATOR_COUNT_PRESENT = 2; + QUERY_AGGREGATE_OPERATOR_SUM = 3; + QUERY_AGGREGATE_OPERATOR_AVG = 4; + QUERY_AGGREGATE_OPERATOR_MIN = 5; + QUERY_AGGREGATE_OPERATOR_MAX = 6; +} +message QueryReduction { + QueryAggregateOperator operator = 1; + QueryExpression operand = 2; + string value_type_json = 3; +} +message QueryOperand { + oneof operand { + QueryExpression expression = 1; + QueryReduction reduction = 2; + } +} +enum QueryComparisonOperator { + QUERY_COMPARISON_OPERATOR_UNSPECIFIED = 0; + QUERY_COMPARISON_OPERATOR_EQ = 1; + QUERY_COMPARISON_OPERATOR_IN = 2; + QUERY_COMPARISON_OPERATOR_IS_NULL = 3; + QUERY_COMPARISON_OPERATOR_LT = 4; + QUERY_COMPARISON_OPERATOR_LTE = 5; + QUERY_COMPARISON_OPERATOR_GT = 6; + QUERY_COMPARISON_OPERATOR_GTE = 7; +} +message QueryComparison { + QueryOperand operand = 1; + QueryComparisonOperator operator = 2; + QueryArgument value = 3; +} +message QueryPredicateList { + repeated QueryPredicate children = 1; +} +enum QueryRelationPredicateOperator { + QUERY_RELATION_PREDICATE_OPERATOR_UNSPECIFIED = 0; + QUERY_RELATION_PREDICATE_OPERATOR_SOME = 1; + QUERY_RELATION_PREDICATE_OPERATOR_NONE = 2; + QUERY_RELATION_PREDICATE_OPERATOR_IS = 3; + QUERY_RELATION_PREDICATE_OPERATOR_IS_NULL = 4; +} +message QueryRelationPredicate { + repeated QueryPathStep path = 1; + QueryRelationPath relation = 2; + QueryRelationPredicateOperator operator = 3; + QueryPredicate predicate = 4; + QueryArgument value = 5; +} +message QueryReductionPredicate { + repeated QueryPathStep path = 1; + QueryRelationPath relation = 2; + QueryPredicate where = 3; + QueryPredicate having = 4; +} +message QueryPredicate { + oneof predicate { + QueryPredicateList and = 1; + QueryPredicateList or = 2; + QueryPredicate not = 3; + QueryComparison compare = 4; + QueryRelationPredicate relation = 5; + QueryReductionPredicate reduce = 6; + } +} +message QueryRow { + oneof row { + QueryObjectRow object = 1; + QueryPairRow pair = 2; + } +} +message QueryObjectRow { + string interface_revision_id = 1; + bool membership = 2; + string key_type = 3; +} +message QueryPairRow { + QueryRow source = 1; + QueryRow target = 2; +} +message QueryKey { + repeated string path = 1; + QueryExpression expression = 2; +} +message QueryDistinct { + repeated QueryKey keys = 1; +} +message QueryExpansion { + repeated QueryPathStep path = 1; + QueryRelationPath relation = 2; +} +message QueryRelationalStage { + uint32 id = 1; + optional uint32 input = 2; + QueryRow row = 3; + oneof operation { + QueryRelationPath source = 4; + QueryPredicate filter = 5; + QueryExpansion expand = 6; + QueryDistinct distinct = 7; + } +} +message QueryReductionProjection { + repeated string path = 1; + QueryReduction reduction = 2; +} +message QueryAggregateOrder { + QueryOperand operand = 1; + bool descending = 2; +} +message QueryRelationalTerminal { + uint32 stage = 1; + repeated string path = 2; + bool groups = 3; + repeated QueryKey keys = 4; + repeated QueryReductionProjection reductions = 5; + QueryPredicate where = 6; + QueryPredicate having = 7; + repeated QueryAggregateOrder order = 8; + QueryArgument first = 9; + QueryArgument all = 10; + QueryArgument after = 11; + repeated QuerySelection selection = 12; + bool residual = 13; + // Internal ordinary-relationship residual: yields ordered membership IDs. + bool rows = 14; +} +message QueryRelationalPlan { + repeated QueryRelationalStage stages = 1; + repeated QueryRelationalTerminal terminals = 2; } message QueryReadBinding { string atom_id = 1; diff --git a/src/capability-language/parser.ts b/src/capability-language/parser.ts index 98f7d25..0a75ba8 100644 --- a/src/capability-language/parser.ts +++ b/src/capability-language/parser.ts @@ -1307,12 +1307,12 @@ const lowerPackage = ( if (parameters.length) { const use = identifier(clause.identifier(1)); const reason = stringValue(clause.stringLiteral()!); - if (!["select", "predicate", "order"].includes(use) || !reason.trim()) + if (!["select", "predicate", "order", "aggregate", "group", "distinct"].includes(use) || !reason.trim()) loweringIssue( state, clause, "invalid-query-allowance", - "Expected select/predicate/order and a nonempty reason", + "Expected select/predicate/order/aggregate/group/distinct and a nonempty reason", ); templateAllowances.push({ interface: state.types.interface(clause.interfaceType()!), @@ -1329,12 +1329,16 @@ const lowerPackage = ( const member = contract?.members.find((entry) => entry.displayName === identifier(clause.identifier(0))); const use = identifier(clause.identifier(1)); const reason = stringValue(clause.stringLiteral()!); - if (!member || !["select", "predicate", "order"].includes(use) || !reason.trim()) + if ( + !member || + !["select", "predicate", "order", "aggregate", "group", "distinct"].includes(use) || + !reason.trim() + ) loweringIssue( state, clause, "invalid-query-allowance", - "Query allowances name an exact member, use (select/predicate/order), and nonempty reason", + "Query allowances name an exact member, use (select/predicate/order/aggregate/group/distinct), and nonempty reason", ); else declaration.allowances.push({ diff --git a/src/gen/camino/api_pb.ts b/src/gen/camino/api_pb.ts index 7511178..90cb22a 100644 --- a/src/gen/camino/api_pb.ts +++ b/src/gen/camino/api_pb.ts @@ -4,7 +4,7 @@ import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; -import type { PersistencePlan, QuerySelection } from "./schema_pb.js"; +import type { PersistencePlan, QueryRelationalPlan, QuerySelection } from "./schema_pb.js"; import { file_camino_schema } from "./schema_pb.js"; import type { Message } from "@bufbuild/protobuf"; @@ -12,7 +12,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file camino/api.proto. */ export const file_camino_api: GenFile = /*@__PURE__*/ - fileDesc("ChBjYW1pbm8vYXBpLnByb3RvEgZjYW1pbm8iCwoJTnVsbFZhbHVlInwKC09iamVjdFZhbHVlEi8KBmZpZWxkcxgBIAMoCzIfLmNhbWluby5PYmplY3RWYWx1ZS5GaWVsZHNFbnRyeRo8CgtGaWVsZHNFbnRyeRILCgNrZXkYASABKAkSHAoFdmFsdWUYAiABKAsyDS5jYW1pbm8uVmFsdWU6AjgBIioKCUxpc3RWYWx1ZRIdCgZ2YWx1ZXMYASADKAsyDS5jYW1pbm8uVmFsdWUiHQoIUmVmVmFsdWUSEQoJb2JqZWN0X2lkGAEgASgJIjwKCUNyZHRWYWx1ZRIMCgR0eXBlGAEgASgJEhAKCGVuY29kaW5nGAIgASgJEg8KB3BheWxvYWQYAyABKAwiqAEKEFN0YXRlVmFsdWVTb3VyY2USEQoJb2JqZWN0X2lkGAEgASgJEg8KB3Nsb3RfaWQYAiABKAkSFwoPdmFsdWVfdHlwZV9qc29uGAMgASgJEhsKE3N0b3JhZ2VfcG9saWN5X2pzb24YBCABKAkSEAoIcmV2aXNpb24YBSABKAQSKAoNY3JkdF9zbmFwc2hvdBgGIAEoCzIRLmNhbWluby5DcmR0VmFsdWUiNgoLVmFsdWVTb3VyY2USJwoFc3RhdGUYASABKAsyGC5jYW1pbm8uU3RhdGVWYWx1ZVNvdXJjZSL5AgoFVmFsdWUSJwoKbnVsbF92YWx1ZRgBIAEoCzIRLmNhbWluby5OdWxsVmFsdWVIABIUCgpib29sX3ZhbHVlGAIgASgISAASFgoMbnVtYmVyX3ZhbHVlGAMgASgBSAASFgoMc3RyaW5nX3ZhbHVlGAQgASgJSAASFwoNaW50ZWdlcl92YWx1ZRgFIAEoCUgAEhUKC2J5dGVzX3ZhbHVlGAYgASgMSAASKwoMb2JqZWN0X3ZhbHVlGAcgASgLMhMuY2FtaW5vLk9iamVjdFZhbHVlSAASJwoKbGlzdF92YWx1ZRgIIAEoCzIRLmNhbWluby5MaXN0VmFsdWVIABIlCglyZWZfdmFsdWUYCSABKAsyEC5jYW1pbm8uUmVmVmFsdWVIABInCgpjcmR0X3ZhbHVlGAogASgLMhEuY2FtaW5vLkNyZHRWYWx1ZUgAEiMKBnNvdXJjZRgLIAEoCzITLmNhbWluby5WYWx1ZVNvdXJjZUIGCgRraW5kIkYKHUluc3RhbGxQZXJzaXN0ZW5jZVBsYW5SZXF1ZXN0EiUKBHBsYW4YASABKAsyFy5jYW1pbm8uUGVyc2lzdGVuY2VQbGFuIkcKHkluc3RhbGxQZXJzaXN0ZW5jZVBsYW5SZXNwb25zZRIlCgRwbGFuGAEgASgLMhcuY2FtaW5vLlBlcnNpc3RlbmNlUGxhbiIbChlHZXRQZXJzaXN0ZW5jZVBsYW5SZXF1ZXN0IkMKGkdldFBlcnNpc3RlbmNlUGxhblJlc3BvbnNlEiUKBHBsYW4YASABKAsyFy5jYW1pbm8uUGVyc2lzdGVuY2VQbGFuIrABChNDcmVhdGVPYmplY3RSZXF1ZXN0Eg8KB2F0b21faWQYASABKAkSRAoNaW5pdGlhbF9zdGF0ZRgCIAMoCzItLmNhbWluby5DcmVhdGVPYmplY3RSZXF1ZXN0LkluaXRpYWxTdGF0ZUVudHJ5GkIKEUluaXRpYWxTdGF0ZUVudHJ5EgsKA2tleRgBIAEoCRIcCgV2YWx1ZRgCIAEoCzINLmNhbWluby5WYWx1ZToCOAEiPAoUQ3JlYXRlT2JqZWN0UmVzcG9uc2USJAoGb2JqZWN0GAEgASgLMhQuY2FtaW5vLkNhbWlub09iamVjdCIlChBHZXRPYmplY3RSZXF1ZXN0EhEKCW9iamVjdF9pZBgBIAEoCSI5ChFHZXRPYmplY3RSZXNwb25zZRIkCgZvYmplY3QYASABKAsyFC5jYW1pbm8uQ2FtaW5vT2JqZWN0IiUKEkxpc3RPYmplY3RzUmVxdWVzdBIPCgdhdG9tX2lkGAEgASgJIjwKE0xpc3RPYmplY3RzUmVzcG9uc2USJQoHb2JqZWN0cxgBIAMoCzIULmNhbWluby5DYW1pbm9PYmplY3QiNgoQUmVhZFN0YXRlUmVxdWVzdBIRCglvYmplY3RfaWQYASABKAkSDwoHc2xvdF9pZBgCIAEoCSIxChFSZWFkU3RhdGVSZXNwb25zZRIcCgV2YWx1ZRgBIAEoCzINLmNhbWluby5WYWx1ZSJxChFXcml0ZVN0YXRlUmVxdWVzdBIRCglvYmplY3RfaWQYASABKAkSDwoHc2xvdF9pZBgCIAEoCRIcCgV2YWx1ZRgDIAEoCzINLmNhbWluby5WYWx1ZRIaChJjbGllbnRfbXV0YXRpb25faWQYBCABKAkiWAoSV3JpdGVTdGF0ZVJlc3BvbnNlEhwKBXZhbHVlGAEgASgLMg0uY2FtaW5vLlZhbHVlEiQKBm9iamVjdBgCIAEoCzIULmNhbWluby5DYW1pbm9PYmplY3QiwAEKEkNvbm5lY3RFZGdlUmVxdWVzdBIUCgxlZGdlX3R5cGVfaWQYASABKAkSFQoNcHJvamVjdGlvbl9pZBgCIAEoCRIRCglvYmplY3RfaWQYAyABKAkSGAoQdGFyZ2V0X29iamVjdF9pZBgEIAEoCRIUCgdvcmRpbmFsGAUgASgFSACIAQESGwoOdGFyZ2V0X29yZGluYWwYBiABKAVIAYgBAUIKCghfb3JkaW5hbEIRCg9fdGFyZ2V0X29yZGluYWwiNwoTQ29ubmVjdEVkZ2VSZXNwb25zZRIgCgRlZGdlGAEgASgLMhIuY2FtaW5vLkNhbWlub0VkZ2UiVAoSUmVzb2x2ZUVkZ2VSZXF1ZXN0EhEKCW9iamVjdF9pZBgBIAEoCRIUCgxlZGdlX3R5cGVfaWQYAiABKAkSFQoNcHJvamVjdGlvbl9pZBgDIAEoCSI4ChNSZXNvbHZlRWRnZVJlc3BvbnNlEiEKBWVkZ2VzGAEgAygLMhIuY2FtaW5vLkNhbWlub0VkZ2UiKAoVRGlzY29ubmVjdEVkZ2VSZXF1ZXN0Eg8KB2VkZ2VfaWQYASABKAkiKQoWRGlzY29ubmVjdEVkZ2VSZXNwb25zZRIPCgdlZGdlX2lkGAEgASgJIlgKD0NvbGxlY3Rpb25FbnRyeRIPCgdlZGdlX2lkGAEgASgJEhgKEHRhcmdldF9vYmplY3RfaWQYAiABKAkSGgoDa2V5GAMgASgLMg0uY2FtaW5vLlZhbHVlIlQKFlJlYWRDb2xsZWN0aW9uUmVzcG9uc2USEAoIcmV2aXNpb24YASABKAQSKAoHZW50cmllcxgCIAMoCzIXLmNhbWluby5Db2xsZWN0aW9uRW50cnkinwEKGFJlcGxhY2VDb2xsZWN0aW9uUmVxdWVzdBIRCglvYmplY3RfaWQYASABKAkSFAoMZWRnZV90eXBlX2lkGAIgASgJEhUKDXByb2plY3Rpb25faWQYAyABKAkSGQoRZXhwZWN0ZWRfcmV2aXNpb24YBCABKAQSKAoHZW50cmllcxgFIAMoCzIXLmNhbWluby5Db2xsZWN0aW9uRW50cnkiIwoOTGlzdE9wc1JlcXVlc3QSEQoJb2JqZWN0X2lkGAEgASgJIjAKD0xpc3RPcHNSZXNwb25zZRIdCgNvcHMYASADKAsyEC5jYW1pbm8uQ2FtaW5vT3AibgoSV2F0Y2hPYmplY3RSZXF1ZXN0EhEKCW9iamVjdF9pZBgBIAEoCRITCgthZnRlcl9vcF9pZBgCIAEoCRIYChBpbmNsdWRlX3NuYXBzaG90GAMgASgIEhYKDmF0dGFjaG1lbnRfaWRzGAQgAygJImsKEFdhdGNoT2JqZWN0RXZlbnQSEQoJb2JqZWN0X2lkGAEgASgJEiYKCHNuYXBzaG90GAIgASgLMhQuY2FtaW5vLkNhbWlub09iamVjdBIcCgJvcBgDIAEoCzIQLmNhbWluby5DYW1pbm9PcCLZAgoMQ2FtaW5vT2JqZWN0EgoKAmlkGAEgASgJEg8KB2F0b21faWQYAiABKAkSHQoVd29ya3NwYWNlX3JldmlzaW9uX2lkGAMgASgJEhIKCmNyZWF0ZWRfYXQYBCABKAkSEgoKdXBkYXRlZF9hdBgFIAEoCRIuCgVzdGF0ZRgGIAMoCzIfLmNhbWluby5DYW1pbm9PYmplY3QuU3RhdGVFbnRyeRJBCg9zdGF0ZV9yZXZpc2lvbnMYByADKAsyKC5jYW1pbm8uQ2FtaW5vT2JqZWN0LlN0YXRlUmV2aXNpb25zRW50cnkaOwoKU3RhdGVFbnRyeRILCgNrZXkYASABKAkSHAoFdmFsdWUYAiABKAsyDS5jYW1pbm8uVmFsdWU6AjgBGjUKE1N0YXRlUmV2aXNpb25zRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgEOgI4ASK/AgoKQ2FtaW5vRWRnZRIKCgJpZBgBIAEoCRIUCgxlZGdlX3R5cGVfaWQYAiABKAkSFwoPZmlyc3Rfb2JqZWN0X2lkGAMgASgJEhgKEHNlY29uZF9vYmplY3RfaWQYBCABKAkSGwoTZmlyc3RfcHJvamVjdGlvbl9pZBgFIAEoCRIcChRzZWNvbmRfcHJvamVjdGlvbl9pZBgGIAEoCRIaCg1maXJzdF9vcmRpbmFsGAcgASgFSACIAQESGwoOc2Vjb25kX29yZGluYWwYCCABKAVIAYgBARISCgpjcmVhdGVkX2F0GAkgASgJEhYKDmZpcnN0X2tleV9qc29uGAogASgJEhcKD3NlY29uZF9rZXlfanNvbhgLIAEoCUIQCg5fZmlyc3Rfb3JkaW5hbEIRCg9fc2Vjb25kX29yZGluYWwimQEKCENhbWlub09wEgoKAmlkGAEgASgJEhEKCW9iamVjdF9pZBgCIAEoCRIPCgdvcF9raW5kGAMgASgJEh4KB3BheWxvYWQYBCABKAsyDS5jYW1pbm8uVmFsdWUSDQoFYWN0b3IYBSABKAkSEgoKY3JlYXRlZF9hdBgGIAEoCRIaChJjbGllbnRfbXV0YXRpb25faWQYByABKAki0AEKDFF1ZXJ5UmVxdWVzdBIQCghxdWVyeV9pZBgBIAEoCRIRCglvYmplY3RfaWQYAiABKAkSNgoJdmFyaWFibGVzGAMgAygLMiMuY2FtaW5vLlF1ZXJ5UmVxdWVzdC5WYXJpYWJsZXNFbnRyeRIiChpleHBlY3RlZF9kZWZpbml0aW9uX2RpZ2VzdBgEIAEoCRo/Cg5WYXJpYWJsZXNFbnRyeRILCgNrZXkYASABKAkSHAoFdmFsdWUYAiABKAsyDS5jYW1pbm8uVmFsdWU6AjgBIjkKDVF1ZXJ5UGF0aFBhcnQSDwoFZmllbGQYASABKAlIABIPCgVpbmRleBgCIAEoDUgAQgYKBHBhcnQijAIKEVF1ZXJ5UGVuZGluZ0ZpZWxkEiMKBHBhdGgYASADKAsyFS5jYW1pbm8uUXVlcnlQYXRoUGFydBIRCglvYmplY3RfaWQYAiABKAkSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAMgASgJEhEKCW1lbWJlcl9pZBgEIAEoCRIUCgxvcGVyYXRpb25faWQYBSABKAkSFwoPdmFsdWVfdHlwZV9qc29uGAYgASgJEhwKD3Jlc2lkdWFsX3dpbmRvdxgHIAEoDUgAiAEBEhQKDHJlc2lkdWFsX3JvdxgIIAEoDRIWCg5yZXNpZHVhbF9maWVsZBgJIAEoCUISChBfcmVzaWR1YWxfd2luZG93IpIBCgpRdWVyeVN0YXRzEhEKCXNxbF9jb3VudBgBIAEoDRIOCgZzcWxfbXMYAiABKAESEQoJcnBjX2NvdW50GAMgASgNEg4KBnJwY19tcxgEIAEoARIUCgxyZXN1bHRfYnl0ZXMYBSABKA0SFgoOcHJlcGFyYXRpb25fbXMYBiABKAESEAoIdG90YWxfbXMYByABKAEivAIKDVF1ZXJ5UmVzcG9uc2USHAoFdmFsdWUYASABKAsyDS5jYW1pbm8uVmFsdWUSFAoMZGF0YV92ZXJzaW9uGAIgASgJEhYKDmJpbmRpbmdfZGlnZXN0GAMgASgJEioKB3BlbmRpbmcYBCADKAsyGS5jYW1pbm8uUXVlcnlQZW5kaW5nRmllbGQSIQoFc3RhdHMYBSABKAsyEi5jYW1pbm8uUXVlcnlTdGF0cxIZChFwcmVwYXJhdGlvbl90b2tlbhgGIAEoCRIpCgZlcnJvcnMYByADKAsyGS5jYW1pbm8uUXVlcnlGaWVsZEZhaWx1cmUSNQoQcmVzaWR1YWxfd2luZG93cxgIIAMoCzIbLmNhbWluby5RdWVyeVJlc2lkdWFsV2luZG93EhMKC2NvbnNpc3RlbmN5GAkgASgJIpgBChBRdWVyeVJlc2lkdWFsUm93EhAKCGVudHJ5X2lkGAEgASgJEjQKBmZpZWxkcxgCIAMoCzIkLmNhbWluby5RdWVyeVJlc2lkdWFsUm93LkZpZWxkc0VudHJ5GjwKC0ZpZWxkc0VudHJ5EgsKA2tleRgBIAEoCRIcCgV2YWx1ZRgCIAEoCzINLmNhbWluby5WYWx1ZToCOAEiNwoSUXVlcnlSZXNpZHVhbE9yZGVyEg0KBWZpZWxkGAEgASgJEhIKCmRlc2NlbmRpbmcYAiABKAgi6QIKE1F1ZXJ5UmVzaWR1YWxXaW5kb3cSIwoEcGF0aBgBIAMoCzIVLmNhbWluby5RdWVyeVBhdGhQYXJ0EiYKBHJvd3MYAiADKAsyGC5jYW1pbm8uUXVlcnlSZXNpZHVhbFJvdxIWCg5wcmVkaWNhdGVfanNvbhgDIAEoCRIpCgVvcmRlchgEIAMoCzIaLmNhbWluby5RdWVyeVJlc2lkdWFsT3JkZXISDQoFbGltaXQYBSABKA0SEwoLYm91bmRlZF9hbGwYBiABKAgSKQoJc2VsZWN0aW9uGAcgAygLMhYuY2FtaW5vLlF1ZXJ5U2VsZWN0aW9uEkAKC2ZpZWxkX3R5cGVzGAggAygLMisuY2FtaW5vLlF1ZXJ5UmVzaWR1YWxXaW5kb3cuRmllbGRUeXBlc0VudHJ5GjEKD0ZpZWxkVHlwZXNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIkcKEVF1ZXJ5RmllbGRGYWlsdXJlEiMKBHBhdGgYASADKAsyFS5jYW1pbm8uUXVlcnlQYXRoUGFydBINCgVlcnJvchgCIAEoCSJQChNRdWVyeUNoYW5nZXNSZXF1ZXN0EhAKCHF1ZXJ5X2lkGAEgASgJEhEKCW9iamVjdF9pZBgCIAEoCRIUCgxkYXRhX3ZlcnNpb24YAyABKAkiJwoUUXVlcnlDaGFuZ2VzUmVzcG9uc2USDwoHY2hhbmdlZBgBIAEoCDLCCQoNQ2FtaW5vU2VydmljZRI7CgxFeGVjdXRlUXVlcnkSFC5jYW1pbm8uUXVlcnlSZXF1ZXN0GhUuY2FtaW5vLlF1ZXJ5UmVzcG9uc2USSQoMUXVlcnlDaGFuZ2VzEhsuY2FtaW5vLlF1ZXJ5Q2hhbmdlc1JlcXVlc3QaHC5jYW1pbm8uUXVlcnlDaGFuZ2VzUmVzcG9uc2USZwoWSW5zdGFsbFBlcnNpc3RlbmNlUGxhbhIlLmNhbWluby5JbnN0YWxsUGVyc2lzdGVuY2VQbGFuUmVxdWVzdBomLmNhbWluby5JbnN0YWxsUGVyc2lzdGVuY2VQbGFuUmVzcG9uc2USWwoSR2V0UGVyc2lzdGVuY2VQbGFuEiEuY2FtaW5vLkdldFBlcnNpc3RlbmNlUGxhblJlcXVlc3QaIi5jYW1pbm8uR2V0UGVyc2lzdGVuY2VQbGFuUmVzcG9uc2USSQoMQ3JlYXRlT2JqZWN0EhsuY2FtaW5vLkNyZWF0ZU9iamVjdFJlcXVlc3QaHC5jYW1pbm8uQ3JlYXRlT2JqZWN0UmVzcG9uc2USQAoJR2V0T2JqZWN0EhguY2FtaW5vLkdldE9iamVjdFJlcXVlc3QaGS5jYW1pbm8uR2V0T2JqZWN0UmVzcG9uc2USRgoLTGlzdE9iamVjdHMSGi5jYW1pbm8uTGlzdE9iamVjdHNSZXF1ZXN0GhsuY2FtaW5vLkxpc3RPYmplY3RzUmVzcG9uc2USQAoJUmVhZFN0YXRlEhguY2FtaW5vLlJlYWRTdGF0ZVJlcXVlc3QaGS5jYW1pbm8uUmVhZFN0YXRlUmVzcG9uc2USQwoKV3JpdGVTdGF0ZRIZLmNhbWluby5Xcml0ZVN0YXRlUmVxdWVzdBoaLmNhbWluby5Xcml0ZVN0YXRlUmVzcG9uc2USRgoLQ29ubmVjdEVkZ2USGi5jYW1pbm8uQ29ubmVjdEVkZ2VSZXF1ZXN0GhsuY2FtaW5vLkNvbm5lY3RFZGdlUmVzcG9uc2USRgoLUmVzb2x2ZUVkZ2USGi5jYW1pbm8uUmVzb2x2ZUVkZ2VSZXF1ZXN0GhsuY2FtaW5vLlJlc29sdmVFZGdlUmVzcG9uc2USTwoORGlzY29ubmVjdEVkZ2USHS5jYW1pbm8uRGlzY29ubmVjdEVkZ2VSZXF1ZXN0Gh4uY2FtaW5vLkRpc2Nvbm5lY3RFZGdlUmVzcG9uc2USTAoOUmVhZENvbGxlY3Rpb24SGi5jYW1pbm8uUmVzb2x2ZUVkZ2VSZXF1ZXN0Gh4uY2FtaW5vLlJlYWRDb2xsZWN0aW9uUmVzcG9uc2USVQoRUmVwbGFjZUNvbGxlY3Rpb24SIC5jYW1pbm8uUmVwbGFjZUNvbGxlY3Rpb25SZXF1ZXN0Gh4uY2FtaW5vLlJlYWRDb2xsZWN0aW9uUmVzcG9uc2USOgoHTGlzdE9wcxIWLmNhbWluby5MaXN0T3BzUmVxdWVzdBoXLmNhbWluby5MaXN0T3BzUmVzcG9uc2USRQoLV2F0Y2hPYmplY3QSGi5jYW1pbm8uV2F0Y2hPYmplY3RSZXF1ZXN0GhguY2FtaW5vLldhdGNoT2JqZWN0RXZlbnQwAWIGcHJvdG8z", [file_camino_schema]); + fileDesc("ChBjYW1pbm8vYXBpLnByb3RvEgZjYW1pbm8iCwoJTnVsbFZhbHVlInwKC09iamVjdFZhbHVlEi8KBmZpZWxkcxgBIAMoCzIfLmNhbWluby5PYmplY3RWYWx1ZS5GaWVsZHNFbnRyeRo8CgtGaWVsZHNFbnRyeRILCgNrZXkYASABKAkSHAoFdmFsdWUYAiABKAsyDS5jYW1pbm8uVmFsdWU6AjgBIioKCUxpc3RWYWx1ZRIdCgZ2YWx1ZXMYASADKAsyDS5jYW1pbm8uVmFsdWUiHQoIUmVmVmFsdWUSEQoJb2JqZWN0X2lkGAEgASgJIjwKCUNyZHRWYWx1ZRIMCgR0eXBlGAEgASgJEhAKCGVuY29kaW5nGAIgASgJEg8KB3BheWxvYWQYAyABKAwiqAEKEFN0YXRlVmFsdWVTb3VyY2USEQoJb2JqZWN0X2lkGAEgASgJEg8KB3Nsb3RfaWQYAiABKAkSFwoPdmFsdWVfdHlwZV9qc29uGAMgASgJEhsKE3N0b3JhZ2VfcG9saWN5X2pzb24YBCABKAkSEAoIcmV2aXNpb24YBSABKAQSKAoNY3JkdF9zbmFwc2hvdBgGIAEoCzIRLmNhbWluby5DcmR0VmFsdWUiNgoLVmFsdWVTb3VyY2USJwoFc3RhdGUYASABKAsyGC5jYW1pbm8uU3RhdGVWYWx1ZVNvdXJjZSL5AgoFVmFsdWUSJwoKbnVsbF92YWx1ZRgBIAEoCzIRLmNhbWluby5OdWxsVmFsdWVIABIUCgpib29sX3ZhbHVlGAIgASgISAASFgoMbnVtYmVyX3ZhbHVlGAMgASgBSAASFgoMc3RyaW5nX3ZhbHVlGAQgASgJSAASFwoNaW50ZWdlcl92YWx1ZRgFIAEoCUgAEhUKC2J5dGVzX3ZhbHVlGAYgASgMSAASKwoMb2JqZWN0X3ZhbHVlGAcgASgLMhMuY2FtaW5vLk9iamVjdFZhbHVlSAASJwoKbGlzdF92YWx1ZRgIIAEoCzIRLmNhbWluby5MaXN0VmFsdWVIABIlCglyZWZfdmFsdWUYCSABKAsyEC5jYW1pbm8uUmVmVmFsdWVIABInCgpjcmR0X3ZhbHVlGAogASgLMhEuY2FtaW5vLkNyZHRWYWx1ZUgAEiMKBnNvdXJjZRgLIAEoCzITLmNhbWluby5WYWx1ZVNvdXJjZUIGCgRraW5kIkYKHUluc3RhbGxQZXJzaXN0ZW5jZVBsYW5SZXF1ZXN0EiUKBHBsYW4YASABKAsyFy5jYW1pbm8uUGVyc2lzdGVuY2VQbGFuIkcKHkluc3RhbGxQZXJzaXN0ZW5jZVBsYW5SZXNwb25zZRIlCgRwbGFuGAEgASgLMhcuY2FtaW5vLlBlcnNpc3RlbmNlUGxhbiIbChlHZXRQZXJzaXN0ZW5jZVBsYW5SZXF1ZXN0IkMKGkdldFBlcnNpc3RlbmNlUGxhblJlc3BvbnNlEiUKBHBsYW4YASABKAsyFy5jYW1pbm8uUGVyc2lzdGVuY2VQbGFuIrABChNDcmVhdGVPYmplY3RSZXF1ZXN0Eg8KB2F0b21faWQYASABKAkSRAoNaW5pdGlhbF9zdGF0ZRgCIAMoCzItLmNhbWluby5DcmVhdGVPYmplY3RSZXF1ZXN0LkluaXRpYWxTdGF0ZUVudHJ5GkIKEUluaXRpYWxTdGF0ZUVudHJ5EgsKA2tleRgBIAEoCRIcCgV2YWx1ZRgCIAEoCzINLmNhbWluby5WYWx1ZToCOAEiPAoUQ3JlYXRlT2JqZWN0UmVzcG9uc2USJAoGb2JqZWN0GAEgASgLMhQuY2FtaW5vLkNhbWlub09iamVjdCIlChBHZXRPYmplY3RSZXF1ZXN0EhEKCW9iamVjdF9pZBgBIAEoCSI5ChFHZXRPYmplY3RSZXNwb25zZRIkCgZvYmplY3QYASABKAsyFC5jYW1pbm8uQ2FtaW5vT2JqZWN0IiUKEkxpc3RPYmplY3RzUmVxdWVzdBIPCgdhdG9tX2lkGAEgASgJIjwKE0xpc3RPYmplY3RzUmVzcG9uc2USJQoHb2JqZWN0cxgBIAMoCzIULmNhbWluby5DYW1pbm9PYmplY3QiNgoQUmVhZFN0YXRlUmVxdWVzdBIRCglvYmplY3RfaWQYASABKAkSDwoHc2xvdF9pZBgCIAEoCSIxChFSZWFkU3RhdGVSZXNwb25zZRIcCgV2YWx1ZRgBIAEoCzINLmNhbWluby5WYWx1ZSJxChFXcml0ZVN0YXRlUmVxdWVzdBIRCglvYmplY3RfaWQYASABKAkSDwoHc2xvdF9pZBgCIAEoCRIcCgV2YWx1ZRgDIAEoCzINLmNhbWluby5WYWx1ZRIaChJjbGllbnRfbXV0YXRpb25faWQYBCABKAkiWAoSV3JpdGVTdGF0ZVJlc3BvbnNlEhwKBXZhbHVlGAEgASgLMg0uY2FtaW5vLlZhbHVlEiQKBm9iamVjdBgCIAEoCzIULmNhbWluby5DYW1pbm9PYmplY3QiwAEKEkNvbm5lY3RFZGdlUmVxdWVzdBIUCgxlZGdlX3R5cGVfaWQYASABKAkSFQoNcHJvamVjdGlvbl9pZBgCIAEoCRIRCglvYmplY3RfaWQYAyABKAkSGAoQdGFyZ2V0X29iamVjdF9pZBgEIAEoCRIUCgdvcmRpbmFsGAUgASgFSACIAQESGwoOdGFyZ2V0X29yZGluYWwYBiABKAVIAYgBAUIKCghfb3JkaW5hbEIRCg9fdGFyZ2V0X29yZGluYWwiNwoTQ29ubmVjdEVkZ2VSZXNwb25zZRIgCgRlZGdlGAEgASgLMhIuY2FtaW5vLkNhbWlub0VkZ2UiVAoSUmVzb2x2ZUVkZ2VSZXF1ZXN0EhEKCW9iamVjdF9pZBgBIAEoCRIUCgxlZGdlX3R5cGVfaWQYAiABKAkSFQoNcHJvamVjdGlvbl9pZBgDIAEoCSI4ChNSZXNvbHZlRWRnZVJlc3BvbnNlEiEKBWVkZ2VzGAEgAygLMhIuY2FtaW5vLkNhbWlub0VkZ2UiKAoVRGlzY29ubmVjdEVkZ2VSZXF1ZXN0Eg8KB2VkZ2VfaWQYASABKAkiKQoWRGlzY29ubmVjdEVkZ2VSZXNwb25zZRIPCgdlZGdlX2lkGAEgASgJIlgKD0NvbGxlY3Rpb25FbnRyeRIPCgdlZGdlX2lkGAEgASgJEhgKEHRhcmdldF9vYmplY3RfaWQYAiABKAkSGgoDa2V5GAMgASgLMg0uY2FtaW5vLlZhbHVlIlQKFlJlYWRDb2xsZWN0aW9uUmVzcG9uc2USEAoIcmV2aXNpb24YASABKAQSKAoHZW50cmllcxgCIAMoCzIXLmNhbWluby5Db2xsZWN0aW9uRW50cnkinwEKGFJlcGxhY2VDb2xsZWN0aW9uUmVxdWVzdBIRCglvYmplY3RfaWQYASABKAkSFAoMZWRnZV90eXBlX2lkGAIgASgJEhUKDXByb2plY3Rpb25faWQYAyABKAkSGQoRZXhwZWN0ZWRfcmV2aXNpb24YBCABKAQSKAoHZW50cmllcxgFIAMoCzIXLmNhbWluby5Db2xsZWN0aW9uRW50cnkiIwoOTGlzdE9wc1JlcXVlc3QSEQoJb2JqZWN0X2lkGAEgASgJIjAKD0xpc3RPcHNSZXNwb25zZRIdCgNvcHMYASADKAsyEC5jYW1pbm8uQ2FtaW5vT3AibgoSV2F0Y2hPYmplY3RSZXF1ZXN0EhEKCW9iamVjdF9pZBgBIAEoCRITCgthZnRlcl9vcF9pZBgCIAEoCRIYChBpbmNsdWRlX3NuYXBzaG90GAMgASgIEhYKDmF0dGFjaG1lbnRfaWRzGAQgAygJImsKEFdhdGNoT2JqZWN0RXZlbnQSEQoJb2JqZWN0X2lkGAEgASgJEiYKCHNuYXBzaG90GAIgASgLMhQuY2FtaW5vLkNhbWlub09iamVjdBIcCgJvcBgDIAEoCzIQLmNhbWluby5DYW1pbm9PcCLZAgoMQ2FtaW5vT2JqZWN0EgoKAmlkGAEgASgJEg8KB2F0b21faWQYAiABKAkSHQoVd29ya3NwYWNlX3JldmlzaW9uX2lkGAMgASgJEhIKCmNyZWF0ZWRfYXQYBCABKAkSEgoKdXBkYXRlZF9hdBgFIAEoCRIuCgVzdGF0ZRgGIAMoCzIfLmNhbWluby5DYW1pbm9PYmplY3QuU3RhdGVFbnRyeRJBCg9zdGF0ZV9yZXZpc2lvbnMYByADKAsyKC5jYW1pbm8uQ2FtaW5vT2JqZWN0LlN0YXRlUmV2aXNpb25zRW50cnkaOwoKU3RhdGVFbnRyeRILCgNrZXkYASABKAkSHAoFdmFsdWUYAiABKAsyDS5jYW1pbm8uVmFsdWU6AjgBGjUKE1N0YXRlUmV2aXNpb25zRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgEOgI4ASK/AgoKQ2FtaW5vRWRnZRIKCgJpZBgBIAEoCRIUCgxlZGdlX3R5cGVfaWQYAiABKAkSFwoPZmlyc3Rfb2JqZWN0X2lkGAMgASgJEhgKEHNlY29uZF9vYmplY3RfaWQYBCABKAkSGwoTZmlyc3RfcHJvamVjdGlvbl9pZBgFIAEoCRIcChRzZWNvbmRfcHJvamVjdGlvbl9pZBgGIAEoCRIaCg1maXJzdF9vcmRpbmFsGAcgASgFSACIAQESGwoOc2Vjb25kX29yZGluYWwYCCABKAVIAYgBARISCgpjcmVhdGVkX2F0GAkgASgJEhYKDmZpcnN0X2tleV9qc29uGAogASgJEhcKD3NlY29uZF9rZXlfanNvbhgLIAEoCUIQCg5fZmlyc3Rfb3JkaW5hbEIRCg9fc2Vjb25kX29yZGluYWwimQEKCENhbWlub09wEgoKAmlkGAEgASgJEhEKCW9iamVjdF9pZBgCIAEoCRIPCgdvcF9raW5kGAMgASgJEh4KB3BheWxvYWQYBCABKAsyDS5jYW1pbm8uVmFsdWUSDQoFYWN0b3IYBSABKAkSEgoKY3JlYXRlZF9hdBgGIAEoCRIaChJjbGllbnRfbXV0YXRpb25faWQYByABKAki0AEKDFF1ZXJ5UmVxdWVzdBIQCghxdWVyeV9pZBgBIAEoCRIRCglvYmplY3RfaWQYAiABKAkSNgoJdmFyaWFibGVzGAMgAygLMiMuY2FtaW5vLlF1ZXJ5UmVxdWVzdC5WYXJpYWJsZXNFbnRyeRIiChpleHBlY3RlZF9kZWZpbml0aW9uX2RpZ2VzdBgEIAEoCRo/Cg5WYXJpYWJsZXNFbnRyeRILCgNrZXkYASABKAkSHAoFdmFsdWUYAiABKAsyDS5jYW1pbm8uVmFsdWU6AjgBIjkKDVF1ZXJ5UGF0aFBhcnQSDwoFZmllbGQYASABKAlIABIPCgVpbmRleBgCIAEoDUgAQgYKBHBhcnQi3QIKEVF1ZXJ5UGVuZGluZ0ZpZWxkEiMKBHBhdGgYASADKAsyFS5jYW1pbm8uUXVlcnlQYXRoUGFydBIRCglvYmplY3RfaWQYAiABKAkSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAMgASgJEhEKCW1lbWJlcl9pZBgEIAEoCRIUCgxvcGVyYXRpb25faWQYBSABKAkSFwoPdmFsdWVfdHlwZV9qc29uGAYgASgJEhwKD3Jlc2lkdWFsX3dpbmRvdxgHIAEoDUgAiAEBEhQKDHJlc2lkdWFsX3JvdxgIIAEoDRIWCg5yZXNpZHVhbF9maWVsZBgJIAEoCRIfChJyZWxhdGlvbmFsX2NhcHR1cmUYCiABKA1IAYgBARIXCg9jYXB0dXJlZF9vYmplY3QYCyABKA1CEgoQX3Jlc2lkdWFsX3dpbmRvd0IVChNfcmVsYXRpb25hbF9jYXB0dXJlIuEBCgpRdWVyeVN0YXRzEhEKCXNxbF9jb3VudBgBIAEoDRIOCgZzcWxfbXMYAiABKAESEQoJcnBjX2NvdW50GAMgASgNEg4KBnJwY19tcxgEIAEoARIUCgxyZXN1bHRfYnl0ZXMYBSABKA0SFgoOcHJlcGFyYXRpb25fbXMYBiABKAESEAoIdG90YWxfbXMYByABKAESGQoRcmVsYXRpb25hbF9zdGFnZXMYCCABKA0SGwoTY2FwdHVyZWRfY2FuZGlkYXRlcxgJIAEoDRIVCg1yZWxhdGlvbmFsX21zGAogASgBIvkCCg1RdWVyeVJlc3BvbnNlEhwKBXZhbHVlGAEgASgLMg0uY2FtaW5vLlZhbHVlEhQKDGRhdGFfdmVyc2lvbhgCIAEoCRIWCg5iaW5kaW5nX2RpZ2VzdBgDIAEoCRIqCgdwZW5kaW5nGAQgAygLMhkuY2FtaW5vLlF1ZXJ5UGVuZGluZ0ZpZWxkEiEKBXN0YXRzGAUgASgLMhIuY2FtaW5vLlF1ZXJ5U3RhdHMSGQoRcHJlcGFyYXRpb25fdG9rZW4YBiABKAkSKQoGZXJyb3JzGAcgAygLMhkuY2FtaW5vLlF1ZXJ5RmllbGRGYWlsdXJlEjUKEHJlc2lkdWFsX3dpbmRvd3MYCCADKAsyGy5jYW1pbm8uUXVlcnlSZXNpZHVhbFdpbmRvdxITCgtjb25zaXN0ZW5jeRgJIAEoCRI7ChNyZWxhdGlvbmFsX2NhcHR1cmVzGAogAygLMh4uY2FtaW5vLlF1ZXJ5UmVsYXRpb25hbENhcHR1cmUiWgoTUXVlcnlDYXB0dXJlZE1lbWJlchIRCglvYmplY3RfaWQYASABKAkSEAoIZW50cnlfaWQYAiABKAkSHgoHbWFwX2tleRgDIAEoCzINLmNhbWluby5WYWx1ZSJEChRRdWVyeUNhcHR1cmVkTWVtYmVycxIsCgdlbnRyaWVzGAEgAygLMhsuY2FtaW5vLlF1ZXJ5Q2FwdHVyZWRNZW1iZXIirwMKE1F1ZXJ5Q2FwdHVyZWRPYmplY3QSEQoJb2JqZWN0X2lkGAEgASgJEjcKBmZpZWxkcxgCIAMoCzInLmNhbWluby5RdWVyeUNhcHR1cmVkT2JqZWN0LkZpZWxkc0VudHJ5EkAKC2ZpZWxkX3R5cGVzGAMgAygLMisuY2FtaW5vLlF1ZXJ5Q2FwdHVyZWRPYmplY3QuRmllbGRUeXBlc0VudHJ5EkUKDXJlbGF0aW9uc2hpcHMYBCADKAsyLi5jYW1pbm8uUXVlcnlDYXB0dXJlZE9iamVjdC5SZWxhdGlvbnNoaXBzRW50cnkaPAoLRmllbGRzRW50cnkSCwoDa2V5GAEgASgJEhwKBXZhbHVlGAIgASgLMg0uY2FtaW5vLlZhbHVlOgI4ARoxCg9GaWVsZFR5cGVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ARpSChJSZWxhdGlvbnNoaXBzRW50cnkSCwoDa2V5GAEgASgJEisKBXZhbHVlGAIgASgLMhwuY2FtaW5vLlF1ZXJ5Q2FwdHVyZWRNZW1iZXJzOgI4ASK5AgoWUXVlcnlSZWxhdGlvbmFsQ2FwdHVyZRIjCgRwYXRoGAEgAygLMhUuY2FtaW5vLlF1ZXJ5UGF0aFBhcnQSKQoEcGxhbhgCIAEoCzIbLmNhbWluby5RdWVyeVJlbGF0aW9uYWxQbGFuEhYKDnJvb3Rfb2JqZWN0X2lkGAMgASgJEiwKB29iamVjdHMYBCADKAsyGy5jYW1pbm8uUXVlcnlDYXB0dXJlZE9iamVjdBIWCg52YXJpYWJsZXNfanNvbhgFIAEoCRIRCglyb3dfbGltaXQYBiABKA0SFwoPY2FuZGlkYXRlX2xpbWl0GAcgASgNEhwKD3Jlc2lkdWFsX3dpbmRvdxgIIAEoDUgAiAEBEhMKC3Jlc3VsdF9wYXRoGAkgAygJQhIKEF9yZXNpZHVhbF93aW5kb3cimAEKEFF1ZXJ5UmVzaWR1YWxSb3cSEAoIZW50cnlfaWQYASABKAkSNAoGZmllbGRzGAIgAygLMiQuY2FtaW5vLlF1ZXJ5UmVzaWR1YWxSb3cuRmllbGRzRW50cnkaPAoLRmllbGRzRW50cnkSCwoDa2V5GAEgASgJEhwKBXZhbHVlGAIgASgLMg0uY2FtaW5vLlZhbHVlOgI4ASI3ChJRdWVyeVJlc2lkdWFsT3JkZXISDQoFZmllbGQYASABKAkSEgoKZGVzY2VuZGluZxgCIAEoCCKWAwoTUXVlcnlSZXNpZHVhbFdpbmRvdxIjCgRwYXRoGAEgAygLMhUuY2FtaW5vLlF1ZXJ5UGF0aFBhcnQSJgoEcm93cxgCIAMoCzIYLmNhbWluby5RdWVyeVJlc2lkdWFsUm93EhYKDnByZWRpY2F0ZV9qc29uGAMgASgJEikKBW9yZGVyGAQgAygLMhouY2FtaW5vLlF1ZXJ5UmVzaWR1YWxPcmRlchINCgVsaW1pdBgFIAEoDRITCgtib3VuZGVkX2FsbBgGIAEoCBIpCglzZWxlY3Rpb24YByADKAsyFi5jYW1pbm8uUXVlcnlTZWxlY3Rpb24SQAoLZmllbGRfdHlwZXMYCCADKAsyKy5jYW1pbm8uUXVlcnlSZXNpZHVhbFdpbmRvdy5GaWVsZFR5cGVzRW50cnkSEgoKcmVsYXRpb25hbBgJIAEoCBIXCg9tYXRjaGVkX2VudHJpZXMYCiADKAkaMQoPRmllbGRUeXBlc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiRwoRUXVlcnlGaWVsZEZhaWx1cmUSIwoEcGF0aBgBIAMoCzIVLmNhbWluby5RdWVyeVBhdGhQYXJ0Eg0KBWVycm9yGAIgASgJIlAKE1F1ZXJ5Q2hhbmdlc1JlcXVlc3QSEAoIcXVlcnlfaWQYASABKAkSEQoJb2JqZWN0X2lkGAIgASgJEhQKDGRhdGFfdmVyc2lvbhgDIAEoCSInChRRdWVyeUNoYW5nZXNSZXNwb25zZRIPCgdjaGFuZ2VkGAEgASgIMsIJCg1DYW1pbm9TZXJ2aWNlEjsKDEV4ZWN1dGVRdWVyeRIULmNhbWluby5RdWVyeVJlcXVlc3QaFS5jYW1pbm8uUXVlcnlSZXNwb25zZRJJCgxRdWVyeUNoYW5nZXMSGy5jYW1pbm8uUXVlcnlDaGFuZ2VzUmVxdWVzdBocLmNhbWluby5RdWVyeUNoYW5nZXNSZXNwb25zZRJnChZJbnN0YWxsUGVyc2lzdGVuY2VQbGFuEiUuY2FtaW5vLkluc3RhbGxQZXJzaXN0ZW5jZVBsYW5SZXF1ZXN0GiYuY2FtaW5vLkluc3RhbGxQZXJzaXN0ZW5jZVBsYW5SZXNwb25zZRJbChJHZXRQZXJzaXN0ZW5jZVBsYW4SIS5jYW1pbm8uR2V0UGVyc2lzdGVuY2VQbGFuUmVxdWVzdBoiLmNhbWluby5HZXRQZXJzaXN0ZW5jZVBsYW5SZXNwb25zZRJJCgxDcmVhdGVPYmplY3QSGy5jYW1pbm8uQ3JlYXRlT2JqZWN0UmVxdWVzdBocLmNhbWluby5DcmVhdGVPYmplY3RSZXNwb25zZRJACglHZXRPYmplY3QSGC5jYW1pbm8uR2V0T2JqZWN0UmVxdWVzdBoZLmNhbWluby5HZXRPYmplY3RSZXNwb25zZRJGCgtMaXN0T2JqZWN0cxIaLmNhbWluby5MaXN0T2JqZWN0c1JlcXVlc3QaGy5jYW1pbm8uTGlzdE9iamVjdHNSZXNwb25zZRJACglSZWFkU3RhdGUSGC5jYW1pbm8uUmVhZFN0YXRlUmVxdWVzdBoZLmNhbWluby5SZWFkU3RhdGVSZXNwb25zZRJDCgpXcml0ZVN0YXRlEhkuY2FtaW5vLldyaXRlU3RhdGVSZXF1ZXN0GhouY2FtaW5vLldyaXRlU3RhdGVSZXNwb25zZRJGCgtDb25uZWN0RWRnZRIaLmNhbWluby5Db25uZWN0RWRnZVJlcXVlc3QaGy5jYW1pbm8uQ29ubmVjdEVkZ2VSZXNwb25zZRJGCgtSZXNvbHZlRWRnZRIaLmNhbWluby5SZXNvbHZlRWRnZVJlcXVlc3QaGy5jYW1pbm8uUmVzb2x2ZUVkZ2VSZXNwb25zZRJPCg5EaXNjb25uZWN0RWRnZRIdLmNhbWluby5EaXNjb25uZWN0RWRnZVJlcXVlc3QaHi5jYW1pbm8uRGlzY29ubmVjdEVkZ2VSZXNwb25zZRJMCg5SZWFkQ29sbGVjdGlvbhIaLmNhbWluby5SZXNvbHZlRWRnZVJlcXVlc3QaHi5jYW1pbm8uUmVhZENvbGxlY3Rpb25SZXNwb25zZRJVChFSZXBsYWNlQ29sbGVjdGlvbhIgLmNhbWluby5SZXBsYWNlQ29sbGVjdGlvblJlcXVlc3QaHi5jYW1pbm8uUmVhZENvbGxlY3Rpb25SZXNwb25zZRI6CgdMaXN0T3BzEhYuY2FtaW5vLkxpc3RPcHNSZXF1ZXN0GhcuY2FtaW5vLkxpc3RPcHNSZXNwb25zZRJFCgtXYXRjaE9iamVjdBIaLmNhbWluby5XYXRjaE9iamVjdFJlcXVlc3QaGC5jYW1pbm8uV2F0Y2hPYmplY3RFdmVudDABYgZwcm90bzM", [file_camino_schema]); /** * @generated from message camino.NullValue @@ -1109,6 +1109,16 @@ export type QueryPendingField = Message<"camino.QueryPendingField"> & { * @generated from field: string residual_field = 9; */ residualField: string; + + /** + * @generated from field: optional uint32 relational_capture = 10; + */ + relationalCapture?: number | undefined; + + /** + * @generated from field: uint32 captured_object = 11; + */ + capturedObject: number; }; /** @@ -1156,6 +1166,21 @@ export type QueryStats = Message<"camino.QueryStats"> & { * @generated from field: double total_ms = 7; */ totalMs: number; + + /** + * @generated from field: uint32 relational_stages = 8; + */ + relationalStages: number; + + /** + * @generated from field: uint32 captured_candidates = 9; + */ + capturedCandidates: number; + + /** + * @generated from field: double relational_ms = 10; + */ + relationalMs: number; }; /** @@ -1219,6 +1244,11 @@ export type QueryResponse = Message<"camino.QueryResponse"> & { * @generated from field: string consistency = 9; */ consistency: string; + + /** + * @generated from field: repeated camino.QueryRelationalCapture relational_captures = 10; + */ + relationalCaptures: QueryRelationalCapture[]; }; /** @@ -1228,6 +1258,142 @@ export type QueryResponse = Message<"camino.QueryResponse"> & { export const QueryResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_camino_api, 42); +/** + * Private coordinator input, removed before publishing a result. Memberships + * and native facts share one snapshot; package reads are sampled afterwards. + * + * @generated from message camino.QueryCapturedMember + */ +export type QueryCapturedMember = Message<"camino.QueryCapturedMember"> & { + /** + * @generated from field: string object_id = 1; + */ + objectId: string; + + /** + * @generated from field: string entry_id = 2; + */ + entryId: string; + + /** + * @generated from field: camino.Value map_key = 3; + */ + mapKey?: Value | undefined; +}; + +/** + * Describes the message camino.QueryCapturedMember. + * Use `create(QueryCapturedMemberSchema)` to create a new message. + */ +export const QueryCapturedMemberSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_camino_api, 43); + +/** + * @generated from message camino.QueryCapturedMembers + */ +export type QueryCapturedMembers = Message<"camino.QueryCapturedMembers"> & { + /** + * @generated from field: repeated camino.QueryCapturedMember entries = 1; + */ + entries: QueryCapturedMember[]; +}; + +/** + * Describes the message camino.QueryCapturedMembers. + * Use `create(QueryCapturedMembersSchema)` to create a new message. + */ +export const QueryCapturedMembersSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_camino_api, 44); + +/** + * @generated from message camino.QueryCapturedObject + */ +export type QueryCapturedObject = Message<"camino.QueryCapturedObject"> & { + /** + * @generated from field: string object_id = 1; + */ + objectId: string; + + /** + * @generated from field: map fields = 2; + */ + fields: { [key: string]: Value }; + + /** + * @generated from field: map field_types = 3; + */ + fieldTypes: { [key: string]: string }; + + /** + * @generated from field: map relationships = 4; + */ + relationships: { [key: string]: QueryCapturedMembers }; +}; + +/** + * Describes the message camino.QueryCapturedObject. + * Use `create(QueryCapturedObjectSchema)` to create a new message. + */ +export const QueryCapturedObjectSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_camino_api, 45); + +/** + * @generated from message camino.QueryRelationalCapture + */ +export type QueryRelationalCapture = Message<"camino.QueryRelationalCapture"> & { + /** + * @generated from field: repeated camino.QueryPathPart path = 1; + */ + path: QueryPathPart[]; + + /** + * @generated from field: camino.QueryRelationalPlan plan = 2; + */ + plan?: QueryRelationalPlan | undefined; + + /** + * @generated from field: string root_object_id = 3; + */ + rootObjectId: string; + + /** + * @generated from field: repeated camino.QueryCapturedObject objects = 4; + */ + objects: QueryCapturedObject[]; + + /** + * @generated from field: string variables_json = 5; + */ + variablesJson: string; + + /** + * @generated from field: uint32 row_limit = 6; + */ + rowLimit: number; + + /** + * @generated from field: uint32 candidate_limit = 7; + */ + candidateLimit: number; + + /** + * @generated from field: optional uint32 residual_window = 8; + */ + residualWindow?: number | undefined; + + /** + * @generated from field: repeated string result_path = 9; + */ + resultPath: string[]; +}; + +/** + * Describes the message camino.QueryRelationalCapture. + * Use `create(QueryRelationalCaptureSchema)` to create a new message. + */ +export const QueryRelationalCaptureSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_camino_api, 46); + /** * @generated from message camino.QueryResidualRow */ @@ -1248,7 +1414,7 @@ export type QueryResidualRow = Message<"camino.QueryResidualRow"> & { * Use `create(QueryResidualRowSchema)` to create a new message. */ export const QueryResidualRowSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_camino_api, 43); + messageDesc(file_camino_api, 47); /** * @generated from message camino.QueryResidualOrder @@ -1270,7 +1436,7 @@ export type QueryResidualOrder = Message<"camino.QueryResidualOrder"> & { * Use `create(QueryResidualOrderSchema)` to create a new message. */ export const QueryResidualOrderSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_camino_api, 44); + messageDesc(file_camino_api, 48); /** * @generated from message camino.QueryResidualWindow @@ -1315,6 +1481,16 @@ export type QueryResidualWindow = Message<"camino.QueryResidualWindow"> & { * @generated from field: map field_types = 8; */ fieldTypes: { [key: string]: string }; + + /** + * @generated from field: bool relational = 9; + */ + relational: boolean; + + /** + * @generated from field: repeated string matched_entries = 10; + */ + matchedEntries: string[]; }; /** @@ -1322,7 +1498,7 @@ export type QueryResidualWindow = Message<"camino.QueryResidualWindow"> & { * Use `create(QueryResidualWindowSchema)` to create a new message. */ export const QueryResidualWindowSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_camino_api, 45); + messageDesc(file_camino_api, 49); /** * @generated from message camino.QueryFieldFailure @@ -1344,7 +1520,7 @@ export type QueryFieldFailure = Message<"camino.QueryFieldFailure"> & { * Use `create(QueryFieldFailureSchema)` to create a new message. */ export const QueryFieldFailureSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_camino_api, 46); + messageDesc(file_camino_api, 50); /** * @generated from message camino.QueryChangesRequest @@ -1371,7 +1547,7 @@ export type QueryChangesRequest = Message<"camino.QueryChangesRequest"> & { * Use `create(QueryChangesRequestSchema)` to create a new message. */ export const QueryChangesRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_camino_api, 47); + messageDesc(file_camino_api, 51); /** * @generated from message camino.QueryChangesResponse @@ -1388,7 +1564,7 @@ export type QueryChangesResponse = Message<"camino.QueryChangesResponse"> & { * Use `create(QueryChangesResponseSchema)` to create a new message. */ export const QueryChangesResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_camino_api, 48); + messageDesc(file_camino_api, 52); /** * @generated from service camino.CaminoService diff --git a/src/gen/camino/schema_pb.ts b/src/gen/camino/schema_pb.ts index fe7cdf3..512b38d 100644 --- a/src/gen/camino/schema_pb.ts +++ b/src/gen/camino/schema_pb.ts @@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file camino/schema.proto. */ export const file_camino_schema: GenFile = /*@__PURE__*/ - fileDesc("ChNjYW1pbm8vc2NoZW1hLnByb3RvEgZjYW1pbm8iNwoOQXRvbURlZmluaXRpb24SDwoHYXRvbV9pZBgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkiWQoPQXRvbUNvbmZvcm1hbmNlEg8KB2F0b21faWQYASABKAkSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAIgASgJEhYKDmNvbmZvcm1hbmNlX2lkGAMgASgJIsIBCg9TdGF0ZUF0dGFjaG1lbnQSDwoHc2xvdF9pZBgBIAEoCRIYChBhdHRhY2hlZF9hdG9tX2lkGAIgASgJEhQKDGRpc3BsYXlfbmFtZRgDIAEoCRIXCg92YWx1ZV90eXBlX2pzb24YBCABKAkSGwoTc3RvcmFnZV9wb2xpY3lfanNvbhgFIAEoCRIaChJkZWZhdWx0X3ZhbHVlX2pzb24YBiABKAkSHAoUb3duZXJfY29uZm9ybWFuY2VfaWQYByABKAkiUAoSRW5kcG9pbnRDb25zdHJhaW50EhEKB2F0b21faWQYASABKAlIABIfChVpbnRlcmZhY2VfcmV2aXNpb25faWQYAiABKAlIAEIGCgRraW5kIvsBCgxFZGdlRW5kcG9pbnQSFQoNcHJvamVjdGlvbl9pZBgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkSLgoKY29uc3RyYWludBgDIAEoCzIaLmNhbWluby5FbmRwb2ludENvbnN0cmFpbnQSKAoLY2FyZGluYWxpdHkYBCABKA4yEy5jYW1pbm8uQ2FyZGluYWxpdHkSDwoHb3JkZXJlZBgFIAEoCBIRCglvbl9kZWxldGUYBiABKAkSFAoMcmV0YWluX290aGVyGAcgASgIEhAKCGtleV90eXBlGAggASgJEhgKEHB1YmxpY190cmF2ZXJzYWwYCSABKAgipQEKDkVkZ2VBdHRhY2htZW50EhQKDGVkZ2VfdHlwZV9pZBgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkSIwoFZmlyc3QYAyABKAsyFC5jYW1pbm8uRWRnZUVuZHBvaW50EiQKBnNlY29uZBgEIAEoCzIULmNhbWluby5FZGdlRW5kcG9pbnQSHAoUb3duZXJfY29uZm9ybWFuY2VfaWQYBSABKAkilQIKD1BlcnNpc3RlbmNlUGxhbhIUCgx3b3Jrc3BhY2VfaWQYASABKAkSHQoVd29ya3NwYWNlX3JldmlzaW9uX2lkGAIgASgJEiUKBWF0b21zGAMgAygLMhYuY2FtaW5vLkF0b21EZWZpbml0aW9uEi0KDGNvbmZvcm1hbmNlcxgEIAMoCzIXLmNhbWluby5BdG9tQ29uZm9ybWFuY2USJwoGc3RhdGVzGAUgAygLMhcuY2FtaW5vLlN0YXRlQXR0YWNobWVudBIlCgVlZGdlcxgGIAMoCzIWLmNhbWluby5FZGdlQXR0YWNobWVudBInCgdxdWVyaWVzGAcgAygLMhYuY2FtaW5vLkluc3RhbGxlZFF1ZXJ5Ip4BCg1RdWVyeUFyZ3VtZW50EhIKCHZhcmlhYmxlGAEgASgJSAASFgoMbGl0ZXJhbF9qc29uGAIgASgJSAASKQoEbGlzdBgDIAEoCzIZLmNhbWluby5RdWVyeUFyZ3VtZW50TGlzdEgAEi0KBm9iamVjdBgEIAEoCzIbLmNhbWluby5RdWVyeUFyZ3VtZW50T2JqZWN0SABCBwoFdmFsdWUiOgoRUXVlcnlBcmd1bWVudExpc3QSJQoGdmFsdWVzGAEgAygLMhUuY2FtaW5vLlF1ZXJ5QXJndW1lbnQilAEKE1F1ZXJ5QXJndW1lbnRPYmplY3QSNwoGZmllbGRzGAEgAygLMicuY2FtaW5vLlF1ZXJ5QXJndW1lbnRPYmplY3QuRmllbGRzRW50cnkaRAoLRmllbGRzRW50cnkSCwoDa2V5GAEgASgJEiQKBXZhbHVlGAIgASgLMhUuY2FtaW5vLlF1ZXJ5QXJndW1lbnQ6AjgBIkcKDlF1ZXJ5Q29uZGl0aW9uEg8KB2luY2x1ZGUYASABKAgSJAoFdmFsdWUYAiABKAsyFS5jYW1pbm8uUXVlcnlBcmd1bWVudCLdAgoOUXVlcnlTZWxlY3Rpb24SDAoEbmFtZRgBIAEoCRILCgNrZXkYAiABKAkSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAMgASgJEhEKCW1lbWJlcl9pZBgEIAEoCRIkChx0YXJnZXRfaW50ZXJmYWNlX3JldmlzaW9uX2lkGAUgASgJEioKCmNvbmRpdGlvbnMYBiADKAsyFi5jYW1pbm8uUXVlcnlDb25kaXRpb24SOAoJYXJndW1lbnRzGAcgAygLMiUuY2FtaW5vLlF1ZXJ5U2VsZWN0aW9uLkFyZ3VtZW50c0VudHJ5EikKCXNlbGVjdGlvbhgIIAMoCzIWLmNhbWluby5RdWVyeVNlbGVjdGlvbhpHCg5Bcmd1bWVudHNFbnRyeRILCgNrZXkYASABKAkSJAoFdmFsdWUYAiABKAsyFS5jYW1pbm8uUXVlcnlBcmd1bWVudDoCOAEi6QIKEFF1ZXJ5UmVhZEJpbmRpbmcSDwoHYXRvbV9pZBgBIAEoCRIdChVpbnRlcmZhY2VfcmV2aXNpb25faWQYAiABKAkSEQoJbWVtYmVyX2lkGAMgASgJEhsKE2dldHRlcl9vcGVyYXRpb25faWQYBCABKAkSDwoHc2xvdF9pZBgFIAEoCRIUCgxlZGdlX3R5cGVfaWQYBiABKAkSFQoNcHJvamVjdGlvbl9pZBgHIAEoCRILCgNycGMYCCABKAgSIAoYd2F0Y2hfc3RhcnRfb3BlcmF0aW9uX2lkGAkgASgJEh8KF3dhdGNoX3N0b3Bfb3BlcmF0aW9uX2lkGAogASgJEhIKCmZpZWxkX25hbWUYCyABKAkSFwoPdmFsdWVfdHlwZV9qc29uGAwgASgJEigKC2NhcmRpbmFsaXR5GA0gASgOMhMuY2FtaW5vLkNhcmRpbmFsaXR5EhAKCGtleV90eXBlGA4gASgJIpIBCgxRdWVyeUJ1ZGdldHMSDAoEcm93cxgBIAEoDRINCgVkZXB0aBgCIAEoDRIUCgxyZXN1bHRfYnl0ZXMYAyABKA0SEgoKY2FuZGlkYXRlcxgEIAEoDRIRCglycGNfY2FsbHMYBSABKA0SEwoLY29uY3VycmVuY3kYBiABKA0SEwoLZGVhZGxpbmVfbXMYByABKA0ijQQKDkluc3RhbGxlZFF1ZXJ5EgoKAmlkGAEgASgJEhkKEWRlZmluaXRpb25fZGlnZXN0GAIgASgJEhYKDmJpbmRpbmdfZGlnZXN0GAMgASgJEiIKGnJvb3RfaW50ZXJmYWNlX3JldmlzaW9uX2lkGAQgASgJEikKCXNlbGVjdGlvbhgFIAMoCzIWLmNhbWluby5RdWVyeVNlbGVjdGlvbhIqCghiaW5kaW5ncxgGIAMoCzIYLmNhbWluby5RdWVyeVJlYWRCaW5kaW5nEiUKB2J1ZGdldHMYByABKAsyFC5jYW1pbm8uUXVlcnlCdWRnZXRzEhsKE3ZhcmlhYmxlc190eXBlX2pzb24YCCABKAkSGAoQb3V0cHV0X3R5cGVfanNvbhgJIAEoCRJHChF2YXJpYWJsZV9kZWZhdWx0cxgKIAMoCzIsLmNhbWluby5JbnN0YWxsZWRRdWVyeS5WYXJpYWJsZURlZmF1bHRzRW50cnkSDQoFd2F0Y2gYCyABKAgSGwoTcG9sbGluZ19pbnRlcnZhbF9tcxgMIAEoDRIeChZycGNfcHJlZGljYXRlX29yX29yZGVyGA0gASgIGk4KFVZhcmlhYmxlRGVmYXVsdHNFbnRyeRILCgNrZXkYASABKAkSJAoFdmFsdWUYAiABKAsyFS5jYW1pbm8uUXVlcnlBcmd1bWVudDoCOAEqaAoLQ2FyZGluYWxpdHkSGwoXQ0FSRElOQUxJVFlfVU5TUEVDSUZJRUQQABIQCgxPUFRJT05BTF9PTkUQARIPCgtFWEFDVExZX09ORRACEggKBE1BTlkQAxIPCgtNQU5ZX1VOSVFVRRAEYgZwcm90bzM"); + fileDesc("ChNjYW1pbm8vc2NoZW1hLnByb3RvEgZjYW1pbm8iNwoOQXRvbURlZmluaXRpb24SDwoHYXRvbV9pZBgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkiWQoPQXRvbUNvbmZvcm1hbmNlEg8KB2F0b21faWQYASABKAkSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAIgASgJEhYKDmNvbmZvcm1hbmNlX2lkGAMgASgJIsIBCg9TdGF0ZUF0dGFjaG1lbnQSDwoHc2xvdF9pZBgBIAEoCRIYChBhdHRhY2hlZF9hdG9tX2lkGAIgASgJEhQKDGRpc3BsYXlfbmFtZRgDIAEoCRIXCg92YWx1ZV90eXBlX2pzb24YBCABKAkSGwoTc3RvcmFnZV9wb2xpY3lfanNvbhgFIAEoCRIaChJkZWZhdWx0X3ZhbHVlX2pzb24YBiABKAkSHAoUb3duZXJfY29uZm9ybWFuY2VfaWQYByABKAkiUAoSRW5kcG9pbnRDb25zdHJhaW50EhEKB2F0b21faWQYASABKAlIABIfChVpbnRlcmZhY2VfcmV2aXNpb25faWQYAiABKAlIAEIGCgRraW5kIvsBCgxFZGdlRW5kcG9pbnQSFQoNcHJvamVjdGlvbl9pZBgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkSLgoKY29uc3RyYWludBgDIAEoCzIaLmNhbWluby5FbmRwb2ludENvbnN0cmFpbnQSKAoLY2FyZGluYWxpdHkYBCABKA4yEy5jYW1pbm8uQ2FyZGluYWxpdHkSDwoHb3JkZXJlZBgFIAEoCBIRCglvbl9kZWxldGUYBiABKAkSFAoMcmV0YWluX290aGVyGAcgASgIEhAKCGtleV90eXBlGAggASgJEhgKEHB1YmxpY190cmF2ZXJzYWwYCSABKAgipQEKDkVkZ2VBdHRhY2htZW50EhQKDGVkZ2VfdHlwZV9pZBgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkSIwoFZmlyc3QYAyABKAsyFC5jYW1pbm8uRWRnZUVuZHBvaW50EiQKBnNlY29uZBgEIAEoCzIULmNhbWluby5FZGdlRW5kcG9pbnQSHAoUb3duZXJfY29uZm9ybWFuY2VfaWQYBSABKAkilQIKD1BlcnNpc3RlbmNlUGxhbhIUCgx3b3Jrc3BhY2VfaWQYASABKAkSHQoVd29ya3NwYWNlX3JldmlzaW9uX2lkGAIgASgJEiUKBWF0b21zGAMgAygLMhYuY2FtaW5vLkF0b21EZWZpbml0aW9uEi0KDGNvbmZvcm1hbmNlcxgEIAMoCzIXLmNhbWluby5BdG9tQ29uZm9ybWFuY2USJwoGc3RhdGVzGAUgAygLMhcuY2FtaW5vLlN0YXRlQXR0YWNobWVudBIlCgVlZGdlcxgGIAMoCzIWLmNhbWluby5FZGdlQXR0YWNobWVudBInCgdxdWVyaWVzGAcgAygLMhYuY2FtaW5vLkluc3RhbGxlZFF1ZXJ5Ip4BCg1RdWVyeUFyZ3VtZW50EhIKCHZhcmlhYmxlGAEgASgJSAASFgoMbGl0ZXJhbF9qc29uGAIgASgJSAASKQoEbGlzdBgDIAEoCzIZLmNhbWluby5RdWVyeUFyZ3VtZW50TGlzdEgAEi0KBm9iamVjdBgEIAEoCzIbLmNhbWluby5RdWVyeUFyZ3VtZW50T2JqZWN0SABCBwoFdmFsdWUiOgoRUXVlcnlBcmd1bWVudExpc3QSJQoGdmFsdWVzGAEgAygLMhUuY2FtaW5vLlF1ZXJ5QXJndW1lbnQilAEKE1F1ZXJ5QXJndW1lbnRPYmplY3QSNwoGZmllbGRzGAEgAygLMicuY2FtaW5vLlF1ZXJ5QXJndW1lbnRPYmplY3QuRmllbGRzRW50cnkaRAoLRmllbGRzRW50cnkSCwoDa2V5GAEgASgJEiQKBXZhbHVlGAIgASgLMhUuY2FtaW5vLlF1ZXJ5QXJndW1lbnQ6AjgBIkcKDlF1ZXJ5Q29uZGl0aW9uEg8KB2luY2x1ZGUYASABKAgSJAoFdmFsdWUYAiABKAsyFS5jYW1pbm8uUXVlcnlBcmd1bWVudCK5AwoOUXVlcnlTZWxlY3Rpb24SDAoEbmFtZRgBIAEoCRILCgNrZXkYAiABKAkSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAMgASgJEhEKCW1lbWJlcl9pZBgEIAEoCRIkChx0YXJnZXRfaW50ZXJmYWNlX3JldmlzaW9uX2lkGAUgASgJEioKCmNvbmRpdGlvbnMYBiADKAsyFi5jYW1pbm8uUXVlcnlDb25kaXRpb24SOAoJYXJndW1lbnRzGAcgAygLMiUuY2FtaW5vLlF1ZXJ5U2VsZWN0aW9uLkFyZ3VtZW50c0VudHJ5EikKCXNlbGVjdGlvbhgIIAMoCzIWLmNhbWluby5RdWVyeVNlbGVjdGlvbhIvCgpyZWxhdGlvbmFsGAkgASgLMhsuY2FtaW5vLlF1ZXJ5UmVsYXRpb25hbFBsYW4SKQoJcHJlZGljYXRlGAogASgLMhYuY2FtaW5vLlF1ZXJ5UHJlZGljYXRlGkcKDkFyZ3VtZW50c0VudHJ5EgsKA2tleRgBIAEoCRIkCgV2YWx1ZRgCIAEoCzIVLmNhbWluby5RdWVyeUFyZ3VtZW50OgI4ASJqCg1RdWVyeVBhdGhTdGVwEhAKBnNvdXJjZRgBIAEoCEgAEhAKBnRhcmdldBgCIAEoCEgAEi0KCHJlbGF0aW9uGAMgASgLMhkuY2FtaW5vLlF1ZXJ5UmVsYXRpb25QYXRoSABCBgoEc3RlcCJ9ChFRdWVyeVJlbGF0aW9uUGF0aBIdChVpbnRlcmZhY2VfcmV2aXNpb25faWQYASABKAkSEQoJbWVtYmVyX2lkGAIgASgJEiQKHHRhcmdldF9pbnRlcmZhY2VfcmV2aXNpb25faWQYAyABKAkSEAoIb3B0aW9uYWwYBCABKAgiRQoRUXVlcnlGaWVsZE9wZXJhbmQSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAEgASgJEhEKCW1lbWJlcl9pZBgCIAEoCSK2AQoPUXVlcnlFeHByZXNzaW9uEiMKBHBhdGgYASADKAsyFS5jYW1pbm8uUXVlcnlQYXRoU3RlcBIqCgVmaWVsZBgCIAEoCzIZLmNhbWluby5RdWVyeUZpZWxkT3BlcmFuZEgAEg0KA3JlZhgDIAEoCUgAEg8KBWVudHJ5GAQgASgISAASEQoHbWFwX2tleRgFIAEoCEgAEhcKD3ZhbHVlX3R5cGVfanNvbhgGIAEoCUIGCgRsZWFmIoUBCg5RdWVyeVJlZHVjdGlvbhIwCghvcGVyYXRvchgBIAEoDjIeLmNhbWluby5RdWVyeUFnZ3JlZ2F0ZU9wZXJhdG9yEigKB29wZXJhbmQYAiABKAsyFy5jYW1pbm8uUXVlcnlFeHByZXNzaW9uEhcKD3ZhbHVlX3R5cGVfanNvbhgDIAEoCSJ1CgxRdWVyeU9wZXJhbmQSLQoKZXhwcmVzc2lvbhgBIAEoCzIXLmNhbWluby5RdWVyeUV4cHJlc3Npb25IABIrCglyZWR1Y3Rpb24YAiABKAsyFi5jYW1pbm8uUXVlcnlSZWR1Y3Rpb25IAEIJCgdvcGVyYW5kIpEBCg9RdWVyeUNvbXBhcmlzb24SJQoHb3BlcmFuZBgBIAEoCzIULmNhbWluby5RdWVyeU9wZXJhbmQSMQoIb3BlcmF0b3IYAiABKA4yHy5jYW1pbm8uUXVlcnlDb21wYXJpc29uT3BlcmF0b3ISJAoFdmFsdWUYAyABKAsyFS5jYW1pbm8uUXVlcnlBcmd1bWVudCI+ChJRdWVyeVByZWRpY2F0ZUxpc3QSKAoIY2hpbGRyZW4YASADKAsyFi5jYW1pbm8uUXVlcnlQcmVkaWNhdGUi9QEKFlF1ZXJ5UmVsYXRpb25QcmVkaWNhdGUSIwoEcGF0aBgBIAMoCzIVLmNhbWluby5RdWVyeVBhdGhTdGVwEisKCHJlbGF0aW9uGAIgASgLMhkuY2FtaW5vLlF1ZXJ5UmVsYXRpb25QYXRoEjgKCG9wZXJhdG9yGAMgASgOMiYuY2FtaW5vLlF1ZXJ5UmVsYXRpb25QcmVkaWNhdGVPcGVyYXRvchIpCglwcmVkaWNhdGUYBCABKAsyFi5jYW1pbm8uUXVlcnlQcmVkaWNhdGUSJAoFdmFsdWUYBSABKAsyFS5jYW1pbm8uUXVlcnlBcmd1bWVudCK6AQoXUXVlcnlSZWR1Y3Rpb25QcmVkaWNhdGUSIwoEcGF0aBgBIAMoCzIVLmNhbWluby5RdWVyeVBhdGhTdGVwEisKCHJlbGF0aW9uGAIgASgLMhkuY2FtaW5vLlF1ZXJ5UmVsYXRpb25QYXRoEiUKBXdoZXJlGAMgASgLMhYuY2FtaW5vLlF1ZXJ5UHJlZGljYXRlEiYKBmhhdmluZxgEIAEoCzIWLmNhbWluby5RdWVyeVByZWRpY2F0ZSKsAgoOUXVlcnlQcmVkaWNhdGUSKQoDYW5kGAEgASgLMhouY2FtaW5vLlF1ZXJ5UHJlZGljYXRlTGlzdEgAEigKAm9yGAIgASgLMhouY2FtaW5vLlF1ZXJ5UHJlZGljYXRlTGlzdEgAEiUKA25vdBgDIAEoCzIWLmNhbWluby5RdWVyeVByZWRpY2F0ZUgAEioKB2NvbXBhcmUYBCABKAsyFy5jYW1pbm8uUXVlcnlDb21wYXJpc29uSAASMgoIcmVsYXRpb24YBSABKAsyHi5jYW1pbm8uUXVlcnlSZWxhdGlvblByZWRpY2F0ZUgAEjEKBnJlZHVjZRgGIAEoCzIfLmNhbWluby5RdWVyeVJlZHVjdGlvblByZWRpY2F0ZUgAQgsKCXByZWRpY2F0ZSJhCghRdWVyeVJvdxIoCgZvYmplY3QYASABKAsyFi5jYW1pbm8uUXVlcnlPYmplY3RSb3dIABIkCgRwYWlyGAIgASgLMhQuY2FtaW5vLlF1ZXJ5UGFpclJvd0gAQgUKA3JvdyJVCg5RdWVyeU9iamVjdFJvdxIdChVpbnRlcmZhY2VfcmV2aXNpb25faWQYASABKAkSEgoKbWVtYmVyc2hpcBgCIAEoCBIQCghrZXlfdHlwZRgDIAEoCSJSCgxRdWVyeVBhaXJSb3cSIAoGc291cmNlGAEgASgLMhAuY2FtaW5vLlF1ZXJ5Um93EiAKBnRhcmdldBgCIAEoCzIQLmNhbWluby5RdWVyeVJvdyJFCghRdWVyeUtleRIMCgRwYXRoGAEgAygJEisKCmV4cHJlc3Npb24YAiABKAsyFy5jYW1pbm8uUXVlcnlFeHByZXNzaW9uIi8KDVF1ZXJ5RGlzdGluY3QSHgoEa2V5cxgBIAMoCzIQLmNhbWluby5RdWVyeUtleSJiCg5RdWVyeUV4cGFuc2lvbhIjCgRwYXRoGAEgAygLMhUuY2FtaW5vLlF1ZXJ5UGF0aFN0ZXASKwoIcmVsYXRpb24YAiABKAsyGS5jYW1pbm8uUXVlcnlSZWxhdGlvblBhdGgimAIKFFF1ZXJ5UmVsYXRpb25hbFN0YWdlEgoKAmlkGAEgASgNEhIKBWlucHV0GAIgASgNSAGIAQESHQoDcm93GAMgASgLMhAuY2FtaW5vLlF1ZXJ5Um93EisKBnNvdXJjZRgEIAEoCzIZLmNhbWluby5RdWVyeVJlbGF0aW9uUGF0aEgAEigKBmZpbHRlchgFIAEoCzIWLmNhbWluby5RdWVyeVByZWRpY2F0ZUgAEigKBmV4cGFuZBgGIAEoCzIWLmNhbWluby5RdWVyeUV4cGFuc2lvbkgAEikKCGRpc3RpbmN0GAcgASgLMhUuY2FtaW5vLlF1ZXJ5RGlzdGluY3RIAEILCglvcGVyYXRpb25CCAoGX2lucHV0IlMKGFF1ZXJ5UmVkdWN0aW9uUHJvamVjdGlvbhIMCgRwYXRoGAEgAygJEikKCXJlZHVjdGlvbhgCIAEoCzIWLmNhbWluby5RdWVyeVJlZHVjdGlvbiJQChNRdWVyeUFnZ3JlZ2F0ZU9yZGVyEiUKB29wZXJhbmQYASABKAsyFC5jYW1pbm8uUXVlcnlPcGVyYW5kEhIKCmRlc2NlbmRpbmcYAiABKAgi0gMKF1F1ZXJ5UmVsYXRpb25hbFRlcm1pbmFsEg0KBXN0YWdlGAEgASgNEgwKBHBhdGgYAiADKAkSDgoGZ3JvdXBzGAMgASgIEh4KBGtleXMYBCADKAsyEC5jYW1pbm8uUXVlcnlLZXkSNAoKcmVkdWN0aW9ucxgFIAMoCzIgLmNhbWluby5RdWVyeVJlZHVjdGlvblByb2plY3Rpb24SJQoFd2hlcmUYBiABKAsyFi5jYW1pbm8uUXVlcnlQcmVkaWNhdGUSJgoGaGF2aW5nGAcgASgLMhYuY2FtaW5vLlF1ZXJ5UHJlZGljYXRlEioKBW9yZGVyGAggAygLMhsuY2FtaW5vLlF1ZXJ5QWdncmVnYXRlT3JkZXISJAoFZmlyc3QYCSABKAsyFS5jYW1pbm8uUXVlcnlBcmd1bWVudBIiCgNhbGwYCiABKAsyFS5jYW1pbm8uUXVlcnlBcmd1bWVudBIkCgVhZnRlchgLIAEoCzIVLmNhbWluby5RdWVyeUFyZ3VtZW50EikKCXNlbGVjdGlvbhgMIAMoCzIWLmNhbWluby5RdWVyeVNlbGVjdGlvbhIQCghyZXNpZHVhbBgNIAEoCBIMCgRyb3dzGA4gASgIIncKE1F1ZXJ5UmVsYXRpb25hbFBsYW4SLAoGc3RhZ2VzGAEgAygLMhwuY2FtaW5vLlF1ZXJ5UmVsYXRpb25hbFN0YWdlEjIKCXRlcm1pbmFscxgCIAMoCzIfLmNhbWluby5RdWVyeVJlbGF0aW9uYWxUZXJtaW5hbCLpAgoQUXVlcnlSZWFkQmluZGluZxIPCgdhdG9tX2lkGAEgASgJEh0KFWludGVyZmFjZV9yZXZpc2lvbl9pZBgCIAEoCRIRCgltZW1iZXJfaWQYAyABKAkSGwoTZ2V0dGVyX29wZXJhdGlvbl9pZBgEIAEoCRIPCgdzbG90X2lkGAUgASgJEhQKDGVkZ2VfdHlwZV9pZBgGIAEoCRIVCg1wcm9qZWN0aW9uX2lkGAcgASgJEgsKA3JwYxgIIAEoCBIgChh3YXRjaF9zdGFydF9vcGVyYXRpb25faWQYCSABKAkSHwoXd2F0Y2hfc3RvcF9vcGVyYXRpb25faWQYCiABKAkSEgoKZmllbGRfbmFtZRgLIAEoCRIXCg92YWx1ZV90eXBlX2pzb24YDCABKAkSKAoLY2FyZGluYWxpdHkYDSABKA4yEy5jYW1pbm8uQ2FyZGluYWxpdHkSEAoIa2V5X3R5cGUYDiABKAkikgEKDFF1ZXJ5QnVkZ2V0cxIMCgRyb3dzGAEgASgNEg0KBWRlcHRoGAIgASgNEhQKDHJlc3VsdF9ieXRlcxgDIAEoDRISCgpjYW5kaWRhdGVzGAQgASgNEhEKCXJwY19jYWxscxgFIAEoDRITCgtjb25jdXJyZW5jeRgGIAEoDRITCgtkZWFkbGluZV9tcxgHIAEoDSKNBAoOSW5zdGFsbGVkUXVlcnkSCgoCaWQYASABKAkSGQoRZGVmaW5pdGlvbl9kaWdlc3QYAiABKAkSFgoOYmluZGluZ19kaWdlc3QYAyABKAkSIgoacm9vdF9pbnRlcmZhY2VfcmV2aXNpb25faWQYBCABKAkSKQoJc2VsZWN0aW9uGAUgAygLMhYuY2FtaW5vLlF1ZXJ5U2VsZWN0aW9uEioKCGJpbmRpbmdzGAYgAygLMhguY2FtaW5vLlF1ZXJ5UmVhZEJpbmRpbmcSJQoHYnVkZ2V0cxgHIAEoCzIULmNhbWluby5RdWVyeUJ1ZGdldHMSGwoTdmFyaWFibGVzX3R5cGVfanNvbhgIIAEoCRIYChBvdXRwdXRfdHlwZV9qc29uGAkgASgJEkcKEXZhcmlhYmxlX2RlZmF1bHRzGAogAygLMiwuY2FtaW5vLkluc3RhbGxlZFF1ZXJ5LlZhcmlhYmxlRGVmYXVsdHNFbnRyeRINCgV3YXRjaBgLIAEoCBIbChNwb2xsaW5nX2ludGVydmFsX21zGAwgASgNEh4KFnJwY19wcmVkaWNhdGVfb3Jfb3JkZXIYDSABKAgaTgoVVmFyaWFibGVEZWZhdWx0c0VudHJ5EgsKA2tleRgBIAEoCRIkCgV2YWx1ZRgCIAEoCzIVLmNhbWluby5RdWVyeUFyZ3VtZW50OgI4ASpoCgtDYXJkaW5hbGl0eRIbChdDQVJESU5BTElUWV9VTlNQRUNJRklFRBAAEhAKDE9QVElPTkFMX09ORRABEg8KC0VYQUNUTFlfT05FEAISCAoETUFOWRADEg8KC01BTllfVU5JUVVFEAQqmgIKFlF1ZXJ5QWdncmVnYXRlT3BlcmF0b3ISKAokUVVFUllfQUdHUkVHQVRFX09QRVJBVE9SX1VOU1BFQ0lGSUVEEAASIgoeUVVFUllfQUdHUkVHQVRFX09QRVJBVE9SX0NPVU5UEAESKgomUVVFUllfQUdHUkVHQVRFX09QRVJBVE9SX0NPVU5UX1BSRVNFTlQQAhIgChxRVUVSWV9BR0dSRUdBVEVfT1BFUkFUT1JfU1VNEAMSIAocUVVFUllfQUdHUkVHQVRFX09QRVJBVE9SX0FWRxAEEiAKHFFVRVJZX0FHR1JFR0FURV9PUEVSQVRPUl9NSU4QBRIgChxRVUVSWV9BR0dSRUdBVEVfT1BFUkFUT1JfTUFYEAYquQIKF1F1ZXJ5Q29tcGFyaXNvbk9wZXJhdG9yEikKJVFVRVJZX0NPTVBBUklTT05fT1BFUkFUT1JfVU5TUEVDSUZJRUQQABIgChxRVUVSWV9DT01QQVJJU09OX09QRVJBVE9SX0VREAESIAocUVVFUllfQ09NUEFSSVNPTl9PUEVSQVRPUl9JThACEiUKIVFVRVJZX0NPTVBBUklTT05fT1BFUkFUT1JfSVNfTlVMTBADEiAKHFFVRVJZX0NPTVBBUklTT05fT1BFUkFUT1JfTFQQBBIhCh1RVUVSWV9DT01QQVJJU09OX09QRVJBVE9SX0xURRAFEiAKHFFVRVJZX0NPTVBBUklTT05fT1BFUkFUT1JfR1QQBhIhCh1RVUVSWV9DT01QQVJJU09OX09QRVJBVE9SX0dURRAHKoQCCh5RdWVyeVJlbGF0aW9uUHJlZGljYXRlT3BlcmF0b3ISMQotUVVFUllfUkVMQVRJT05fUFJFRElDQVRFX09QRVJBVE9SX1VOU1BFQ0lGSUVEEAASKgomUVVFUllfUkVMQVRJT05fUFJFRElDQVRFX09QRVJBVE9SX1NPTUUQARIqCiZRVUVSWV9SRUxBVElPTl9QUkVESUNBVEVfT1BFUkFUT1JfTk9ORRACEigKJFFVRVJZX1JFTEFUSU9OX1BSRURJQ0FURV9PUEVSQVRPUl9JUxADEi0KKVFVRVJZX1JFTEFUSU9OX1BSRURJQ0FURV9PUEVSQVRPUl9JU19OVUxMEARiBnByb3RvMw"); /** * @generated from message camino.AtomDefinition @@ -430,6 +430,16 @@ export type QuerySelection = Message<"camino.QuerySelection"> & { * @generated from field: repeated camino.QuerySelection selection = 8; */ selection: QuerySelection[]; + + /** + * @generated from field: camino.QueryRelationalPlan relational = 9; + */ + relational?: QueryRelationalPlan | undefined; + + /** + * @generated from field: camino.QueryPredicate predicate = 10; + */ + predicate?: QueryPredicate | undefined; }; /** @@ -439,6 +449,716 @@ export type QuerySelection = Message<"camino.QuerySelection"> & { export const QuerySelectionSchema: GenMessage = /*@__PURE__*/ messageDesc(file_camino_schema, 11); +/** + * Resolved IDs, not GraphQL names, determine execution. Response selections + * remain separate so aliases and fragments cannot change relational semantics. + * + * @generated from message camino.QueryPathStep + */ +export type QueryPathStep = Message<"camino.QueryPathStep"> & { + /** + * @generated from oneof camino.QueryPathStep.step + */ + step: { + /** + * @generated from field: bool source = 1; + */ + value: boolean; + case: "source"; + } | { + /** + * @generated from field: bool target = 2; + */ + value: boolean; + case: "target"; + } | { + /** + * @generated from field: camino.QueryRelationPath relation = 3; + */ + value: QueryRelationPath; + case: "relation"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message camino.QueryPathStep. + * Use `create(QueryPathStepSchema)` to create a new message. + */ +export const QueryPathStepSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_camino_schema, 12); + +/** + * @generated from message camino.QueryRelationPath + */ +export type QueryRelationPath = Message<"camino.QueryRelationPath"> & { + /** + * @generated from field: string interface_revision_id = 1; + */ + interfaceRevisionId: string; + + /** + * @generated from field: string member_id = 2; + */ + memberId: string; + + /** + * @generated from field: string target_interface_revision_id = 3; + */ + targetInterfaceRevisionId: string; + + /** + * @generated from field: bool optional = 4; + */ + optional: boolean; +}; + +/** + * Describes the message camino.QueryRelationPath. + * Use `create(QueryRelationPathSchema)` to create a new message. + */ +export const QueryRelationPathSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_camino_schema, 13); + +/** + * @generated from message camino.QueryFieldOperand + */ +export type QueryFieldOperand = Message<"camino.QueryFieldOperand"> & { + /** + * @generated from field: string interface_revision_id = 1; + */ + interfaceRevisionId: string; + + /** + * @generated from field: string member_id = 2; + */ + memberId: string; +}; + +/** + * Describes the message camino.QueryFieldOperand. + * Use `create(QueryFieldOperandSchema)` to create a new message. + */ +export const QueryFieldOperandSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_camino_schema, 14); + +/** + * @generated from message camino.QueryExpression + */ +export type QueryExpression = Message<"camino.QueryExpression"> & { + /** + * @generated from field: repeated camino.QueryPathStep path = 1; + */ + path: QueryPathStep[]; + + /** + * @generated from oneof camino.QueryExpression.leaf + */ + leaf: { + /** + * @generated from field: camino.QueryFieldOperand field = 2; + */ + value: QueryFieldOperand; + case: "field"; + } | { + /** + * @generated from field: string ref = 3; + */ + value: string; + case: "ref"; + } | { + /** + * @generated from field: bool entry = 4; + */ + value: boolean; + case: "entry"; + } | { + /** + * @generated from field: bool map_key = 5; + */ + value: boolean; + case: "mapKey"; + } | { case: undefined; value?: undefined }; + + /** + * @generated from field: string value_type_json = 6; + */ + valueTypeJson: string; +}; + +/** + * Describes the message camino.QueryExpression. + * Use `create(QueryExpressionSchema)` to create a new message. + */ +export const QueryExpressionSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_camino_schema, 15); + +/** + * @generated from message camino.QueryReduction + */ +export type QueryReduction = Message<"camino.QueryReduction"> & { + /** + * @generated from field: camino.QueryAggregateOperator operator = 1; + */ + operator: QueryAggregateOperator; + + /** + * @generated from field: camino.QueryExpression operand = 2; + */ + operand?: QueryExpression | undefined; + + /** + * @generated from field: string value_type_json = 3; + */ + valueTypeJson: string; +}; + +/** + * Describes the message camino.QueryReduction. + * Use `create(QueryReductionSchema)` to create a new message. + */ +export const QueryReductionSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_camino_schema, 16); + +/** + * @generated from message camino.QueryOperand + */ +export type QueryOperand = Message<"camino.QueryOperand"> & { + /** + * @generated from oneof camino.QueryOperand.operand + */ + operand: { + /** + * @generated from field: camino.QueryExpression expression = 1; + */ + value: QueryExpression; + case: "expression"; + } | { + /** + * @generated from field: camino.QueryReduction reduction = 2; + */ + value: QueryReduction; + case: "reduction"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message camino.QueryOperand. + * Use `create(QueryOperandSchema)` to create a new message. + */ +export const QueryOperandSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_camino_schema, 17); + +/** + * @generated from message camino.QueryComparison + */ +export type QueryComparison = Message<"camino.QueryComparison"> & { + /** + * @generated from field: camino.QueryOperand operand = 1; + */ + operand?: QueryOperand | undefined; + + /** + * @generated from field: camino.QueryComparisonOperator operator = 2; + */ + operator: QueryComparisonOperator; + + /** + * @generated from field: camino.QueryArgument value = 3; + */ + value?: QueryArgument | undefined; +}; + +/** + * Describes the message camino.QueryComparison. + * Use `create(QueryComparisonSchema)` to create a new message. + */ +export const QueryComparisonSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_camino_schema, 18); + +/** + * @generated from message camino.QueryPredicateList + */ +export type QueryPredicateList = Message<"camino.QueryPredicateList"> & { + /** + * @generated from field: repeated camino.QueryPredicate children = 1; + */ + children: QueryPredicate[]; +}; + +/** + * Describes the message camino.QueryPredicateList. + * Use `create(QueryPredicateListSchema)` to create a new message. + */ +export const QueryPredicateListSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_camino_schema, 19); + +/** + * @generated from message camino.QueryRelationPredicate + */ +export type QueryRelationPredicate = Message<"camino.QueryRelationPredicate"> & { + /** + * @generated from field: repeated camino.QueryPathStep path = 1; + */ + path: QueryPathStep[]; + + /** + * @generated from field: camino.QueryRelationPath relation = 2; + */ + relation?: QueryRelationPath | undefined; + + /** + * @generated from field: camino.QueryRelationPredicateOperator operator = 3; + */ + operator: QueryRelationPredicateOperator; + + /** + * @generated from field: camino.QueryPredicate predicate = 4; + */ + predicate?: QueryPredicate | undefined; + + /** + * @generated from field: camino.QueryArgument value = 5; + */ + value?: QueryArgument | undefined; +}; + +/** + * Describes the message camino.QueryRelationPredicate. + * Use `create(QueryRelationPredicateSchema)` to create a new message. + */ +export const QueryRelationPredicateSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_camino_schema, 20); + +/** + * @generated from message camino.QueryReductionPredicate + */ +export type QueryReductionPredicate = Message<"camino.QueryReductionPredicate"> & { + /** + * @generated from field: repeated camino.QueryPathStep path = 1; + */ + path: QueryPathStep[]; + + /** + * @generated from field: camino.QueryRelationPath relation = 2; + */ + relation?: QueryRelationPath | undefined; + + /** + * @generated from field: camino.QueryPredicate where = 3; + */ + where?: QueryPredicate | undefined; + + /** + * @generated from field: camino.QueryPredicate having = 4; + */ + having?: QueryPredicate | undefined; +}; + +/** + * Describes the message camino.QueryReductionPredicate. + * Use `create(QueryReductionPredicateSchema)` to create a new message. + */ +export const QueryReductionPredicateSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_camino_schema, 21); + +/** + * @generated from message camino.QueryPredicate + */ +export type QueryPredicate = Message<"camino.QueryPredicate"> & { + /** + * @generated from oneof camino.QueryPredicate.predicate + */ + predicate: { + /** + * @generated from field: camino.QueryPredicateList and = 1; + */ + value: QueryPredicateList; + case: "and"; + } | { + /** + * @generated from field: camino.QueryPredicateList or = 2; + */ + value: QueryPredicateList; + case: "or"; + } | { + /** + * @generated from field: camino.QueryPredicate not = 3; + */ + value: QueryPredicate; + case: "not"; + } | { + /** + * @generated from field: camino.QueryComparison compare = 4; + */ + value: QueryComparison; + case: "compare"; + } | { + /** + * @generated from field: camino.QueryRelationPredicate relation = 5; + */ + value: QueryRelationPredicate; + case: "relation"; + } | { + /** + * @generated from field: camino.QueryReductionPredicate reduce = 6; + */ + value: QueryReductionPredicate; + case: "reduce"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message camino.QueryPredicate. + * Use `create(QueryPredicateSchema)` to create a new message. + */ +export const QueryPredicateSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_camino_schema, 22); + +/** + * @generated from message camino.QueryRow + */ +export type QueryRow = Message<"camino.QueryRow"> & { + /** + * @generated from oneof camino.QueryRow.row + */ + row: { + /** + * @generated from field: camino.QueryObjectRow object = 1; + */ + value: QueryObjectRow; + case: "object"; + } | { + /** + * @generated from field: camino.QueryPairRow pair = 2; + */ + value: QueryPairRow; + case: "pair"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message camino.QueryRow. + * Use `create(QueryRowSchema)` to create a new message. + */ +export const QueryRowSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_camino_schema, 23); + +/** + * @generated from message camino.QueryObjectRow + */ +export type QueryObjectRow = Message<"camino.QueryObjectRow"> & { + /** + * @generated from field: string interface_revision_id = 1; + */ + interfaceRevisionId: string; + + /** + * @generated from field: bool membership = 2; + */ + membership: boolean; + + /** + * @generated from field: string key_type = 3; + */ + keyType: string; +}; + +/** + * Describes the message camino.QueryObjectRow. + * Use `create(QueryObjectRowSchema)` to create a new message. + */ +export const QueryObjectRowSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_camino_schema, 24); + +/** + * @generated from message camino.QueryPairRow + */ +export type QueryPairRow = Message<"camino.QueryPairRow"> & { + /** + * @generated from field: camino.QueryRow source = 1; + */ + source?: QueryRow | undefined; + + /** + * @generated from field: camino.QueryRow target = 2; + */ + target?: QueryRow | undefined; +}; + +/** + * Describes the message camino.QueryPairRow. + * Use `create(QueryPairRowSchema)` to create a new message. + */ +export const QueryPairRowSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_camino_schema, 25); + +/** + * @generated from message camino.QueryKey + */ +export type QueryKey = Message<"camino.QueryKey"> & { + /** + * @generated from field: repeated string path = 1; + */ + path: string[]; + + /** + * @generated from field: camino.QueryExpression expression = 2; + */ + expression?: QueryExpression | undefined; +}; + +/** + * Describes the message camino.QueryKey. + * Use `create(QueryKeySchema)` to create a new message. + */ +export const QueryKeySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_camino_schema, 26); + +/** + * @generated from message camino.QueryDistinct + */ +export type QueryDistinct = Message<"camino.QueryDistinct"> & { + /** + * @generated from field: repeated camino.QueryKey keys = 1; + */ + keys: QueryKey[]; +}; + +/** + * Describes the message camino.QueryDistinct. + * Use `create(QueryDistinctSchema)` to create a new message. + */ +export const QueryDistinctSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_camino_schema, 27); + +/** + * @generated from message camino.QueryExpansion + */ +export type QueryExpansion = Message<"camino.QueryExpansion"> & { + /** + * @generated from field: repeated camino.QueryPathStep path = 1; + */ + path: QueryPathStep[]; + + /** + * @generated from field: camino.QueryRelationPath relation = 2; + */ + relation?: QueryRelationPath | undefined; +}; + +/** + * Describes the message camino.QueryExpansion. + * Use `create(QueryExpansionSchema)` to create a new message. + */ +export const QueryExpansionSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_camino_schema, 28); + +/** + * @generated from message camino.QueryRelationalStage + */ +export type QueryRelationalStage = Message<"camino.QueryRelationalStage"> & { + /** + * @generated from field: uint32 id = 1; + */ + id: number; + + /** + * @generated from field: optional uint32 input = 2; + */ + input?: number | undefined; + + /** + * @generated from field: camino.QueryRow row = 3; + */ + row?: QueryRow | undefined; + + /** + * @generated from oneof camino.QueryRelationalStage.operation + */ + operation: { + /** + * @generated from field: camino.QueryRelationPath source = 4; + */ + value: QueryRelationPath; + case: "source"; + } | { + /** + * @generated from field: camino.QueryPredicate filter = 5; + */ + value: QueryPredicate; + case: "filter"; + } | { + /** + * @generated from field: camino.QueryExpansion expand = 6; + */ + value: QueryExpansion; + case: "expand"; + } | { + /** + * @generated from field: camino.QueryDistinct distinct = 7; + */ + value: QueryDistinct; + case: "distinct"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message camino.QueryRelationalStage. + * Use `create(QueryRelationalStageSchema)` to create a new message. + */ +export const QueryRelationalStageSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_camino_schema, 29); + +/** + * @generated from message camino.QueryReductionProjection + */ +export type QueryReductionProjection = Message<"camino.QueryReductionProjection"> & { + /** + * @generated from field: repeated string path = 1; + */ + path: string[]; + + /** + * @generated from field: camino.QueryReduction reduction = 2; + */ + reduction?: QueryReduction | undefined; +}; + +/** + * Describes the message camino.QueryReductionProjection. + * Use `create(QueryReductionProjectionSchema)` to create a new message. + */ +export const QueryReductionProjectionSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_camino_schema, 30); + +/** + * @generated from message camino.QueryAggregateOrder + */ +export type QueryAggregateOrder = Message<"camino.QueryAggregateOrder"> & { + /** + * @generated from field: camino.QueryOperand operand = 1; + */ + operand?: QueryOperand | undefined; + + /** + * @generated from field: bool descending = 2; + */ + descending: boolean; +}; + +/** + * Describes the message camino.QueryAggregateOrder. + * Use `create(QueryAggregateOrderSchema)` to create a new message. + */ +export const QueryAggregateOrderSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_camino_schema, 31); + +/** + * @generated from message camino.QueryRelationalTerminal + */ +export type QueryRelationalTerminal = Message<"camino.QueryRelationalTerminal"> & { + /** + * @generated from field: uint32 stage = 1; + */ + stage: number; + + /** + * @generated from field: repeated string path = 2; + */ + path: string[]; + + /** + * @generated from field: bool groups = 3; + */ + groups: boolean; + + /** + * @generated from field: repeated camino.QueryKey keys = 4; + */ + keys: QueryKey[]; + + /** + * @generated from field: repeated camino.QueryReductionProjection reductions = 5; + */ + reductions: QueryReductionProjection[]; + + /** + * @generated from field: camino.QueryPredicate where = 6; + */ + where?: QueryPredicate | undefined; + + /** + * @generated from field: camino.QueryPredicate having = 7; + */ + having?: QueryPredicate | undefined; + + /** + * @generated from field: repeated camino.QueryAggregateOrder order = 8; + */ + order: QueryAggregateOrder[]; + + /** + * @generated from field: camino.QueryArgument first = 9; + */ + first?: QueryArgument | undefined; + + /** + * @generated from field: camino.QueryArgument all = 10; + */ + all?: QueryArgument | undefined; + + /** + * @generated from field: camino.QueryArgument after = 11; + */ + after?: QueryArgument | undefined; + + /** + * @generated from field: repeated camino.QuerySelection selection = 12; + */ + selection: QuerySelection[]; + + /** + * @generated from field: bool residual = 13; + */ + residual: boolean; + + /** + * Internal ordinary-relationship residual: yields ordered membership IDs. + * + * @generated from field: bool rows = 14; + */ + rows: boolean; +}; + +/** + * Describes the message camino.QueryRelationalTerminal. + * Use `create(QueryRelationalTerminalSchema)` to create a new message. + */ +export const QueryRelationalTerminalSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_camino_schema, 32); + +/** + * @generated from message camino.QueryRelationalPlan + */ +export type QueryRelationalPlan = Message<"camino.QueryRelationalPlan"> & { + /** + * @generated from field: repeated camino.QueryRelationalStage stages = 1; + */ + stages: QueryRelationalStage[]; + + /** + * @generated from field: repeated camino.QueryRelationalTerminal terminals = 2; + */ + terminals: QueryRelationalTerminal[]; +}; + +/** + * Describes the message camino.QueryRelationalPlan. + * Use `create(QueryRelationalPlanSchema)` to create a new message. + */ +export const QueryRelationalPlanSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_camino_schema, 33); + /** * @generated from message camino.QueryReadBinding */ @@ -519,7 +1239,7 @@ export type QueryReadBinding = Message<"camino.QueryReadBinding"> & { * Use `create(QueryReadBindingSchema)` to create a new message. */ export const QueryReadBindingSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_camino_schema, 12); + messageDesc(file_camino_schema, 34); /** * @generated from message camino.QueryBudgets @@ -566,7 +1286,7 @@ export type QueryBudgets = Message<"camino.QueryBudgets"> & { * Use `create(QueryBudgetsSchema)` to create a new message. */ export const QueryBudgetsSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_camino_schema, 13); + messageDesc(file_camino_schema, 35); /** * @generated from message camino.InstalledQuery @@ -643,7 +1363,7 @@ export type InstalledQuery = Message<"camino.InstalledQuery"> & { * Use `create(InstalledQuerySchema)` to create a new message. */ export const InstalledQuerySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_camino_schema, 14); + messageDesc(file_camino_schema, 36); /** * @generated from enum camino.Cardinality @@ -681,3 +1401,136 @@ export enum Cardinality { export const CardinalitySchema: GenEnum = /*@__PURE__*/ enumDesc(file_camino_schema, 0); +/** + * @generated from enum camino.QueryAggregateOperator + */ +export enum QueryAggregateOperator { + /** + * @generated from enum value: QUERY_AGGREGATE_OPERATOR_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: QUERY_AGGREGATE_OPERATOR_COUNT = 1; + */ + COUNT = 1, + + /** + * @generated from enum value: QUERY_AGGREGATE_OPERATOR_COUNT_PRESENT = 2; + */ + COUNT_PRESENT = 2, + + /** + * @generated from enum value: QUERY_AGGREGATE_OPERATOR_SUM = 3; + */ + SUM = 3, + + /** + * @generated from enum value: QUERY_AGGREGATE_OPERATOR_AVG = 4; + */ + AVG = 4, + + /** + * @generated from enum value: QUERY_AGGREGATE_OPERATOR_MIN = 5; + */ + MIN = 5, + + /** + * @generated from enum value: QUERY_AGGREGATE_OPERATOR_MAX = 6; + */ + MAX = 6, +} + +/** + * Describes the enum camino.QueryAggregateOperator. + */ +export const QueryAggregateOperatorSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_camino_schema, 1); + +/** + * @generated from enum camino.QueryComparisonOperator + */ +export enum QueryComparisonOperator { + /** + * @generated from enum value: QUERY_COMPARISON_OPERATOR_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: QUERY_COMPARISON_OPERATOR_EQ = 1; + */ + EQ = 1, + + /** + * @generated from enum value: QUERY_COMPARISON_OPERATOR_IN = 2; + */ + IN = 2, + + /** + * @generated from enum value: QUERY_COMPARISON_OPERATOR_IS_NULL = 3; + */ + IS_NULL = 3, + + /** + * @generated from enum value: QUERY_COMPARISON_OPERATOR_LT = 4; + */ + LT = 4, + + /** + * @generated from enum value: QUERY_COMPARISON_OPERATOR_LTE = 5; + */ + LTE = 5, + + /** + * @generated from enum value: QUERY_COMPARISON_OPERATOR_GT = 6; + */ + GT = 6, + + /** + * @generated from enum value: QUERY_COMPARISON_OPERATOR_GTE = 7; + */ + GTE = 7, +} + +/** + * Describes the enum camino.QueryComparisonOperator. + */ +export const QueryComparisonOperatorSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_camino_schema, 2); + +/** + * @generated from enum camino.QueryRelationPredicateOperator + */ +export enum QueryRelationPredicateOperator { + /** + * @generated from enum value: QUERY_RELATION_PREDICATE_OPERATOR_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: QUERY_RELATION_PREDICATE_OPERATOR_SOME = 1; + */ + SOME = 1, + + /** + * @generated from enum value: QUERY_RELATION_PREDICATE_OPERATOR_NONE = 2; + */ + NONE = 2, + + /** + * @generated from enum value: QUERY_RELATION_PREDICATE_OPERATOR_IS = 3; + */ + IS = 3, + + /** + * @generated from enum value: QUERY_RELATION_PREDICATE_OPERATOR_IS_NULL = 4; + */ + IS_NULL = 4, +} + +/** + * Describes the enum camino.QueryRelationPredicateOperator. + */ +export const QueryRelationPredicateOperatorSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_camino_schema, 3); + diff --git a/src/query/compile.ts b/src/query/compile.ts index 752c130..1c9de21 100644 --- a/src/query/compile.ts +++ b/src/query/compile.ts @@ -34,6 +34,8 @@ import { type InterfaceRevisionId, } from "../capability-model/types.js"; import { querySchema } from "./schema.js"; +import { objectRow } from "./relational-schema.js"; +import { relationalCompiler } from "./relational-compile.js"; import { QueryCompileError, type QueryDeclaration, @@ -58,26 +60,6 @@ const fail = (code: string, message: string, node?: ASTNode): never => { : undefined, ); }; -const shapeScalar = (name: string): ValueType => { - switch (name) { - case "Boolean": - return valueType.bool; - case "Int": - return valueType.int32; - case "Int64": - return valueType.int64; - case "UInt32": - return valueType.uint32; - case "UInt64": - return valueType.uint64; - case "Float": - return valueType.double; - case "Bytes": - return valueType.bytes; - default: - return valueType.string; - } -}; /** Only immutable repository sources call this. Runtime requests never compile documents. */ export async function compileQuery( @@ -104,7 +86,7 @@ export async function compileQuery( definitions.push(...document.definitions); } const document: DocumentNode = { kind: "Document" as DocumentNode["kind"], definitions }; - const generated = querySchema(declaration, interfaces); + const generated = querySchema(declaration, interfaces, document); const operations = definitions.filter((entry) => entry.kind === "OperationDefinition"); if ( operations.length !== 1 || @@ -114,6 +96,8 @@ export async function compileQuery( fail("QUERY_UNSUPPORTED_FEATURE", "Exactly one named query matching the declaration is required"); visit(document, { Field(node) { + if (node.name.value === "_unavailable") + fail("QUERY_AGGREGATE_TYPE", "No supported field exists at this operator path", node); if (node.name.value.startsWith("__")) fail("QUERY_UNSUPPORTED_FEATURE", "Introspection is not supported", node); }, Directive(node) { @@ -130,6 +114,11 @@ export async function compileQuery( fail("QUERY_VALIDATION", error.message, error.nodes?.[0]); } const effects = new Map(); + const fragments = new Map( + definitions + .filter((entry): entry is FragmentDefinitionNode => entry.kind === "FragmentDefinition") + .map((entry) => [entry.name.value, entry]), + ); const mark = (id: InterfaceRevisionId, name: string, use: QueryUse, node: ASTNode) => { const contract = generated.contracts.get(id)!; const member = contract.members.find((entry) => entry.displayName === name); @@ -167,7 +156,13 @@ export async function compileQuery( node, ); }; + const relational = relationalCompiler(generated, declaration, mark, fragments); + const compiledPredicates = new WeakMap(); const inputEffects = (id: InterfaceRevisionId, value: ValueNode, use: "predicate" | "order") => { + if (use === "predicate") { + relational.predicate(objectRow(id), value); + return; + } if (value.kind === "Variable") { // A dynamic filter may name any field in its declared input type. for (const member of generated.contracts.get(id)!.members) @@ -175,12 +170,10 @@ export async function compileQuery( } else if (value.kind === "ListValue") value.values.forEach((child) => inputEffects(id, child, use)); else if (value.kind === "ObjectValue") for (const field of value.fields) { - if (use === "predicate" && ["and", "or", "not"].includes(field.name.value)) inputEffects(id, field.value, use); - else mark(id, field.name.value, use, field); + mark(id, field.name.value, use, field); } }; const info = new TypeInfo(generated.schema); - let hasContinuation = false; visit( document, visitWithTypeInfo(info, { @@ -200,9 +193,9 @@ export async function compileQuery( if (bounds[0]!.name.value === "all" && node.arguments?.some((argument) => argument.name.value === "after")) fail("QUERY_UNSUPPORTED_FEATURE", "Bounded-all does not accept a continuation", node); for (const argument of node.arguments ?? []) { - if (argument.name.value === "where") inputEffects(target, argument.value, "predicate"); + if (argument.name.value === "where") + compiledPredicates.set(node, relational.predicate(objectRow(target), argument.value)); if (argument.name.value === "orderBy") inputEffects(target, argument.value, "order"); - if (argument.name.value === "after") hasContinuation = true; if ( ["first", "all"].includes(argument.name.value) && argument.value.kind !== "Variable" && @@ -216,6 +209,33 @@ export async function compileQuery( argument, ); } + if (node.arguments?.some((a) => a.name.value === "after")) { + const rpcOrder = (value: ValueNode): boolean => + value.kind === "Variable" + ? generated.contracts + .get(target)! + .members.some((m) => m.kind === "value" && m.queryRead?.execution === "rpc-permitted") + : value.kind === "ListValue" + ? value.values.some(rpcOrder) + : value.kind === "ObjectValue" && + value.fields.some((f) => + generated.contracts + .get(target)! + .members.some( + (m) => + m.displayName === f.name.value && + m.kind === "value" && + m.queryRead?.execution === "rpc-permitted", + ), + ); + const order = node.arguments.find((a) => a.name.value === "orderBy"); + if (relational.needsRpc(compiledPredicates.get(node)) || (order && rpcOrder(order.value))) + fail( + "QUERY_UNSUPPORTED_FEATURE", + "RPC predicates/order require a bounded first/all window without continuation", + node, + ); + } }, FragmentSpread(node) { const fragment = definitions.find( @@ -226,18 +246,6 @@ export async function compileQuery( }, }), ); - if ( - hasContinuation && - [...effects.values()].some( - (effect) => effect.execution === "rpc-permitted" && effect.uses.some((use) => use !== "select"), - ) - ) - fail("QUERY_UNSUPPORTED_FEATURE", "RPC predicates/order support bounded first windows, not continuation"); - const fragments = new Map( - definitions - .filter((entry): entry is FragmentDefinitionNode => entry.kind === "FragmentDefinition") - .map((entry) => [entry.name.value, entry]), - ); let expandedFields = 0; // GraphQL merges repeated response keys. In particular, fragments may each // contribute different children of one relationship; last-write-wins would @@ -280,16 +288,6 @@ export async function compileQuery( depth + 1, conditional || !!selection.directives?.length, ); - if (selection.name.value === "_qx") { - const contract = generated.byName.get(type.name)!; - const metadataFields: Record = {}; - for (const metadata of selection.selectionSet?.selections ?? []) { - if (metadata.kind !== "Field" || metadata.name.value !== "ref") - fail("QUERY_UNSUPPORTED_FEATURE", "Select _qx.ref directly", metadata); - else metadataFields[metadata.alias?.value ?? "ref"] = valueType.interfaceRef(contract.revisionId); - } - fields[key] = { kind: "record", fields: metadataFields }; - } if ((conditional || selection.directives?.length) && fields[key]!.kind !== "optional") fields[key] = valueType.optional(fields[key]!); if (previous) fields[key] = mergeOutput(previous, fields[key]!); @@ -298,7 +296,10 @@ export async function compileQuery( if (selections) add(selections, conditional); return { kind: "record", fields }; } - return shapeScalar(getNamedType(type)!.name); + return ( + generated.scalarTypes.get(getNamedType(type)!.name) ?? + fail("QUERY_TYPE", `Unsupported query scalar ${getNamedType(type)!.name}`) + ); }; const variables: Record = {}; const inputShape = (type: GraphQLType, seen = new Set()): ValueType => { @@ -324,7 +325,9 @@ export async function compileQuery( ), }; } - if (isScalarType(type) || isEnumType(type)) return shapeScalar(type.name); + if (isEnumType(type)) return valueType.string; + if (isScalarType(type)) + return generated.scalarTypes.get(type.name) ?? fail("QUERY_TYPE", `Unsupported query scalar ${type.name}`); return fail("QUERY_VARIABLE_TYPE", "Unsupported variable type"); }; for (const variable of operations[0]!.variableDefinitions ?? []) @@ -370,6 +373,19 @@ export async function compileQuery( const contract = generated.byName.get(parent.name); const member = contract?.members.find((entry) => entry.displayName === node.name.value); const type = getNamedType(parent.getFields()[node.name.value]!.type); + const role = generated.relational.roles.get(parent.name); + const relationship = + role?.kind === "relations" + ? generated.relational.relations(role.row).find((m) => m.displayName === node.name.value) + : undefined; + const plan = + relationship && role?.row.kind === "object" && node.selectionSet + ? relational.plan(role.row.interfaceRevisionId, relationship, node.selectionSet, (set, name) => { + const t = generated.schema.getType(name); + if (!t || !isObjectType(t)) return fail("QUERY_TYPE", `Missing generated output type ${name}`, node); + return selection(set, t); + }) + : undefined; return [ { name: node.name.value, @@ -381,15 +397,18 @@ export async function compileQuery( (node.arguments ?? []).map((entry) => [entry.name.value, argument(entry.value)]), ), selection: node.selectionSet && isObjectType(type) ? selection(node.selectionSet, type) : [], + ...(plan ? { relational: plan } : {}), + ...(compiledPredicates.has(node) ? { predicate: compiledPredicates.get(node)! } : {}), }, ]; }); const normalized = print(document), schema = printSchema(generated.schema); + const selections = selection(operations[0]!.selectionSet, generated.schema.getQueryType()!); const definitionDigest = createHash("sha256") .update( canonicalJson({ - semantics: 1, + semantics: 2, declaration, normalized, interfaces: [...generated.byName.values()].sort((a, b) => @@ -406,7 +425,7 @@ export async function compileQuery( variables: { kind: "record", fields: variables }, output: result, effects: [...effects.values()], - selection: selection(operations[0]!.selectionSet, generated.schema.getQueryType()!), + selection: selections, variableDefaults: Object.fromEntries( (operations[0]!.variableDefinitions ?? []) .filter((entry) => entry.defaultValue) diff --git a/src/query/proto.ts b/src/query/proto.ts index f6be26b..62ced89 100644 --- a/src/query/proto.ts +++ b/src/query/proto.ts @@ -10,6 +10,7 @@ import { import type { LinkedQuery } from "./link.js"; import type { QueryArgument, QuerySelection } from "./types.js"; import type { WorkspaceRevision } from "../capability-model/types.js"; +import { relationalWire } from "./relational-proto.js"; const argument = (entry: QueryArgument): WireArgument => { switch (entry.kind) { @@ -34,8 +35,12 @@ const selection = (entry: QuerySelection): import("../gen/camino/schema_pb.js"). conditions: entry.conditions.map((condition) => ({ include: condition.include, value: argument(condition.value) })), arguments: Object.fromEntries(Object.entries(entry.arguments).map(([k, v]) => [k, argument(v)])), selection: entry.selection.map(selection), + relational: entry.relational && relationalWire(argument, selection).plan(entry.relational), + predicate: entry.predicate && relationalWire(argument, selection).predicate(entry.predicate), }); +export const querySelectionToWire = selection; + export const queryRuntimePlan = (linked: LinkedQuery, workspace: WorkspaceRevision) => toJson( InstalledQuerySchema, diff --git a/src/query/relational-compile.ts b/src/query/relational-compile.ts new file mode 100644 index 0000000..c9723bf --- /dev/null +++ b/src/query/relational-compile.ts @@ -0,0 +1,605 @@ +import type { ASTNode, FieldNode, FragmentDefinitionNode, SelectionSetNode, ValueNode } from "graphql"; +import { + valueType, + type InterfaceRevisionId, + type RelationshipInterfaceMember, + type ValueType, +} from "../capability-model/types.js"; +import { + QueryCompileError, + type QueryArgument, + type QueryDeclaration, + type QuerySelection, + type QueryUse, +} from "./types.js"; +import { + aggregateType, + queryKeyType, + type AggregateOperator, + type QueryAggregatePredicate, + type QueryEffectRecorder, + type QueryExpression, + type QueryKey, + type QueryPathStep, + type QueryPredicate, + type QueryReduction, + type QueryRelationalPlan, + type QueryRelationalTerminal, + type QueryRow, +} from "./relational.js"; +import { objectRow } from "./relational-schema.js"; + +export function queryArgument(node: ValueNode): QueryArgument { + if (node.kind === "Variable") return { kind: "variable", name: node.name.value }; + if (node.kind === "ListValue") return { kind: "list", values: node.values.map(queryArgument) }; + if (node.kind === "ObjectValue") + return { + kind: "object", + fields: Object.fromEntries(node.fields.map((f) => [f.name.value, queryArgument(f.value)])), + }; + return { kind: "literal", value: node.kind === "NullValue" ? null : node.value }; +} +type Environment = ReturnType; +type Availability = readonly QueryKey[] | undefined; +function fail(code: string, message: string, node: ASTNode): never { + const token = node.loc?.startToken; + throw new QueryCompileError( + code, + message, + token ? { file: node.loc!.source.name, line: token.line, column: token.column } : undefined, + ); +} +const pairs = (node: ValueNode): readonly import("graphql").ObjectFieldNode[] => + node.kind === "ObjectValue" + ? node.fields + : fail("QUERY_UNSUPPORTED_FEATURE", "Query structure must be a literal object", node); +const ops = new Set(["eq", "in", "isNull", "lt", "lte", "gt", "gte"]); +const prefix = (a: readonly string[], b: readonly string[]) => a.every((part, i) => part === b[i]); + +export function relationalCompiler( + env: Environment, + declaration: QueryDeclaration, + mark: QueryEffectRecorder, + fragments: Map, +) { + const rowFields = env.relational.fields; + const relationships = env.relational.relations; + const needsRpc = (value: unknown): boolean => { + if (!value || typeof value !== "object") return false; + if ("leaf" in value) { + const leaf = (value as QueryExpression).leaf; + if (leaf.kind === "field") + return ( + env.contracts + .get(leaf.interfaceRevisionId) + ?.members.some( + (m) => m.id === leaf.memberId && m.kind === "value" && m.queryRead?.execution === "rpc-permitted", + ) ?? false + ); + } + return Object.values(value).some(needsRpc); + }; + let stageCount = 0; + const fields = (set: SelectionSetNode): FieldNode[] => + set.selections.flatMap((n) => + n.kind === "Field" ? [n] : n.kind === "FragmentSpread" ? fields(fragments.get(n.name.value)!.selectionSet) : [], + ); + const requireAvailable = (path: string[], available: Availability, node: ASTNode) => { + if (!available) return; + const ok = available.some((key) => { + if (key.path.join(".") === path.join(".")) return true; + const last = key.expression.leaf.kind; + if (last !== "ref" && last !== "entry") return false; + const base = key.path.slice(0, -2); + if (!prefix(base, path)) return false; + // Object identity does not determine which incoming membership carried it. + return ( + last === "entry" || !(path[base.length] === "_qx" && ["entry", "mapKey"].includes(path[base.length + 1] ?? "")) + ); + }); + if (!ok) + fail( + "QUERY_DISTINCT_FIELD_UNAVAILABLE", + `${path.join(".")} was dropped by distinct; retain its identity or key explicitly`, + node, + ); + }; + const walk = ( + root: QueryRow, + names: string[], + uses: QueryUse[], + node: ASTNode, + ): { row: QueryRow; path: QueryPathStep[]; nullable: boolean } => { + let row = root, + nullable = false; + const path: QueryPathStep[] = []; + for (const name of names) { + if (row.kind === "pair") { + if (name !== "source" && name !== "target") + return fail("QUERY_FIELD_NOT_QUERYABLE", `Unknown row binding ${name}`, node); + path.push({ kind: name }); + row = row[name]; + continue; + } + const relation = relationships(row).find((m) => m.displayName === name); + if (!relation) return fail("QUERY_FIELD_NOT_QUERYABLE", `Expected queryable relationship ${name}`, node); + if (relation.cardinality === "many" || relation.cardinality === "many-unique") + return fail( + "QUERY_EXPANSION_REQUIRED", + `${name} is to-many; explicitly expand it, quantify it or aggregate it`, + node, + ); + for (const use of uses) mark(row.interfaceRevisionId, name, use, node); + const target = env.target(relation), + optional = relation.cardinality === "optional-one"; + path.push({ + kind: "relation", + interfaceRevisionId: row.interfaceRevisionId, + memberId: relation.id, + targetInterfaceRevisionId: target, + optional, + }); + nullable ||= optional; + row = objectRow(target); + } + if (path.length > declaration.budgets.depth) fail("QUERY_DEPTH_LIMIT", "Expression path exceeds query depth", node); + return { row, path, nullable }; + }; + const expression = ( + root: QueryRow, + names: string[], + uses: QueryUse[], + node: ASTNode, + available?: Availability, + ): QueryExpression => { + requireAvailable(names, available, node); + const meta = names.at(-2) === "_qx"; + const { row, path, nullable } = walk(root, names.slice(0, meta ? -2 : -1), uses, node); + const name = names.at(-1)!; + let type: ValueType, leaf: QueryExpression["leaf"]; + if (meta) { + if (name === "ref" && row.kind === "object") { + type = valueType.interfaceRef(row.interfaceRevisionId); + leaf = { kind: "ref", interfaceRevisionId: row.interfaceRevisionId }; + } else if (name === "entry" && (row.kind === "pair" || row.membership)) { + type = valueType.string; + leaf = { kind: "entry" }; + } else if (name === "mapKey" && row.kind === "object" && row.membership?.keyType) { + type = + row.membership.keyType === "boolean" + ? valueType.bool + : row.membership.keyType === "int64" + ? valueType.int64 + : valueType.string; + leaf = { kind: "mapKey" }; + } else return fail("QUERY_KEY_INVALID", `Unavailable metadata ${names.join(".")}`, node); + } else { + if (row.kind !== "object") + return fail("QUERY_FIELD_NOT_QUERYABLE", `Expected source or target, not ${name}`, node); + const field = rowFields(row).get(name); + if (!field || field.kind !== "leaf") { + if (relationships(row).some((m) => m.displayName === name)) + fail("QUERY_EXPANSION_REQUIRED", `${name} is not a scalar operand`, node); + return fail("QUERY_FIELD_NOT_QUERYABLE", `Unknown queryable scalar ${name}`, node); + } + for (const use of uses) mark(row.interfaceRevisionId, name, use, node); + const member = env.contracts.get(row.interfaceRevisionId)!.members.find((m) => m.displayName === name)!; + type = field.type; + leaf = { kind: "field", interfaceRevisionId: row.interfaceRevisionId, memberId: member.id }; + } + return { path, leaf, type: nullable && type.kind !== "optional" ? valueType.optional(type) : type }; + }; + const keyList = (row: QueryRow, node: ValueNode, use: "group" | "distinct", available?: Availability): QueryKey[] => { + const result: QueryKey[] = []; + const visit = (node: ValueNode, path: string[]) => { + if (node.kind === "BooleanValue") { + if (!node.value) fail("QUERY_KEY_INVALID", "Key selector leaves must be literal true", node); + const expr = expression(row, path, [use], node, available); + if (!queryKeyType(expr.type)) fail("QUERY_KEY_INVALID", `Unsupported key ${path.join(".")}`, node); + result.push({ path, expression: expr }); + return; + } + for (const field of pairs(node)) visit(field.value, [...path, field.name.value]); + }; + visit(node, []); + if (!result.length) fail("QUERY_KEY_INVALID", "Key selectors must not be empty", node); + return result.sort((a, b) => JSON.stringify(a.expression).localeCompare(JSON.stringify(b.expression))); + }; + const comparisons = ( + node: ValueNode, + make: ( + operator: Extract["operator"], + value: QueryArgument, + ) => QueryPredicate | QueryAggregatePredicate, + ) => + pairs(node).map((field) => { + if (!ops.has(field.name.value)) + return fail("QUERY_PREDICATE_INVALID", `Unsupported comparison ${field.name.value}`, field); + if (field.name.value === "in" && field.value.kind === "ListValue" && field.value.values.length > 1000) + fail("QUERY_WORK_LIMIT", "in supports at most 1000 operands", field); + return make( + field.name.value as Extract["operator"], + queryArgument(field.value), + ); + }); + const reduction = ( + row: QueryRow, + op: AggregateOperator, + path: string[], + node: ASTNode, + uses: QueryUse[], + available?: Availability, + ): QueryReduction => { + if (op === "count") return { operator: op, type: aggregateType(op) }; + const operand = expression(row, path, ["aggregate", ...uses], node, available); + return { operator: op, operand, type: aggregateType(op, operand.type) }; + }; + const aggregatePredicate = ( + row: QueryRow, + node: ValueNode, + keys: QueryKey[] = [], + available?: Availability, + ): QueryAggregatePredicate => { + const children: QueryAggregatePredicate[] = []; + for (const field of pairs(node)) { + const name = field.name.value; + if (name === "and" || name === "or") { + if (field.value.kind !== "ListValue") + fail("QUERY_UNSUPPORTED_FEATURE", "Boolean predicate structure is fixed in the document", field); + children.push({ + kind: name, + children: field.value.values.map((v) => aggregatePredicate(row, v, keys, available)), + }); + } else if (name === "not") + children.push({ kind: "not", child: aggregatePredicate(row, field.value, keys, available) }); + else if (name === "count") + children.push( + ...(comparisons(field.value, (operator, value) => ({ + kind: "compare", + expression: reduction(row, "count", [], field, []), + operator, + value, + })) as QueryAggregatePredicate[]), + ); + else { + const visit = (n: ValueNode, path: string[]) => { + const entries = pairs(n); + if (entries.some((f) => ops.has(f.name.value))) { + if (name === "group" && !keys.some((k) => k.path.join(".") === path.join("."))) + fail("QUERY_GROUP_FIELD_UNAVAILABLE", `${path.join(".")} is not a grouping key`, n); + const expr = + name === "group" + ? expression(row, path, ["predicate"], n, available) + : reduction(row, name as AggregateOperator, path, n, ["predicate"], available); + children.push( + ...(comparisons(n, (operator, value) => ({ + kind: "compare", + expression: expr, + operator, + value, + })) as QueryAggregatePredicate[]), + ); + } else for (const entry of entries) visit(entry.value, [...path, entry.name.value]); + }; + visit(field.value, []); + } + } + return { kind: "and", children }; + }; + const predicate = (row: QueryRow, node: ValueNode, available?: Availability): QueryPredicate => { + const children: QueryPredicate[] = []; + const visit = (current: QueryRow, n: ValueNode, base: string[]) => { + for (const field of pairs(n)) { + const name = field.name.value; + if (name === "and" || name === "or") { + if (field.value.kind !== "ListValue") + fail("QUERY_UNSUPPORTED_FEATURE", "Boolean predicate structure is fixed in the document", field); + const branches = field.value.values.map((v) => { + const before = children.length; + visit(current, v, base); + return { kind: "and", children: children.splice(before) } as QueryPredicate; + }); + children.push({ kind: name, children: branches }); + } else if (name === "not") { + const before = children.length; + visit(current, field.value, base); + children.push({ kind: "not", child: { kind: "and", children: children.splice(before) } }); + } else if (current.kind === "pair" && (name === "source" || name === "target")) + visit(current[name], field.value, [...base, name]); + else if (name === "_qx") { + for (const meta of pairs(field.value)) { + if (meta.name.value !== "relations") { + const expr = expression(row, [...base, "_qx", meta.name.value], ["predicate"], meta, available); + children.push( + ...(comparisons(meta.value, (operator, value) => ({ + kind: "compare", + expression: expr, + operator, + value, + })) as QueryPredicate[]), + ); + continue; + } + for (const edge of pairs(meta.value)) { + if (current.kind !== "object") + fail("QUERY_CONTRACT", "A pair is not an object with graph relationships", edge); + const member = relationships(current).find((m) => m.displayName === edge.name.value)!; + requireAvailable([...base, member.displayName], available, edge); + mark(current.interfaceRevisionId, member.displayName, "predicate", edge); + const common = { + path: walk(row, base, ["predicate"], edge).path, + interfaceRevisionId: current.interfaceRevisionId, + memberId: member.id, + targetInterfaceRevisionId: env.target(member), + }; + for (const op of pairs(edge.value)) { + if (op.name.value === "aggregate") { + const config = pairs(op.value), + where = config.find((f) => f.name.value === "where"), + having = config.find((f) => f.name.value === "having")!; + children.push({ + kind: "reduce", + ...common, + where: where ? predicate(env.relational.relationRow(member), where.value) : undefined, + having: aggregatePredicate(env.relational.relationRow(member), having.value), + }); + } else if (op.name.value === "isNull") + children.push({ kind: "relation", ...common, operator: "isNull", value: queryArgument(op.value) }); + else + children.push({ + kind: "relation", + ...common, + operator: op.name.value as "some" | "none" | "is", + predicate: predicate(env.relational.relationRow(member), op.value), + }); + } + } + } + } else { + const expr = expression(row, [...base, name], ["predicate"], field, available); + children.push( + ...(comparisons(field.value, (operator, value) => ({ + kind: "compare", + expression: expr, + operator, + value, + })) as QueryPredicate[]), + ); + } + } + }; + visit(row, node, []); + return { kind: "and", children }; + }; + + const plan = ( + owner: InterfaceRevisionId, + member: RelationshipInterfaceMember, + set: SelectionSetNode, + selection: (set: SelectionSetNode, typeName: string) => QuerySelection[], + ): QueryRelationalPlan => { + const result: QueryRelationalPlan = { stages: [], terminals: [] }; + const add = (row: QueryRow, operation: QueryRelationalPlan["stages"][number]["operation"], input?: number) => { + if (++stageCount > 256) throw new QueryCompileError("QUERY_WORK_LIMIT", "Query exceeds 256 relational stages"); + const id = result.stages.length; + result.stages.push({ id, row, operation, input }); + return id; + }; + const root = env.relational.relationRow(member); + const initial = add(root, { + kind: "source", + interfaceRevisionId: owner, + memberId: member.id, + targetInterfaceRevisionId: env.target(member), + }); + mark(owner, member.displayName, "select", set); + const bound = (node: FieldNode) => { + const args = node.arguments ?? [], + bounds = args.filter((a) => a.name.value === "first" || a.name.value === "all"); + if (bounds.length !== 1) fail("QUERY_ROW_LIMIT", "Specify exactly one first or all for groups", node); + if (bounds[0]!.name.value === "all" && args.some((a) => a.name.value === "after")) + fail("QUERY_UNSUPPORTED_FEATURE", "Bounded-all cannot continue", node); + for (const b of bounds) + if ( + b.value.kind !== "Variable" && + (b.value.kind !== "IntValue" || Number(b.value.value) < 1 || Number(b.value.value) > declaration.budgets.rows) + ) + fail("QUERY_ROW_LIMIT", "Group page exceeds row budget", b); + }; + const reductions = ( + row: QueryRow, + set: SelectionSetNode, + available: Availability, + ): QueryRelationalTerminal["reductions"] => { + const output: QueryRelationalTerminal["reductions"] = []; + const visit = (set: SelectionSetNode, path: string[], response: string[], op: AggregateOperator) => { + for (const n of fields(set)) { + const key = n.alias?.value ?? n.name.value, + next = [...path, n.name.value], + out = [...response, key]; + if (n.selectionSet) visit(n.selectionSet, next, out, op); + else output.push({ path: out, reduction: reduction(row, op, next, n, [], available) }); + } + }; + for (const n of fields(set)) { + const op = n.name.value as AggregateOperator, + key = n.alias?.value ?? n.name.value; + if (op === "count") output.push({ path: [key], reduction: reduction(row, op, [], n, []) }); + else if (n.selectionSet) visit(n.selectionSet, [], [key], op); + } + return output; + }; + const groupsProjection = (row: QueryRow, set: SelectionSetNode, keys: QueryKey[], available: Availability) => { + let values: QueryRelationalTerminal["reductions"] = []; + for (const entries of fields(set)) + if (entries.name.value === "entries" && entries.selectionSet) + for (const n of fields(entries.selectionSet)) { + if (n.name.value === "aggregate" && n.selectionSet) + values.push( + ...reductions(row, n.selectionSet, available).map((v) => ({ + ...v, + path: [entries.alias?.value ?? entries.name.value, n.alias?.value ?? n.name.value, ...v.path], + })), + ); + if (n.name.value === "group" && n.selectionSet) { + const visit = (set: SelectionSetNode, path: string[]) => { + for (const f of fields(set)) { + const next = [...path, f.name.value]; + if (f.selectionSet) visit(f.selectionSet, next); + else if (!keys.some((k) => k.path.join(".") === next.join("."))) + fail("QUERY_GROUP_FIELD_UNAVAILABLE", `${next.join(".")} is not a selected group key`, f); + } + }; + visit(n.selectionSet, []); + } + } + return values; + }; + const run = (row: QueryRow, stage: number, set: SelectionSetNode, path: string[], available?: Availability) => { + for (const node of fields(set)) { + if (!node.selectionSet) continue; + if (node.directives?.length) + fail("QUERY_UNSUPPORTED_FEATURE", "Relational stages cannot be conditionally selected", node); + const name = node.name.value, + outputPath = [...path, node.alias?.value ?? name]; + const args = new Map((node.arguments ?? []).map((a) => [a.name.value, a.value])); + if (name === "filter") + run( + row, + add(row, { kind: "filter", predicate: predicate(row, args.get("where")!, available) }, stage), + node.selectionSet, + outputPath, + available, + ); + else if (name === "distinct") { + const keys = keyList(row, args.get("by")!, "distinct", available); + run(row, add(row, { kind: "distinct", keys }, stage), node.selectionSet, outputPath, keys); + } else if (name === "expand") { + const expand = (current: QueryRow, set: SelectionSetNode, memberPath: string[], output: string[]) => { + for (const n of fields(set)) { + if (!n.selectionSet) continue; + const member = relationships(current).find((m) => m.displayName === n.name.value); + const next = [...memberPath, n.name.value], + out = [...output, n.alias?.value ?? n.name.value]; + if (member && (member.cardinality === "many" || member.cardinality === "many-unique")) { + requireAvailable(next, available, n); + if (current.kind !== "object") fail("QUERY_CONTRACT", "Expected object expansion source", n); + const walked = walk(row, memberPath, ["select"], n); + mark(current.interfaceRevisionId, member.displayName, "select", n); + const target = env.relational.relationRow(member), + pair: QueryRow = { kind: "pair", source: row, target }; + const newAvailable = available + ? [ + ...available.map((k) => ({ + ...k, + path: ["source", ...k.path], + expression: { ...k.expression, path: [{ kind: "source" as const }, ...k.expression.path] }, + })), + { + path: ["target", "_qx", "entry"], + expression: { + path: [{ kind: "target" as const }], + leaf: { kind: "entry" as const }, + type: valueType.string, + }, + }, + ] + : undefined; + run( + pair, + add( + pair, + { + kind: "expand", + path: walked.path, + interfaceRevisionId: current.interfaceRevisionId, + memberId: member.id, + targetInterfaceRevisionId: env.target(member), + }, + stage, + ), + n.selectionSet, + out, + newAvailable, + ); + } else { + const f = rowFields(current).get(n.name.value); + if (f?.kind === "row") expand(f.row, n.selectionSet, next, out); + else fail("QUERY_EXPANSION_REQUIRED", "Expected a declared expansion path", n); + } + } + }; + expand(row, node.selectionSet, [], outputPath); + } else if (name === "aggregate" || name === "groups") { + if (name === "groups") bound(node); + const keys = name === "groups" ? keyList(row, args.get("by")!, "group", available) : []; + const terminal: QueryRelationalTerminal = { + stage, + path: outputPath, + kind: name, + keys, + reductions: + name === "aggregate" + ? reductions(row, node.selectionSet, available) + : groupsProjection(row, node.selectionSet, keys, available), + order: [], + residual: false, + where: args.has("where") ? predicate(row, args.get("where")!, available) : undefined, + having: args.has("having") ? aggregatePredicate(row, args.get("having")!, keys, available) : undefined, + first: args.has("first") ? queryArgument(args.get("first")!) : undefined, + all: args.has("all") ? queryArgument(args.get("all")!) : undefined, + after: args.has("after") ? queryArgument(args.get("after")!) : undefined, + selection: selection( + node.selectionSet, + name === "aggregate" + ? env.relational.rowset(row).getFields().aggregate!.type.toString().replace(/!$/u, "") + : env.relational.rowset(row).getFields().groups!.type.toString().replace(/!$/u, ""), + ), + }; + const order = args.get("orderBy"); + if (order) { + if (order.kind !== "ListValue") + fail("QUERY_UNSUPPORTED_FEATURE", "Ordering is a fixed list of field directions", order); + for (const item of order.values) { + const start = terminal.order.length; + const visit = (n: ValueNode, path: string[]) => { + if (n.kind === "EnumValue") { + const [op, ...operand] = path; + let expr: QueryExpression | QueryReduction; + if (op === "group") { + if (!keys.some((k) => k.path.join(".") === operand.join("."))) + fail("QUERY_GROUP_FIELD_UNAVAILABLE", "Sort key was not grouped", n); + expr = expression(row, operand, ["order"], n, available); + } else expr = reduction(row, op as AggregateOperator, operand, n, ["order"], available); + terminal.order.push({ expression: expr, descending: n.value === "DESC" }); + } else for (const f of pairs(n)) visit(f.value, [...path, f.name.value]); + }; + visit(item, []); + if (terminal.order.length !== start + 1) + fail("QUERY_ORDER_INVALID", "Each orderBy element must name exactly one field", item); + } + } + result.terminals.push(terminal); + } else fail("QUERY_UNSUPPORTED_FEATURE", `Unsupported relational stage ${name}`, node); + } + }; + run(root, initial, set, []); + for (const terminal of result.terminals) { + let stage: QueryRelationalPlan["stages"][number] | undefined = result.stages[terminal.stage]; + let rpc = needsRpc(terminal); + while (stage) { + rpc ||= needsRpc(stage.operation); + stage = stage.input === undefined ? undefined : result.stages[stage.input]; + } + terminal.residual = rpc; + if (rpc && terminal.after) + fail( + "QUERY_UNSUPPORTED_FEATURE", + "RPC relational inputs require a bounded first/all window without continuation", + set, + ); + } + return result; + }; + return { expression, predicate, aggregatePredicate, keyList, plan, needsRpc }; +} diff --git a/src/query/relational-proto.ts b/src/query/relational-proto.ts new file mode 100644 index 0000000..9f5d6a3 --- /dev/null +++ b/src/query/relational-proto.ts @@ -0,0 +1,164 @@ +import { create } from "@bufbuild/protobuf"; +import * as wire from "../gen/camino/schema_pb.js"; +import type { QueryArgument, QuerySelection } from "./types.js"; +import type { + QueryAggregatePredicate, + QueryExpression, + QueryPathStep, + QueryPredicate, + QueryReduction, + QueryRelationalPlan, + QueryRow, +} from "./relational.js"; + +export function relationalWire( + argument: (v: QueryArgument) => wire.QueryArgument, + selection: (v: QuerySelection) => wire.QuerySelection, +) { + const path = (p: QueryPathStep): wire.QueryPathStep => + create(wire.QueryPathStepSchema, { + step: p.kind === "relation" ? { case: "relation", value: p } : { case: p.kind, value: true }, + }); + const expression = (e: QueryExpression) => + create(wire.QueryExpressionSchema, { + path: e.path.map(path), + valueTypeJson: JSON.stringify(e.type), + leaf: + e.leaf.kind === "field" + ? { case: "field", value: e.leaf } + : e.leaf.kind === "ref" + ? { case: "ref", value: e.leaf.interfaceRevisionId } + : { case: e.leaf.kind, value: true }, + }); + const reduction = (r: QueryReduction) => + create(wire.QueryReductionSchema, { + operator: { + count: wire.QueryAggregateOperator.COUNT, + countPresent: wire.QueryAggregateOperator.COUNT_PRESENT, + sum: wire.QueryAggregateOperator.SUM, + avg: wire.QueryAggregateOperator.AVG, + min: wire.QueryAggregateOperator.MIN, + max: wire.QueryAggregateOperator.MAX, + }[r.operator], + operand: r.operand && expression(r.operand), + valueTypeJson: JSON.stringify(r.type), + }); + const operand = (v: QueryExpression | QueryReduction) => + create(wire.QueryOperandSchema, { + operand: + "operator" in v ? { case: "reduction", value: reduction(v) } : { case: "expression", value: expression(v) }, + }); + const predicate = (p: QueryPredicate | QueryAggregatePredicate): wire.QueryPredicate => { + switch (p.kind) { + case "and": + case "or": + return create(wire.QueryPredicateSchema, { + predicate: { case: p.kind, value: { children: p.children.map(predicate) } }, + }); + case "not": + return create(wire.QueryPredicateSchema, { predicate: { case: "not", value: predicate(p.child) } }); + case "compare": + return create(wire.QueryPredicateSchema, { + predicate: { + case: "compare", + value: { + operand: operand(p.expression), + value: argument(p.value), + operator: { + eq: wire.QueryComparisonOperator.EQ, + in: wire.QueryComparisonOperator.IN, + isNull: wire.QueryComparisonOperator.IS_NULL, + lt: wire.QueryComparisonOperator.LT, + lte: wire.QueryComparisonOperator.LTE, + gt: wire.QueryComparisonOperator.GT, + gte: wire.QueryComparisonOperator.GTE, + }[p.operator], + }, + }, + }); + case "relation": + return create(wire.QueryPredicateSchema, { + predicate: { + case: "relation", + value: { + path: p.path.map(path), + relation: p, + operator: { + some: wire.QueryRelationPredicateOperator.SOME, + none: wire.QueryRelationPredicateOperator.NONE, + is: wire.QueryRelationPredicateOperator.IS, + isNull: wire.QueryRelationPredicateOperator.IS_NULL, + }[p.operator], + predicate: p.predicate && predicate(p.predicate), + value: p.value && argument(p.value), + }, + }, + }); + case "reduce": + return create(wire.QueryPredicateSchema, { + predicate: { + case: "reduce", + value: { + path: p.path.map(path), + relation: p, + where: p.where && predicate(p.where), + having: predicate(p.having), + }, + }, + }); + } + }; + const row = (r: QueryRow): wire.QueryRow => + create(wire.QueryRowSchema, { + row: + r.kind === "object" + ? { + case: "object", + value: { + interfaceRevisionId: r.interfaceRevisionId, + membership: !!r.membership, + keyType: r.membership?.keyType, + }, + } + : { case: "pair", value: { source: row(r.source), target: row(r.target) } }, + }); + const plan = (p: QueryRelationalPlan) => + create(wire.QueryRelationalPlanSchema, { + stages: p.stages.map((s) => { + const op = s.operation; + const operation: wire.QueryRelationalStage["operation"] = + op.kind === "source" + ? { case: "source", value: create(wire.QueryRelationPathSchema, op) } + : op.kind === "filter" + ? { case: "filter", value: predicate(op.predicate) } + : op.kind === "expand" + ? { + case: "expand", + value: create(wire.QueryExpansionSchema, { path: op.path.map(path), relation: op }), + } + : { + case: "distinct", + value: create(wire.QueryDistinctSchema, { + keys: op.keys.map((k) => ({ ...k, expression: expression(k.expression) })), + }), + }; + return create(wire.QueryRelationalStageSchema, { id: s.id, input: s.input, row: row(s.row), operation }); + }), + terminals: p.terminals.map((t) => ({ + stage: t.stage, + path: t.path, + groups: t.kind === "groups", + keys: t.keys.map((k) => ({ ...k, expression: expression(k.expression) })), + reductions: t.reductions.map((r) => ({ ...r, reduction: reduction(r.reduction) })), + where: t.where && predicate(t.where), + having: t.having && predicate(t.having), + order: t.order.map((o) => ({ operand: operand(o.expression), descending: o.descending })), + first: t.first && argument(t.first), + all: t.all && argument(t.all), + after: t.after && argument(t.after), + selection: t.selection.map(selection), + residual: t.residual, + })), + }); + return { predicate, plan }; +} diff --git a/src/query/relational-schema.ts b/src/query/relational-schema.ts new file mode 100644 index 0000000..b26050b --- /dev/null +++ b/src/query/relational-schema.ts @@ -0,0 +1,497 @@ +import { + GraphQLBoolean, + GraphQLString, + GraphQLInt, + GraphQLScalarType, + GraphQLObjectType, + GraphQLInputObjectType, + GraphQLList, + GraphQLNonNull, + type GraphQLInputType, + type GraphQLOutputType, + type GraphQLInputFieldConfigMap, + type GraphQLFieldConfigMap, + type GraphQLEnumType, + type DocumentNode, + type SelectionSetNode, + type FragmentDefinitionNode, +} from "graphql"; +import { createHash } from "node:crypto"; +import { + valueType, + type ValueType, + type InterfaceRevision, + type InterfaceRevisionId, + type RelationshipInterfaceMember, +} from "../capability-model/types.js"; +import { + aggregateType, + aggregateOperators, + queryKeyType, + type QueryRow, + type AggregateOperator, +} from "./relational.js"; +import { QueryCompileError } from "./types.js"; + +export const rowKey = (row: QueryRow): string => JSON.stringify(row); +export const objectRow = ( + interfaceRevisionId: InterfaceRevisionId, + membership?: { keyType?: "string" | "boolean" | "int64" }, +): QueryRow => ({ kind: "object", interfaceRevisionId, ...(membership ? { membership } : {}) }); +export type SchemaRowField = { kind: "leaf"; type: ValueType } | { kind: "row"; row: QueryRow; optional: boolean }; +export interface RelationalSchemaEnvironment { + contracts: Map; + target(member: RelationshipInterfaceMember): InterfaceRevisionId; + name(id: InterfaceRevisionId): string; + scalar(type: ValueType): GraphQLInputType & GraphQLOutputType; + comparison(type: ValueType): GraphQLInputObjectType; + scalarTypes: Map; + direction: GraphQLEnumType; + cursor: GraphQLScalarType; + pageInfo: GraphQLObjectType; +} + +export function relationalSchema(env: RelationalSchemaEnvironment, document?: DocumentNode) { + const outputCache = new Map(); + const inputCache = new Map(); + const referenceCache = new Map(); + const rowsets = new Map(); + const expansionPaths = new Map>(); + const roles = new Map< + string, + { kind: "rowset" | "aggregate" | "groups" | "keys" | "expand" | "relations"; row: QueryRow } + >(); + const many = (member: RelationshipInterfaceMember) => + member.cardinality === "many" || member.cardinality === "many-unique"; + const relationRow = (member: RelationshipInterfaceMember) => + objectRow(env.target(member), { keyType: member.keyType }); + const relations = (row: QueryRow) => + row.kind === "object" + ? env.contracts + .get(row.interfaceRevisionId)! + .members.filter( + (member): member is RelationshipInterfaceMember => member.kind === "relationship" && !!member.queryRead, + ) + : []; + const fields = (row: QueryRow): Map => { + if (row.kind === "pair") + return new Map([ + ["source", { kind: "row", row: row.source, optional: false }], + ["target", { kind: "row", row: row.target, optional: false }], + ]); + return new Map( + env.contracts.get(row.interfaceRevisionId)!.members.flatMap((member): [string, SchemaRowField][] => { + if (member.kind === "operation" || !member.queryRead) return []; + if (member.kind === "value") return [[member.displayName, { kind: "leaf", type: member.valueType }]]; + if (many(member)) return []; + return [ + [ + member.displayName, + { kind: "row", row: objectRow(env.target(member)), optional: member.cardinality === "optional-one" }, + ], + ]; + }), + ); + }; + const refs = (id: InterfaceRevisionId) => { + let result = referenceCache.get(id); + if (!result) { + result = new GraphQLScalarType({ + name: `QxRef_${env.name(id)}`, + parseValue: (value) => value, + parseLiteral() { + throw new Error("Managed references must be supplied as typed variables"); + }, + }); + referenceCache.set(id, result); + env.scalarTypes.set(result.name, valueType.interfaceRef(id)); + } + return result; + }; + const scalar = (type: ValueType): GraphQLInputType & GraphQLOutputType => { + if (type.kind === "optional") return scalar(type.value); + return type.kind === "object-ref" && type.expectation.kind === "interface" + ? refs(type.expectation.interfaceRevisionId) + : env.scalar(type); + }; + const leafFields = (row: QueryRow) => { + const result = new Map(); + if (row.kind === "object") result.set("ref", valueType.interfaceRef(row.interfaceRevisionId)); + if (row.kind === "pair" || row.membership) result.set("entry", valueType.string); + if (row.kind === "object" && row.membership?.keyType) + result.set( + "mapKey", + row.membership.keyType === "boolean" + ? valueType.bool + : row.membership.keyType === "int64" + ? valueType.int64 + : valueType.string, + ); + return result; + }; + const typeName = (role: string, row: QueryRow, suffix = "") => + `Qx${role}_${createHash("sha256") + .update(rowKey(row) + suffix) + .digest("hex") + .slice(0, 16)}`; + const out = (role: string, row: QueryRow, make: () => GraphQLFieldConfigMap, suffix = "") => { + const key = typeName(role, row, suffix); + let result = outputCache.get(key); + if (!result) { + result = new GraphQLObjectType({ name: key, fields: make }); + outputCache.set(key, result); + } + return result; + }; + const inp = (role: string, row: QueryRow, make: () => GraphQLInputFieldConfigMap, suffix = "") => { + const key = typeName(role, row, suffix); + let result = inputCache.get(key); + if (!result) { + result = new GraphQLInputObjectType({ name: key, fields: make }); + inputCache.set(key, result); + } + return result; + }; + const compare = (type: ValueType) => { + const base = type.kind === "optional" ? type.value : type; + if (base.kind !== "object-ref") return env.comparison(type); + return inp( + "RefCompare", + objectRow( + base.expectation.kind === "interface" + ? base.expectation.interfaceRevisionId + : (() => { + throw new Error("Expected interface reference"); + })(), + ), + () => ({ eq: { type: scalar(type) }, in: { type: new GraphQLList(new GraphQLNonNull(scalar(type))) } }), + ); + }; + // Recursive interface graphs are interned. Synthetic pair graphs are finite and document-driven. + const columns = ( + row: QueryRow, + operator?: Exclude, + nullable = false, + ): GraphQLObjectType => + out( + "Columns", + row, + () => { + const result: GraphQLFieldConfigMap = {}; + for (const [name, field] of fields(row)) { + if (field.kind === "row") + result[name] = { type: new GraphQLNonNull(columns(field.row, operator, nullable || field.optional)) }; + else { + let type = field.type; + if (operator) { + try { + type = aggregateType(operator, type); + } catch { + continue; + } + } else if (!queryKeyType(type)) continue; + result[name] = { + type: + type.kind === "optional" || (!operator && nullable) ? scalar(type) : new GraphQLNonNull(scalar(type)), + }; + } + } + const metadata: GraphQLFieldConfigMap = {}; + for (const [name, type] of leafFields(row)) { + if (operator && operator !== "countPresent") continue; + const t = scalar(operator ? aggregateType(operator, type) : type); + metadata[name] = { type: !operator && nullable ? t : new GraphQLNonNull(t) }; + } + if (Object.keys(metadata).length) + result._qx = { type: new GraphQLNonNull(out("ColumnsMeta", row, () => metadata, `${operator}:${nullable}`)) }; + // A type may have no matching scalar at this node; a private schema sentinel + // keeps it valid while compiler validation forbids selecting that sentinel. + if (!Object.keys(result).length) result._unavailable = { type: GraphQLBoolean }; + return result; + }, + `${operator}:${nullable}`, + ); + const aggregate = (row: QueryRow): GraphQLObjectType => { + const result = out("Aggregate", row, () => + Object.fromEntries( + aggregateOperators.map((op) => [ + op, + { type: new GraphQLNonNull(op === "count" ? env.scalar(valueType.uint64) : columns(row, op)) }, + ]), + ), + ); + roles.set(result.name, { kind: "aggregate", row }); + return result; + }; + const keys = (row: QueryRow): GraphQLInputObjectType => + inp("Keys", row, () => { + const result: GraphQLInputFieldConfigMap = {}; + for (const [name, field] of fields(row)) { + if (field.kind === "row") result[name] = { type: keys(field.row) }; + else if (queryKeyType(field.type)) result[name] = { type: GraphQLBoolean }; + } + const meta = Object.fromEntries([...leafFields(row)].map(([name]) => [name, { type: GraphQLBoolean }])); + if (Object.keys(meta).length) result._qx = { type: inp("KeyMeta", row, () => meta) }; + return result; + }); + const comparisons = ( + row: QueryRow, + operator?: Exclude, + ordering = false, + ): GraphQLInputObjectType => + inp( + "Expressions", + row, + () => { + const result: GraphQLInputFieldConfigMap = {}; + for (const [name, field] of fields(row)) { + if (field.kind === "row") result[name] = { type: comparisons(field.row, operator, ordering) }; + else { + let type = field.type; + if (operator) { + try { + type = aggregateType(operator, type); + } catch { + continue; + } + } + result[name] = { type: ordering ? env.direction : compare(type) }; + } + } + const meta: GraphQLInputFieldConfigMap = {}; + for (const [name, type] of leafFields(row)) + if (!operator || operator === "countPresent") + meta[name] = { type: ordering ? env.direction : compare(operator ? aggregateType(operator, type) : type) }; + if (Object.keys(meta).length) + result._qx = { type: inp("ExpressionMeta", row, () => meta, `${operator}:${ordering}`) }; + if (!Object.keys(result).length) result._unavailable = { type: GraphQLBoolean }; + return result; + }, + `${operator}:${ordering}`, + ); + const having = (row: QueryRow, ordering = false): GraphQLInputObjectType => + inp( + "Having", + row, + () => { + const result: GraphQLInputFieldConfigMap = { group: { type: comparisons(row, undefined, ordering) } }; + for (const op of aggregateOperators) + result[op] = { + type: + op === "count" ? (ordering ? env.direction : compare(valueType.uint64)) : comparisons(row, op, ordering), + }; + if (!ordering) + Object.assign(result, { + and: { type: new GraphQLList(new GraphQLNonNull(having(row))) }, + or: { type: new GraphQLList(new GraphQLNonNull(having(row))) }, + not: { type: having(row) }, + }); + return result; + }, + String(ordering), + ); + const where = (row: QueryRow): GraphQLInputObjectType => + inp("Where", row, () => { + const result: GraphQLInputFieldConfigMap = { + and: { type: new GraphQLList(new GraphQLNonNull(where(row))) }, + or: { type: new GraphQLList(new GraphQLNonNull(where(row))) }, + not: { type: where(row) }, + }; + for (const [name, field] of fields(row)) { + if (row.kind === "object" && field.kind === "row") continue; + if (name in result) throw new QueryCompileError("QUERY_SCHEMA_NAME", `Reserved predicate name ${name}`); + result[name] = { type: field.kind === "row" ? where(field.row) : compare(field.type) }; + } + result._qx = { + type: inp("WhereMeta", row, () => { + const meta: GraphQLInputFieldConfigMap = {}; + for (const [name, type] of leafFields(row)) meta[name] = { type: compare(type) }; + const edges = relations(row); + if (edges.length) + meta.relations = { + type: inp("WhereRelations", row, () => + Object.fromEntries( + edges.map((member) => { + const target = relationRow(member); + return [ + member.displayName, + { + type: inp( + "RelationPredicate", + target, + (): GraphQLInputFieldConfigMap => + many(member) + ? { + some: { type: where(target) }, + none: { type: where(target) }, + aggregate: { + type: inp("ReductionPredicate", target, () => ({ + where: { type: where(target) }, + having: { type: new GraphQLNonNull(having(target)) }, + })), + }, + } + : { is: { type: where(target) }, isNull: { type: GraphQLBoolean } }, + rowKey(row) + member.id, + ), + }, + ]; + }), + ), + ), + }; + return meta; + }), + }; + return result; + }); + + const fragments = new Map( + (document?.definitions ?? []) + .filter((d): d is FragmentDefinitionNode => d.kind === "FragmentDefinition") + .map((d) => [d.name.value, d]), + ); + let visited = 0; + const selections = (set: SelectionSetNode, active: ReadonlySet = new Set()): import("graphql").FieldNode[] => + set.selections.flatMap((node) => { + if (++visited > 10000) + throw new QueryCompileError("QUERY_WORK_LIMIT", "Query schema discovery exceeds 10000 nodes"); + if (node.kind === "Field") return [node]; + if (node.kind === "FragmentSpread") { + if (active.has(node.name.value) || active.size >= 128) + throw new QueryCompileError("QUERY_VALIDATION", "Cyclic or excessively nested fragments"); + const fragment = fragments.get(node.name.value); + return fragment ? selections(fragment.selectionSet, new Set([...active, node.name.value])) : []; + } + return []; + }); + const discoverExpansion = (root: QueryRow, current: QueryRow, set: SelectionSetNode, path: string[] = []) => { + for (const node of selections(set)) { + if (!node.selectionSet) continue; + const member = relations(current).find((m) => m.displayName === node.name.value); + const next = [...path, node.name.value]; + if (member && many(member)) { + const map = expansionPaths.get(rowKey(root)) ?? new Map(); + map.set(next.join("."), next); + expansionPaths.set(rowKey(root), map); + discoverRows({ kind: "pair", source: root, target: relationRow(member) }, node.selectionSet); + } else { + const field = fields(current).get(node.name.value); + if (field?.kind === "row") discoverExpansion(root, field.row, node.selectionSet, next); + } + } + }; + const discoverRows = (row: QueryRow, set: SelectionSetNode) => { + rowsets.set(rowKey(row), row); + if (rowsets.size > 256) throw new QueryCompileError("QUERY_WORK_LIMIT", "Query exceeds 256 row shapes"); + for (const node of selections(set)) + if (node.selectionSet) { + if (node.name.value === "expand") discoverExpansion(row, row, node.selectionSet); + else if (node.name.value === "filter" || node.name.value === "distinct") discoverRows(row, node.selectionSet); + } + }; + const discoverObject = (row: QueryRow, set: SelectionSetNode) => { + for (const node of selections(set)) + if (node.selectionSet) { + if (node.name.value === "_qx") { + for (const meta of selections(node.selectionSet)) + if (meta.name.value === "relations" && meta.selectionSet) + for (const edge of selections(meta.selectionSet)) { + const member = relations(row).find((m) => m.displayName === edge.name.value); + if (member && many(member) && edge.selectionSet) discoverRows(relationRow(member), edge.selectionSet); + } + } else { + const member = relations(row).find((m) => m.displayName === node.name.value); + if (member) { + if (!many(member)) discoverObject(objectRow(env.target(member)), node.selectionSet); + else + for (const entries of selections(node.selectionSet)) + if (entries.name.value === "entries" && entries.selectionSet) + for (const child of selections(entries.selectionSet)) + if (child.name.value === "node" && child.selectionSet) + discoverObject(objectRow(env.target(member)), child.selectionSet); + } + } + } + }; + const discover = (root: InterfaceRevisionId) => { + for (const operation of document?.definitions ?? []) + if (operation.kind === "OperationDefinition") + for (const node of selections(operation.selectionSet)) + if (node.name.value === "root" && node.selectionSet) discoverObject(objectRow(root), node.selectionSet); + }; + const expand = (root: QueryRow, current: QueryRow, path: string[] = []): GraphQLObjectType => + out( + "Expand", + current, + () => { + const result: GraphQLFieldConfigMap = {}; + const paths = [...(expansionPaths.get(rowKey(root))?.values() ?? [])].filter((p) => + path.every((part, i) => p[i] === part), + ); + for (const name of new Set(paths.map((p) => p[path.length]).filter((v): v is string => !!v))) { + const member = relations(current).find((m) => m.displayName === name); + if (member && many(member)) + result[name] = { + type: new GraphQLNonNull(rowset({ kind: "pair", source: root, target: relationRow(member) })), + }; + else { + const field = fields(current).get(name); + if (field?.kind === "row") + result[name] = { type: new GraphQLNonNull(expand(root, field.row, [...path, name])) }; + } + } + if (!Object.keys(result).length) result._unavailable = { type: GraphQLBoolean }; + return result; + }, + rowKey(root) + JSON.stringify(path), + ); + const rowset = (row: QueryRow): GraphQLObjectType => { + const result = out("Rows", row, () => { + const entry = out("GroupEntry", row, () => ({ + key: { type: new GraphQLNonNull(GraphQLString) }, + cursor: { type: env.cursor }, + group: { type: new GraphQLNonNull(columns(row)) }, + aggregate: { type: new GraphQLNonNull(aggregate(row)) }, + })); + const connection = out("Groups", row, () => ({ + entries: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(entry))) }, + pageInfo: { type: new GraphQLNonNull(env.pageInfo) }, + })); + roles.set(connection.name, { kind: "groups", row }); + return { + filter: { type: new GraphQLNonNull(rowset(row)), args: { where: { type: new GraphQLNonNull(where(row)) } } }, + distinct: { type: new GraphQLNonNull(rowset(row)), args: { by: { type: new GraphQLNonNull(keys(row)) } } }, + expand: { type: new GraphQLNonNull(expand(row, row)) }, + aggregate: { type: new GraphQLNonNull(aggregate(row)), args: { where: { type: where(row) } } }, + groups: { + type: new GraphQLNonNull(connection), + args: { + by: { type: new GraphQLNonNull(keys(row)) }, + where: { type: where(row) }, + having: { type: having(row) }, + orderBy: { type: new GraphQLList(new GraphQLNonNull(having(row, true))) }, + first: { type: GraphQLInt }, + all: { type: GraphQLInt }, + after: { type: env.cursor }, + }, + }, + }; + }); + roles.set(result.name, { kind: "rowset", row }); + return result; + }; + const helpers = (id: InterfaceRevisionId): GraphQLObjectType | undefined => { + const row = objectRow(id), + edges = relations(row).filter(many); + if (!edges.length) return undefined; + const result = out("Relations", row, () => + Object.fromEntries( + edges.map((member) => [member.displayName, { type: new GraphQLNonNull(rowset(relationRow(member))) }]), + ), + ); + roles.set(result.name, { kind: "relations", row }); + return result; + }; + return { refs, fields, relations, relationRow, where, rowset, helpers, roles, discover, scalar, compare }; +} diff --git a/src/query/relational.ts b/src/query/relational.ts new file mode 100644 index 0000000..fee63c0 --- /dev/null +++ b/src/query/relational.ts @@ -0,0 +1,149 @@ +import type { InterfaceRevisionId, MemberId, ValueType } from "../capability-model/types.js"; +import { valueType } from "../capability-model/types.js"; +import { QueryCompileError, type QueryArgument, type QueryUse } from "./types.js"; + +/** A row is an input membership, not a newly allocated Camino object. */ +export type QueryRow = + | { + kind: "object"; + interfaceRevisionId: InterfaceRevisionId; + membership?: { keyType?: "string" | "boolean" | "int64" }; + } + | { kind: "pair"; source: QueryRow; target: QueryRow }; + +export type AggregateOperator = "count" | "countPresent" | "sum" | "avg" | "min" | "max"; +export const aggregateOperators: readonly AggregateOperator[] = ["count", "countPresent", "sum", "avg", "min", "max"]; + +export function aggregateType(operator: AggregateOperator, input?: ValueType): ValueType { + if (operator === "count") return valueType.uint64; + const base = input?.kind === "optional" ? input.value : input; + if (operator === "countPresent" && (base?.kind === "scalar" || base?.kind === "object-ref")) return valueType.uint64; + if (base?.kind !== "scalar") + throw new QueryCompileError("QUERY_AGGREGATE_TYPE", `${operator} requires a supported scalar operand`); + const numeric = ["int32", "uint32", "int64", "uint64", "double"].includes(base.name); + if ((operator === "min" || operator === "max") && (numeric || base.name === "string")) + return valueType.optional(base); + if (numeric && operator === "avg") return valueType.optional(valueType.double); + if (numeric && operator === "sum") + return valueType.optional( + base.name === "double" ? valueType.double : base.name.startsWith("u") ? valueType.uint64 : valueType.int64, + ); + throw new QueryCompileError("QUERY_AGGREGATE_TYPE", `${operator} does not accept ${base.name}`); +} + +export function queryKeyType(type: ValueType): boolean { + if (type.kind === "optional") return queryKeyType(type.value); + return type.kind === "object-ref" || (type.kind === "scalar" && type.name !== "bytes"); +} + +/** Paths are resolved once by the compiler. Runtime never resolves authored names. */ +export type QueryPathStep = + | { kind: "source" | "target" } + | { + kind: "relation"; + interfaceRevisionId: InterfaceRevisionId; + memberId: MemberId; + targetInterfaceRevisionId: InterfaceRevisionId; + optional: boolean; + }; +export interface QueryExpression { + path: QueryPathStep[]; + leaf: + | { kind: "field"; interfaceRevisionId: InterfaceRevisionId; memberId: MemberId } + | { kind: "ref"; interfaceRevisionId: InterfaceRevisionId } + | { kind: "entry" | "mapKey" }; + type: ValueType; +} +export interface QueryReduction { + operator: AggregateOperator; + operand?: QueryExpression; + type: ValueType; +} +export type QueryPredicate = + | { kind: "and" | "or"; children: QueryPredicate[] } + | { kind: "not"; child: QueryPredicate } + | { + kind: "compare"; + expression: QueryExpression; + operator: "eq" | "in" | "isNull" | "lt" | "lte" | "gt" | "gte"; + value: QueryArgument; + } + | { + kind: "relation"; + path: QueryPathStep[]; + interfaceRevisionId: InterfaceRevisionId; + memberId: MemberId; + targetInterfaceRevisionId: InterfaceRevisionId; + operator: "some" | "none" | "is" | "isNull"; + predicate?: QueryPredicate; + value?: QueryArgument; + } + | { + kind: "reduce"; + path: QueryPathStep[]; + interfaceRevisionId: InterfaceRevisionId; + memberId: MemberId; + targetInterfaceRevisionId: InterfaceRevisionId; + where?: QueryPredicate; + having: QueryAggregatePredicate; + }; +export type QueryAggregatePredicate = + | { kind: "and" | "or"; children: QueryAggregatePredicate[] } + | { kind: "not"; child: QueryAggregatePredicate } + | { + kind: "compare"; + expression: QueryExpression | QueryReduction; + operator: "eq" | "in" | "isNull" | "lt" | "lte" | "gt" | "gte"; + value: QueryArgument; + }; +export interface QueryKey { + path: string[]; + expression: QueryExpression; +} +export interface QueryRelationalStage { + id: number; + input?: number; + row: QueryRow; + operation: + | { + kind: "source"; + interfaceRevisionId: InterfaceRevisionId; + memberId: MemberId; + targetInterfaceRevisionId: InterfaceRevisionId; + } + | { kind: "filter"; predicate: QueryPredicate } + | { + kind: "expand"; + path: QueryPathStep[]; + interfaceRevisionId: InterfaceRevisionId; + memberId: MemberId; + targetInterfaceRevisionId: InterfaceRevisionId; + } + | { kind: "distinct"; keys: QueryKey[] }; +} +export interface QueryRelationalTerminal { + stage: number; + path: string[]; + kind: "aggregate" | "groups"; + keys: QueryKey[]; + reductions: { path: string[]; reduction: QueryReduction }[]; + where?: QueryPredicate; + having?: QueryAggregatePredicate; + order: { expression: QueryExpression | QueryReduction; descending: boolean }[]; + first?: QueryArgument; + all?: QueryArgument; + after?: QueryArgument; + /** Projection tree remains separate from the operator plan. */ + selection: import("./types.js").QuerySelection[]; + residual: boolean; +} +export interface QueryRelationalPlan { + stages: QueryRelationalStage[]; + terminals: QueryRelationalTerminal[]; +} +export type QueryEffectRecorder = ( + id: InterfaceRevisionId, + name: string, + use: QueryUse, + node: import("graphql").ASTNode, +) => void; diff --git a/src/query/schema.ts b/src/query/schema.ts index 900a310..2d3c7d6 100644 --- a/src/query/schema.ts +++ b/src/query/schema.ts @@ -14,6 +14,7 @@ import { type GraphQLInputType, type GraphQLFieldConfigMap, type GraphQLInputFieldConfigMap, + type DocumentNode, } from "graphql"; import { createHash } from "node:crypto"; import type { @@ -23,6 +24,7 @@ import type { RelationshipInterfaceMember, } from "../capability-model/types.js"; import { QueryCompileError, type QueryDeclaration } from "./types.js"; +import { relationalSchema, objectRow } from "./relational-schema.js"; // Validate losslessly; callers carry decimal strings across JSON transports. function integerParser(name: string, min: bigint, max: bigint, value: unknown): string { @@ -67,19 +69,21 @@ export const cursorScalar = new GraphQLScalarType({ return value; }, }); -export const referenceScalar = new GraphQLScalarType({ name: "ManagedReference" }); -export function querySchema(declaration: QueryDeclaration, interfaces: readonly InterfaceRevision[]) { +export function querySchema( + declaration: QueryDeclaration, + interfaces: readonly InterfaceRevision[], + document?: DocumentNode, +) { const contracts = new Map(interfaces.map((entry) => [entry.revisionId, entry])); const objects = new Map(); - const filters = new Map(); const orders = new Map(); const byName = new Map(); const comparisons = new Map(); - const metadata = new GraphQLObjectType({ - name: "QxMetadata", - fields: { ref: { type: new GraphQLNonNull(referenceScalar) } }, - }); + const scalarTypes = new Map( + Object.entries(queryScalars).map(([name, type]) => [type.name, { kind: "scalar", name } as ValueType]), + ); + scalarTypes.set("Cursor", { kind: "scalar", name: "string" }); const pageInfo = new GraphQLObjectType({ name: "QxPageInfo", fields: { @@ -130,6 +134,8 @@ export function querySchema(declaration: QueryDeclaration, interfaces: readonly let result = comparisons.get(key); if (!result) { const fields: GraphQLInputFieldConfigMap = { eq: { type: value }, isNull: { type: GraphQLBoolean } }; + if (base.kind === "scalar" && base.name !== "bytes") + fields.in = { type: new GraphQLList(new GraphQLNonNull(value)) }; if (base.kind === "scalar" && base.name !== "bool" && base.name !== "bytes") for (const op of ["lt", "lte", "gt", "gte"]) fields[op] = { type: value }; result = new GraphQLInputObjectType({ name: `QxCompare${key}`, fields }); @@ -137,29 +143,12 @@ export function querySchema(declaration: QueryDeclaration, interfaces: readonly } return result; }; - const filter = (id: InterfaceRevisionId): GraphQLInputObjectType => { - const existing = filters.get(id); - if (existing) return existing; - const result = new GraphQLInputObjectType({ - name: `${name(id)}Where`, - fields: () => { - const fields: GraphQLInputFieldConfigMap = { - and: { type: new GraphQLList(new GraphQLNonNull(result)) }, - or: { type: new GraphQLList(new GraphQLNonNull(result)) }, - not: { type: result }, - }; - for (const member of contract(id).members) - if (member.kind === "value" && member.queryRead) { - if (member.displayName in fields) - throw new QueryCompileError("QUERY_SCHEMA_NAME", `Reserved predicate name ${member.displayName}`); - fields[member.displayName] = { type: comparison(member.valueType) }; - } - return fields; - }, - }); - filters.set(id, result); - return result; - }; + const relational = relationalSchema( + { contracts, target, name, scalar, comparison, scalarTypes, direction, cursor: cursorScalar, pageInfo }, + document, + ); + relational.discover(declaration.root); + const filter = (id: InterfaceRevisionId) => relational.where(objectRow(id)); const order = (id: InterfaceRevisionId) => { let result = orders.get(id); if (!result) { @@ -181,6 +170,14 @@ export function querySchema(declaration: QueryDeclaration, interfaces: readonly const result = new GraphQLObjectType({ name: name(id), fields: () => { + const helpers = relational.helpers(id); + const metadata = new GraphQLObjectType({ + name: `QxMetadata_${name(id)}`, + fields: { + ref: { type: new GraphQLNonNull(relational.refs(id)) }, + ...(helpers ? { relations: { type: new GraphQLNonNull(helpers) } } : {}), + }, + }); const fields: GraphQLFieldConfigMap = { _qx: { type: new GraphQLNonNull(metadata) } }; for (const member of contract(id).members) { if (member.kind === "operation" || !member.queryRead) continue; @@ -249,5 +246,5 @@ export function querySchema(declaration: QueryDeclaration, interfaces: readonly fields: { root: { type: new GraphQLNonNull(object(declaration.root)) } }, }), }); - return { schema, byName, target, contracts }; + return { schema, byName, target, contracts, relational, scalarTypes }; } diff --git a/src/query/types.ts b/src/query/types.ts index 9d5768b..3f01b79 100644 --- a/src/query/types.ts +++ b/src/query/types.ts @@ -6,7 +6,7 @@ import type { ClosedTypeArgument, } from "../capability-model/generics.js"; -export type QueryUse = "select" | "predicate" | "order"; +export type QueryUse = "select" | "predicate" | "order" | "aggregate" | "group" | "distinct"; export interface QueryAllowance { interfaceRevisionId: InterfaceRevisionId; memberId: MemberId; @@ -106,4 +106,6 @@ export interface QuerySelection { conditions: { include: boolean; value: QueryArgument }[]; arguments: Record; selection: QuerySelection[]; + relational?: import("./relational.js").QueryRelationalPlan; + predicate?: import("./relational.js").QueryPredicate; } diff --git a/test/fixtures/query-aggregation.ts b/test/fixtures/query-aggregation.ts new file mode 100644 index 0000000..fee26fe --- /dev/null +++ b/test/fixtures/query-aggregation.ts @@ -0,0 +1,68 @@ +import assert from "node:assert/strict"; +import { compileCapabilityResourceSource } from "../../src/capability-language/index.js"; +import type { InterfaceRevision } from "../../src/capability-model/types.js"; +import { compileQuery } from "../../src/query/compile.js"; + +const source = { repository: "https://example.test/aggregation.git", commit: "a".repeat(40) }; +const interfaces: InterfaceRevision[] = []; +function resource(text: string) { + const result = compileCapabilityResourceSource(`external atom TaskObject id "task";\n${text}`, { + source, + environment: { + interfaces: new Map(interfaces.map((i) => [i.displayName, i])), + interfaceClosure: interfaces, + }, + }); + assert.ok(result.ok, JSON.stringify(result.diagnostics)); + if (result.resource.kind === "interface") interfaces.push(result.resource.revision); + return result.resource; +} +resource(`interface Person id "person" revision "person@1" { + queryable value name id "name" : string { get id "name:get"; } +}`); +resource(`interface Tag id "tag" revision "tag@1" { + queryable value color id "color" : string { get id "color:get"; } + queryable relation tasks id "tag-tasks" : many atom TaskObject { resolve id "tag-tasks:resolve"; } +}`); +resource(`import interface Tag; import interface Person; +interface Task id "task" revision "task@1" { + queryable value estimatedHours id "hours" : optional { get id "hours:get"; } + queryable value cost id "cost" : int64 { get id "cost:get"; } + queryable rpc value score id "score" : optional { get id "score:get"; } + queryable relation tags id "tags" : many-unique interface Tag { resolve id "tags:resolve"; } + queryable relation assignee id "assignee" : optional-one interface Person { resolve id "assignee:resolve"; } +}`); +resource(`import interface Task; +interface Tasks id "tasks" revision "tasks@1" { + queryable relation tasks id "tasks" : many-unique interface Task { resolve id "tasks:resolve"; } + queryable relation copies id "copies" : many interface Task ordered { resolve id "copies:resolve"; } + queryable relation slots id "slots" : many interface Task keyed "int64" { resolve id "slots:resolve"; } +}`); +const pkg = resource(`import interface Tasks; import interface Task; +package Reports id "reports" revision "reports@1" { + query Report id "report" root Tasks document "report.graphql" operation "Report" { + max rows 50; + view TaskObject as Task; + allow Task.score aggregate "Bounded aggregation fixture"; + allow Task.score predicate "Bounded aggregation fixture"; + allow Task.score order "Bounded aggregation fixture"; + allow Task.score group "Bounded aggregation fixture"; + allow Task.score distinct "Bounded aggregation fixture"; + } +}`); +if (pkg.kind !== "package") throw new Error("package expected"); +const declaration = pkg.revision.queries![0]!; +export const compileAggregationDocument = (document: string) => + compileQuery(declaration, interfaces, async () => document); +export const aggregationBindingSchema = (checked: Awaited>) => ({ + format: "quixos-bindings" as const, + version: 1 as const, + interfaces, + packages: [{ ...pkg.revision, checkedQueries: [checked] }], +}); +export const compileAggregation = (body: string, variables = "") => + compileQuery( + declaration, + interfaces, + async () => `query Report${variables} { root { _qx { relations { tasks { ${body} } } } } }`, + ); diff --git a/test/fixtures/query-workspace.ts b/test/fixtures/query-workspace.ts index fe63e9d..3582329 100644 --- a/test/fixtures/query-workspace.ts +++ b/test/fixtures/query-workspace.ts @@ -55,6 +55,13 @@ export async function queryWorkspaceFixture() { allow TaskFacts.score predicate "Bounded local collection"; allow TaskFacts.score order "Bounded local collection"; } + query Totals id "totals" root Collection document "totals.graphql" operation "Totals" { + max rows 30; watch; + } + query ScoreTotals id "score-totals" root Collection document "score-totals.graphql" operation "ScoreTotals" { + max rows 30; max candidates 60; + allow TaskFacts.score aggregate "Complete bounded collection report"; + } }`; sources.Queries = packageSource; const pkgResult = compileCapabilityResourceSource(packageSource, { @@ -70,6 +77,8 @@ export async function queryWorkspaceFixture() { "row.graphql": `fragment TaskRow on TaskFacts {title rank done assignee {name}}`, "enriched.graphql": `query Enriched {root {items(first: 3) {entries {key node {_qx {ref} title score}}}}}`, "ranked.graphql": `query Ranked {root {items(first: 3, where: {score: {gt: 0}}, orderBy: [{score: DESC}]) {entries {key node {_qx {ref} title}}}}}`, + "totals.graphql": `query Totals {root {_qx {relations {items {aggregate {count sum {rank}} groups(by: {done: true}, first: 10) {entries {group {done} aggregate {count sum {rank}}}}}}}}}`, + "score-totals.graphql": `query ScoreTotals {root {_qx {relations {items {aggregate {count sum {score}}}}}}}`, }; pkg.checkedQueries = await Promise.all( pkg.queries!.map((query) => compileQuery(query, allInterfaces, async (name) => documents[name]!)), diff --git a/test/query-aggregation.test.ts b/test/query-aggregation.test.ts new file mode 100644 index 0000000..142f503 --- /dev/null +++ b/test/query-aggregation.test.ts @@ -0,0 +1,110 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { compileAggregation as compile, compileAggregationDocument } from "./fixtures/query-aggregation.js"; +import type { QuerySelection } from "../src/query/types.js"; +import type { QueryRelationalPlan } from "../src/query/relational.js"; + +function findPlan(selections: QuerySelection[]): QueryRelationalPlan | undefined { + for (const selection of selections) { + const plan = selection.relational ?? findPlan(selection.selection); + if (plan) return plan; + } +} + +test("aggregate output uses exact count and sum types and nullable reductions", async () => { + const checked = await compile( + `aggregate { count countPresent { estimatedHours } sum { estimatedHours cost } avg { cost } }`, + ); + const json = JSON.stringify(checked.output); + assert.match(json, /"count":\{"kind":"scalar","name":"uint64"\}/); + assert.match(json, /"cost":\{"kind":"optional","value":\{"kind":"scalar","name":"int64"\}\}/); + assert.ok(checked.effects.some((e) => e.memberId === "hours" && e.uses.includes("aggregate"))); + const plan = findPlan(checked.selection); + assert.ok(plan); + assert.equal(plan.terminals[0]!.reductions.length, 5); +}); + +test("explicit expansion and identity distinct retain checked grouped operands", async () => { + const checked = await compile(`expand { tags { + distinct(by: {source: {_qx: {ref: true}}, target: {color: true}}) { + groups(by: {target: {color: true}}, first: 50, + having: {sum: {source: {estimatedHours: {gt: 0}}}}, + orderBy: [{sum: {source: {estimatedHours: DESC}}}]) { + entries { group {target {color}} aggregate {sum {source {estimatedHours}}} } + pageInfo {hasNextPage endCursor} + } + } + } }`); + const plan = findPlan(checked.selection)!; + assert.deepEqual( + plan.stages.map((s) => s.operation.kind), + ["source", "expand", "distinct"], + ); + assert.equal(plan.terminals[0]!.kind, "groups"); + assert.ok( + checked.effects.some( + (e) => e.memberId === "hours" && ["aggregate", "predicate", "order"].every((u) => e.uses.includes(u as never)), + ), + ); +}); + +test("distinct and grouping reject fields not determined by their selected keys", async () => { + await assert.rejects(compile(`distinct(by: {cost: true}) {aggregate {sum {estimatedHours}}}`), /dropped by distinct/); + await assert.rejects(compile(`groups(by: {cost: true}, first: 10) {entries {group {estimatedHours}}}`), /group key/); +}); + +test("relationship existence predicates compile to typed plans", async () => { + const checked = await compile( + `filter(where: {_qx: {relations: {tags: {some: {color: {in: ["red","blue"]}}}}}}) {aggregate {count}}`, + ); + const plan = findPlan(checked.selection)!; + assert.equal(plan.stages[1]!.operation.kind, "filter"); + assert.ok(checked.effects.some((e) => e.memberId === "color" && e.uses.includes("predicate"))); +}); + +test("group structure is literal, bounded and cannot project a representative field", async () => { + await assert.rejects(compile(`groups(by: {}, first: 2) {entries {aggregate {count}}}`), /key|empty/i); + await assert.rejects(compile(`groups(by: {cost: false}, first: 2) {entries {aggregate {count}}}`), /true/); + await assert.rejects(compile(`groups(by: {cost: true}) {entries {aggregate {count}}}`), /first or all/); + await assert.rejects( + compile( + `groups(by: {cost: true}, first: 2, orderBy: [{sum: {cost: ASC, estimatedHours: DESC}}]) {entries {aggregate {count}}}`, + ), + /exactly one field/, + ); + await assert.rejects(compile(`expand {tags {aggregate {sum {target {color}}}}}`), /QUERY_VALIDATION/); +}); + +test("RPC continuation rejection is local to the affected membership boundary", async () => { + await assert.rejects( + compile(`groups(by: {cost: true}, first: 2, after: null) {entries {aggregate {sum {score}}}}`), + /without continuation/, + ); + await assert.rejects( + compileAggregationDocument( + `query Report {root {tasks(first: 2, after: null, where: {score: {gt: 0}}) {entries {node {cost}}}}}`, + ), + /without continuation/, + ); + const checked = await compileAggregationDocument(`query Report($after: Cursor) {root { + tasks(first: 2, after: $after) {entries {node {cost}}} + _qx {relations {tasks {aggregate {sum {score}}}}} + }}`); + assert.equal(findPlan(checked.selection)!.terminals[0]!.residual, true); +}); + +test("typed reference operands and scalar membership tests reject unbounded literal lists", async () => { + const checked = await compile( + `filter(where: {_qx: {ref: {in: $tasks}}}) {aggregate {count}}`, + "($tasks: [QxRef_Task!]!)", + ); + assert.match(JSON.stringify(checked.variables), /object-ref/); + await assert.rejects( + compile(`filter(where: {_qx: {ref: {eq: "obj:made-up"}}}) {aggregate {count}}`), + /typed variables/, + ); + await assert.rejects( + compile(`filter(where: {cost: {in: [${Array(1001).fill("1").join(",")}]}}) {aggregate {count}}`), + /1000 operands/, + ); +}); diff --git a/test/query-workspace.test.ts b/test/query-workspace.test.ts index d30b120..bdbf8f4 100644 --- a/test/query-workspace.test.ts +++ b/test/query-workspace.test.ts @@ -17,4 +17,13 @@ test("query fixture links generic collections, both implementations, and native assert.equal(title.kind, "value"); delete title.queryRead; assert.throws(() => linkQueries(broken), /changed/); + const aggregateBroken = structuredClone(workspace); + const rank = aggregateBroken.interfaceImports + .find((i) => i.displayName === "TaskFacts")! + .members.find((m) => m.id === "rank")!; + assert.equal(rank.kind, "value"); + delete rank.queryRead; + assert.throws(() => linkQueries(aggregateBroken), /changed/); + const totals = workspace.linkedQueries.find((q) => q.id.endsWith(":totals"))!; + assert.equal(totals.fields.filter((f) => f.memberId === "rank").length, 2); });