From 3728612e8cef7d799d84c18543328d2f547a2060 Mon Sep 17 00:00:00 2001 From: "Timothy J. Aveni" Date: Sat, 26 Sep 2026 11:16:46 -0700 Subject: [PATCH] Implement implicit object references and capability query foundations --- proto/camino/schema.proto | 5 + src/execution-world/query-operators.ts | 1 + src/gen/camino/schema_pb.ts | 64 +++++++--- src/query/compile.ts | 161 ++++++++++++++++++++++--- src/query/link.ts | 12 ++ src/query/relational-compile.ts | 30 +++++ src/query/relational-proto.ts | 7 ++ src/query/relational-schema.ts | 39 +++++- src/query/relational.ts | 1 + src/query/schema.ts | 14 +++ test/controller-registry.test.ts | 2 +- test/query-compiler.test.ts | 89 +++++++++++++- test/query-structured.test.ts | 11 ++ 13 files changed, 401 insertions(+), 35 deletions(-) diff --git a/proto/camino/schema.proto b/proto/camino/schema.proto index 7b4cef9..1e2e123 100644 --- a/proto/camino/schema.proto +++ b/proto/camino/schema.proto @@ -196,6 +196,10 @@ message QueryReductionPredicate { QueryPredicate where = 3; QueryPredicate having = 4; } +message QueryInterfacePredicate { + string interface_revision_id = 1; + QueryPredicate predicate = 2; +} message QueryPredicate { oneof predicate { QueryPredicateList and = 1; @@ -204,6 +208,7 @@ message QueryPredicate { QueryComparison compare = 4; QueryRelationPredicate relation = 5; QueryReductionPredicate reduce = 6; + QueryInterfacePredicate as = 7; } } message QueryRow { diff --git a/src/execution-world/query-operators.ts b/src/execution-world/query-operators.ts index 713eec9..4682306 100644 --- a/src/execution-world/query-operators.ts +++ b/src/execution-world/query-operators.ts @@ -10,6 +10,7 @@ export function unsupportedQueryOperator(query: CheckedQuery): string | undefine if (value.kind === "reduce") return "aggregate predicates"; if (value.kind === "and" || value.kind === "or") return value.children.map(predicate).find(Boolean); if (value.kind === "not") return predicate(value.child); + if (value.kind === "as") return predicate(value.predicate); if (value.kind === "compare") return path(value.expression.path); if (value.kind === "relation") return path(value.path) ?? (value.predicate && predicate(value.predicate)); return undefined; diff --git a/src/gen/camino/schema_pb.ts b/src/gen/camino/schema_pb.ts index 3c05ed7..c239c35 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("ChNjYW1pbm8vc2NoZW1hLnByb3RvEgZjYW1pbm8iNwoOQXRvbURlZmluaXRpb24SDwoHYXRvbV9pZBgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkiWQoPQXRvbUNvbmZvcm1hbmNlEg8KB2F0b21faWQYASABKAkSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAIgASgJEhYKDmNvbmZvcm1hbmNlX2lkGAMgASgJIsIBCg9TdGF0ZUF0dGFjaG1lbnQSDwoHc2xvdF9pZBgBIAEoCRIYChBhdHRhY2hlZF9hdG9tX2lkGAIgASgJEhQKDGRpc3BsYXlfbmFtZRgDIAEoCRIXCg92YWx1ZV90eXBlX2pzb24YBCABKAkSGwoTc3RvcmFnZV9wb2xpY3lfanNvbhgFIAEoCRIaChJkZWZhdWx0X3ZhbHVlX2pzb24YBiABKAkSHAoUb3duZXJfY29uZm9ybWFuY2VfaWQYByABKAkiUAoSRW5kcG9pbnRDb25zdHJhaW50EhEKB2F0b21faWQYASABKAlIABIfChVpbnRlcmZhY2VfcmV2aXNpb25faWQYAiABKAlIAEIGCgRraW5kIvsBCgxFZGdlRW5kcG9pbnQSFQoNcHJvamVjdGlvbl9pZBgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkSLgoKY29uc3RyYWludBgDIAEoCzIaLmNhbWluby5FbmRwb2ludENvbnN0cmFpbnQSKAoLY2FyZGluYWxpdHkYBCABKA4yEy5jYW1pbm8uQ2FyZGluYWxpdHkSDwoHb3JkZXJlZBgFIAEoCBIRCglvbl9kZWxldGUYBiABKAkSFAoMcmV0YWluX290aGVyGAcgASgIEhAKCGtleV90eXBlGAggASgJEhgKEHB1YmxpY190cmF2ZXJzYWwYCSABKAgipQEKDkVkZ2VBdHRhY2htZW50EhQKDGVkZ2VfdHlwZV9pZBgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkSIwoFZmlyc3QYAyABKAsyFC5jYW1pbm8uRWRnZUVuZHBvaW50EiQKBnNlY29uZBgEIAEoCzIULmNhbWluby5FZGdlRW5kcG9pbnQSHAoUb3duZXJfY29uZm9ybWFuY2VfaWQYBSABKAkilQIKD1BlcnNpc3RlbmNlUGxhbhIUCgx3b3Jrc3BhY2VfaWQYASABKAkSHQoVd29ya3NwYWNlX3JldmlzaW9uX2lkGAIgASgJEiUKBWF0b21zGAMgAygLMhYuY2FtaW5vLkF0b21EZWZpbml0aW9uEi0KDGNvbmZvcm1hbmNlcxgEIAMoCzIXLmNhbWluby5BdG9tQ29uZm9ybWFuY2USJwoGc3RhdGVzGAUgAygLMhcuY2FtaW5vLlN0YXRlQXR0YWNobWVudBIlCgVlZGdlcxgGIAMoCzIWLmNhbWluby5FZGdlQXR0YWNobWVudBInCgdxdWVyaWVzGAcgAygLMhYuY2FtaW5vLkluc3RhbGxlZFF1ZXJ5Ip4BCg1RdWVyeUFyZ3VtZW50EhIKCHZhcmlhYmxlGAEgASgJSAASFgoMbGl0ZXJhbF9qc29uGAIgASgJSAASKQoEbGlzdBgDIAEoCzIZLmNhbWluby5RdWVyeUFyZ3VtZW50TGlzdEgAEi0KBm9iamVjdBgEIAEoCzIbLmNhbWluby5RdWVyeUFyZ3VtZW50T2JqZWN0SABCBwoFdmFsdWUiOgoRUXVlcnlBcmd1bWVudExpc3QSJQoGdmFsdWVzGAEgAygLMhUuY2FtaW5vLlF1ZXJ5QXJndW1lbnQilAEKE1F1ZXJ5QXJndW1lbnRPYmplY3QSNwoGZmllbGRzGAEgAygLMicuY2FtaW5vLlF1ZXJ5QXJndW1lbnRPYmplY3QuRmllbGRzRW50cnkaRAoLRmllbGRzRW50cnkSCwoDa2V5GAEgASgJEiQKBXZhbHVlGAIgASgLMhUuY2FtaW5vLlF1ZXJ5QXJndW1lbnQ6AjgBIkcKDlF1ZXJ5Q29uZGl0aW9uEg8KB2luY2x1ZGUYASABKAgSJAoFdmFsdWUYAiABKAsyFS5jYW1pbm8uUXVlcnlBcmd1bWVudCLUAwoOUXVlcnlTZWxlY3Rpb24SDAoEbmFtZRgBIAEoCRILCgNrZXkYAiABKAkSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAMgASgJEhEKCW1lbWJlcl9pZBgEIAEoCRIkChx0YXJnZXRfaW50ZXJmYWNlX3JldmlzaW9uX2lkGAUgASgJEioKCmNvbmRpdGlvbnMYBiADKAsyFi5jYW1pbm8uUXVlcnlDb25kaXRpb24SOAoJYXJndW1lbnRzGAcgAygLMiUuY2FtaW5vLlF1ZXJ5U2VsZWN0aW9uLkFyZ3VtZW50c0VudHJ5EikKCXNlbGVjdGlvbhgIIAMoCzIWLmNhbWluby5RdWVyeVNlbGVjdGlvbhIvCgpyZWxhdGlvbmFsGAkgASgLMhsuY2FtaW5vLlF1ZXJ5UmVsYXRpb25hbFBsYW4SKQoJcHJlZGljYXRlGAogASgLMhYuY2FtaW5vLlF1ZXJ5UHJlZGljYXRlEhkKEWxpdmVfc2VsZWN0aW9uX2lkGAsgASgJGkcKDkFyZ3VtZW50c0VudHJ5EgsKA2tleRgBIAEoCRIkCgV2YWx1ZRgCIAEoCzIVLmNhbWluby5RdWVyeUFyZ3VtZW50OgI4ASJqCg1RdWVyeVBhdGhTdGVwEhAKBnNvdXJjZRgBIAEoCEgAEhAKBnRhcmdldBgCIAEoCEgAEi0KCHJlbGF0aW9uGAMgASgLMhkuY2FtaW5vLlF1ZXJ5UmVsYXRpb25QYXRoSABCBgoEc3RlcCJ9ChFRdWVyeVJlbGF0aW9uUGF0aBIdChVpbnRlcmZhY2VfcmV2aXNpb25faWQYASABKAkSEQoJbWVtYmVyX2lkGAIgASgJEiQKHHRhcmdldF9pbnRlcmZhY2VfcmV2aXNpb25faWQYAyABKAkSEAoIb3B0aW9uYWwYBCABKAgiWQoRUXVlcnlGaWVsZE9wZXJhbmQSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAEgASgJEhEKCW1lbWJlcl9pZBgCIAEoCRISCgp2YWx1ZV9wYXRoGAMgAygJIrYBCg9RdWVyeUV4cHJlc3Npb24SIwoEcGF0aBgBIAMoCzIVLmNhbWluby5RdWVyeVBhdGhTdGVwEioKBWZpZWxkGAIgASgLMhkuY2FtaW5vLlF1ZXJ5RmllbGRPcGVyYW5kSAASDQoDcmVmGAMgASgJSAASDwoFZW50cnkYBCABKAhIABIRCgdtYXBfa2V5GAUgASgISAASFwoPdmFsdWVfdHlwZV9qc29uGAYgASgJQgYKBGxlYWYihQEKDlF1ZXJ5UmVkdWN0aW9uEjAKCG9wZXJhdG9yGAEgASgOMh4uY2FtaW5vLlF1ZXJ5QWdncmVnYXRlT3BlcmF0b3ISKAoHb3BlcmFuZBgCIAEoCzIXLmNhbWluby5RdWVyeUV4cHJlc3Npb24SFwoPdmFsdWVfdHlwZV9qc29uGAMgASgJInUKDFF1ZXJ5T3BlcmFuZBItCgpleHByZXNzaW9uGAEgASgLMhcuY2FtaW5vLlF1ZXJ5RXhwcmVzc2lvbkgAEisKCXJlZHVjdGlvbhgCIAEoCzIWLmNhbWluby5RdWVyeVJlZHVjdGlvbkgAQgkKB29wZXJhbmQikQEKD1F1ZXJ5Q29tcGFyaXNvbhIlCgdvcGVyYW5kGAEgASgLMhQuY2FtaW5vLlF1ZXJ5T3BlcmFuZBIxCghvcGVyYXRvchgCIAEoDjIfLmNhbWluby5RdWVyeUNvbXBhcmlzb25PcGVyYXRvchIkCgV2YWx1ZRgDIAEoCzIVLmNhbWluby5RdWVyeUFyZ3VtZW50Ij4KElF1ZXJ5UHJlZGljYXRlTGlzdBIoCghjaGlsZHJlbhgBIAMoCzIWLmNhbWluby5RdWVyeVByZWRpY2F0ZSL1AQoWUXVlcnlSZWxhdGlvblByZWRpY2F0ZRIjCgRwYXRoGAEgAygLMhUuY2FtaW5vLlF1ZXJ5UGF0aFN0ZXASKwoIcmVsYXRpb24YAiABKAsyGS5jYW1pbm8uUXVlcnlSZWxhdGlvblBhdGgSOAoIb3BlcmF0b3IYAyABKA4yJi5jYW1pbm8uUXVlcnlSZWxhdGlvblByZWRpY2F0ZU9wZXJhdG9yEikKCXByZWRpY2F0ZRgEIAEoCzIWLmNhbWluby5RdWVyeVByZWRpY2F0ZRIkCgV2YWx1ZRgFIAEoCzIVLmNhbWluby5RdWVyeUFyZ3VtZW50IroBChdRdWVyeVJlZHVjdGlvblByZWRpY2F0ZRIjCgRwYXRoGAEgAygLMhUuY2FtaW5vLlF1ZXJ5UGF0aFN0ZXASKwoIcmVsYXRpb24YAiABKAsyGS5jYW1pbm8uUXVlcnlSZWxhdGlvblBhdGgSJQoFd2hlcmUYAyABKAsyFi5jYW1pbm8uUXVlcnlQcmVkaWNhdGUSJgoGaGF2aW5nGAQgASgLMhYuY2FtaW5vLlF1ZXJ5UHJlZGljYXRlIqwCCg5RdWVyeVByZWRpY2F0ZRIpCgNhbmQYASABKAsyGi5jYW1pbm8uUXVlcnlQcmVkaWNhdGVMaXN0SAASKAoCb3IYAiABKAsyGi5jYW1pbm8uUXVlcnlQcmVkaWNhdGVMaXN0SAASJQoDbm90GAMgASgLMhYuY2FtaW5vLlF1ZXJ5UHJlZGljYXRlSAASKgoHY29tcGFyZRgEIAEoCzIXLmNhbWluby5RdWVyeUNvbXBhcmlzb25IABIyCghyZWxhdGlvbhgFIAEoCzIeLmNhbWluby5RdWVyeVJlbGF0aW9uUHJlZGljYXRlSAASMQoGcmVkdWNlGAYgASgLMh8uY2FtaW5vLlF1ZXJ5UmVkdWN0aW9uUHJlZGljYXRlSABCCwoJcHJlZGljYXRlImEKCFF1ZXJ5Um93EigKBm9iamVjdBgBIAEoCzIWLmNhbWluby5RdWVyeU9iamVjdFJvd0gAEiQKBHBhaXIYAiABKAsyFC5jYW1pbm8uUXVlcnlQYWlyUm93SABCBQoDcm93IlUKDlF1ZXJ5T2JqZWN0Um93Eh0KFWludGVyZmFjZV9yZXZpc2lvbl9pZBgBIAEoCRISCgptZW1iZXJzaGlwGAIgASgIEhAKCGtleV90eXBlGAMgASgJIlIKDFF1ZXJ5UGFpclJvdxIgCgZzb3VyY2UYASABKAsyEC5jYW1pbm8uUXVlcnlSb3cSIAoGdGFyZ2V0GAIgASgLMhAuY2FtaW5vLlF1ZXJ5Um93IkUKCFF1ZXJ5S2V5EgwKBHBhdGgYASADKAkSKwoKZXhwcmVzc2lvbhgCIAEoCzIXLmNhbWluby5RdWVyeUV4cHJlc3Npb24iLwoNUXVlcnlEaXN0aW5jdBIeCgRrZXlzGAEgAygLMhAuY2FtaW5vLlF1ZXJ5S2V5ImIKDlF1ZXJ5RXhwYW5zaW9uEiMKBHBhdGgYASADKAsyFS5jYW1pbm8uUXVlcnlQYXRoU3RlcBIrCghyZWxhdGlvbhgCIAEoCzIZLmNhbWluby5RdWVyeVJlbGF0aW9uUGF0aCKYAgoUUXVlcnlSZWxhdGlvbmFsU3RhZ2USCgoCaWQYASABKA0SEgoFaW5wdXQYAiABKA1IAYgBARIdCgNyb3cYAyABKAsyEC5jYW1pbm8uUXVlcnlSb3cSKwoGc291cmNlGAQgASgLMhkuY2FtaW5vLlF1ZXJ5UmVsYXRpb25QYXRoSAASKAoGZmlsdGVyGAUgASgLMhYuY2FtaW5vLlF1ZXJ5UHJlZGljYXRlSAASKAoGZXhwYW5kGAYgASgLMhYuY2FtaW5vLlF1ZXJ5RXhwYW5zaW9uSAASKQoIZGlzdGluY3QYByABKAsyFS5jYW1pbm8uUXVlcnlEaXN0aW5jdEgAQgsKCW9wZXJhdGlvbkIICgZfaW5wdXQiUwoYUXVlcnlSZWR1Y3Rpb25Qcm9qZWN0aW9uEgwKBHBhdGgYASADKAkSKQoJcmVkdWN0aW9uGAIgASgLMhYuY2FtaW5vLlF1ZXJ5UmVkdWN0aW9uIlAKE1F1ZXJ5QWdncmVnYXRlT3JkZXISJQoHb3BlcmFuZBgBIAEoCzIULmNhbWluby5RdWVyeU9wZXJhbmQSEgoKZGVzY2VuZGluZxgCIAEoCCLSAwoXUXVlcnlSZWxhdGlvbmFsVGVybWluYWwSDQoFc3RhZ2UYASABKA0SDAoEcGF0aBgCIAMoCRIOCgZncm91cHMYAyABKAgSHgoEa2V5cxgEIAMoCzIQLmNhbWluby5RdWVyeUtleRI0CgpyZWR1Y3Rpb25zGAUgAygLMiAuY2FtaW5vLlF1ZXJ5UmVkdWN0aW9uUHJvamVjdGlvbhIlCgV3aGVyZRgGIAEoCzIWLmNhbWluby5RdWVyeVByZWRpY2F0ZRImCgZoYXZpbmcYByABKAsyFi5jYW1pbm8uUXVlcnlQcmVkaWNhdGUSKgoFb3JkZXIYCCADKAsyGy5jYW1pbm8uUXVlcnlBZ2dyZWdhdGVPcmRlchIkCgVmaXJzdBgJIAEoCzIVLmNhbWluby5RdWVyeUFyZ3VtZW50EiIKA2FsbBgKIAEoCzIVLmNhbWluby5RdWVyeUFyZ3VtZW50EiQKBWFmdGVyGAsgASgLMhUuY2FtaW5vLlF1ZXJ5QXJndW1lbnQSKQoJc2VsZWN0aW9uGAwgAygLMhYuY2FtaW5vLlF1ZXJ5U2VsZWN0aW9uEhAKCHJlc2lkdWFsGA0gASgIEgwKBHJvd3MYDiABKAgidwoTUXVlcnlSZWxhdGlvbmFsUGxhbhIsCgZzdGFnZXMYASADKAsyHC5jYW1pbm8uUXVlcnlSZWxhdGlvbmFsU3RhZ2USMgoJdGVybWluYWxzGAIgAygLMh8uY2FtaW5vLlF1ZXJ5UmVsYXRpb25hbFRlcm1pbmFsIukCChBRdWVyeVJlYWRCaW5kaW5nEg8KB2F0b21faWQYASABKAkSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAIgASgJEhEKCW1lbWJlcl9pZBgDIAEoCRIbChNnZXR0ZXJfb3BlcmF0aW9uX2lkGAQgASgJEg8KB3Nsb3RfaWQYBSABKAkSFAoMZWRnZV90eXBlX2lkGAYgASgJEhUKDXByb2plY3Rpb25faWQYByABKAkSCwoDcnBjGAggASgIEiAKGHdhdGNoX3N0YXJ0X29wZXJhdGlvbl9pZBgJIAEoCRIfChd3YXRjaF9zdG9wX29wZXJhdGlvbl9pZBgKIAEoCRISCgpmaWVsZF9uYW1lGAsgASgJEhcKD3ZhbHVlX3R5cGVfanNvbhgMIAEoCRIoCgtjYXJkaW5hbGl0eRgNIAEoDjITLmNhbWluby5DYXJkaW5hbGl0eRIQCghrZXlfdHlwZRgOIAEoCSKvAQoSUXVlcnlGaWVsZENvbnRyYWN0Eh0KFWludGVyZmFjZV9yZXZpc2lvbl9pZBgBIAEoCRIRCgltZW1iZXJfaWQYAiABKAkSEgoKZmllbGRfbmFtZRgDIAEoCRIXCg92YWx1ZV90eXBlX2pzb24YBCABKAkSKAoLY2FyZGluYWxpdHkYBSABKA4yEy5jYW1pbm8uQ2FyZGluYWxpdHkSEAoIa2V5X3R5cGUYBiABKAkikgEKDFF1ZXJ5QnVkZ2V0cxIMCgRyb3dzGAEgASgNEg0KBWRlcHRoGAIgASgNEhQKDHJlc3VsdF9ieXRlcxgDIAEoDRISCgpjYW5kaWRhdGVzGAQgASgNEhEKCXJwY19jYWxscxgFIAEoDRITCgtjb25jdXJyZW5jeRgGIAEoDRITCgtkZWFkbGluZV9tcxgHIAEoDSJMCg5RdWVyeVBsYWNlbWVudBIRCgdyZXBsaWNhGAEgASgISAASHwoVbG9jYWxfc291cmNlX3F1ZXJ5X2lkGAIgASgJSABCBgoEa2luZCKaBQoOSW5zdGFsbGVkUXVlcnkSCgoCaWQYASABKAkSGQoRZGVmaW5pdGlvbl9kaWdlc3QYAiABKAkSFgoOYmluZGluZ19kaWdlc3QYAyABKAkSGgoQb2JqZWN0X2ludGVyZmFjZRgEIAEoCUgAEh0KE3dvcmtzcGFjZV9pbnRlcmZhY2UYDiABKAlIABIpCglzZWxlY3Rpb24YBSADKAsyFi5jYW1pbm8uUXVlcnlTZWxlY3Rpb24SKgoIYmluZGluZ3MYBiADKAsyGC5jYW1pbm8uUXVlcnlSZWFkQmluZGluZxIlCgdidWRnZXRzGAcgASgLMhQuY2FtaW5vLlF1ZXJ5QnVkZ2V0cxIbChN2YXJpYWJsZXNfdHlwZV9qc29uGAggASgJEhgKEG91dHB1dF90eXBlX2pzb24YCSABKAkSRwoRdmFyaWFibGVfZGVmYXVsdHMYCiADKAsyLC5jYW1pbm8uSW5zdGFsbGVkUXVlcnkuVmFyaWFibGVEZWZhdWx0c0VudHJ5Eg0KBXdhdGNoGAsgASgIEhsKE3BvbGxpbmdfaW50ZXJ2YWxfbXMYDCABKA0SHgoWcnBjX3ByZWRpY2F0ZV9vcl9vcmRlchgNIAEoCBIVCg1yb290X2F0b21faWRzGA8gAygJEioKBmZpZWxkcxgQIAMoCzIaLmNhbWluby5RdWVyeUZpZWxkQ29udHJhY3QSKQoJcGxhY2VtZW50GBEgASgLMhYuY2FtaW5vLlF1ZXJ5UGxhY2VtZW50Gk4KFVZhcmlhYmxlRGVmYXVsdHNFbnRyeRILCgNrZXkYASABKAkSJAoFdmFsdWUYAiABKAsyFS5jYW1pbm8uUXVlcnlBcmd1bWVudDoCOAFCBgoEcm9vdCpoCgtDYXJkaW5hbGl0eRIbChdDQVJESU5BTElUWV9VTlNQRUNJRklFRBAAEhAKDE9QVElPTkFMX09ORRABEg8KC0VYQUNUTFlfT05FEAISCAoETUFOWRADEg8KC01BTllfVU5JUVVFEAQqmgIKFlF1ZXJ5QWdncmVnYXRlT3BlcmF0b3ISKAokUVVFUllfQUdHUkVHQVRFX09QRVJBVE9SX1VOU1BFQ0lGSUVEEAASIgoeUVVFUllfQUdHUkVHQVRFX09QRVJBVE9SX0NPVU5UEAESKgomUVVFUllfQUdHUkVHQVRFX09QRVJBVE9SX0NPVU5UX1BSRVNFTlQQAhIgChxRVUVSWV9BR0dSRUdBVEVfT1BFUkFUT1JfU1VNEAMSIAocUVVFUllfQUdHUkVHQVRFX09QRVJBVE9SX0FWRxAEEiAKHFFVRVJZX0FHR1JFR0FURV9PUEVSQVRPUl9NSU4QBRIgChxRVUVSWV9BR0dSRUdBVEVfT1BFUkFUT1JfTUFYEAYq7QIKF1F1ZXJ5Q29tcGFyaXNvbk9wZXJhdG9yEikKJVFVRVJZX0NPTVBBUklTT05fT1BFUkFUT1JfVU5TUEVDSUZJRUQQABIgChxRVUVSWV9DT01QQVJJU09OX09QRVJBVE9SX0VREAESIAocUVVFUllfQ09NUEFSSVNPTl9PUEVSQVRPUl9JThACEiUKIVFVRVJZX0NPTVBBUklTT05fT1BFUkFUT1JfSVNfTlVMTBADEiAKHFFVRVJZX0NPTVBBUklTT05fT1BFUkFUT1JfTFQQBBIhCh1RVUVSWV9DT01QQVJJU09OX09QRVJBVE9SX0xURRAFEiAKHFFVRVJZX0NPTVBBUklTT05fT1BFUkFUT1JfR1QQBhIhCh1RVUVSWV9DT01QQVJJU09OX09QRVJBVE9SX0dURRAHEjIKLlFVRVJZX0NPTVBBUklTT05fT1BFUkFUT1JfQ09OVEFJTlNfSU5TRU5TSVRJVkUQCCqEAgoeUXVlcnlSZWxhdGlvblByZWRpY2F0ZU9wZXJhdG9yEjEKLVFVRVJZX1JFTEFUSU9OX1BSRURJQ0FURV9PUEVSQVRPUl9VTlNQRUNJRklFRBAAEioKJlFVRVJZX1JFTEFUSU9OX1BSRURJQ0FURV9PUEVSQVRPUl9TT01FEAESKgomUVVFUllfUkVMQVRJT05fUFJFRElDQVRFX09QRVJBVE9SX05PTkUQAhIoCiRRVUVSWV9SRUxBVElPTl9QUkVESUNBVEVfT1BFUkFUT1JfSVMQAxItCilRVUVSWV9SRUxBVElPTl9QUkVESUNBVEVfT1BFUkFUT1JfSVNfTlVMTBAEYgZwcm90bzM"); + fileDesc("ChNjYW1pbm8vc2NoZW1hLnByb3RvEgZjYW1pbm8iNwoOQXRvbURlZmluaXRpb24SDwoHYXRvbV9pZBgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkiWQoPQXRvbUNvbmZvcm1hbmNlEg8KB2F0b21faWQYASABKAkSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAIgASgJEhYKDmNvbmZvcm1hbmNlX2lkGAMgASgJIsIBCg9TdGF0ZUF0dGFjaG1lbnQSDwoHc2xvdF9pZBgBIAEoCRIYChBhdHRhY2hlZF9hdG9tX2lkGAIgASgJEhQKDGRpc3BsYXlfbmFtZRgDIAEoCRIXCg92YWx1ZV90eXBlX2pzb24YBCABKAkSGwoTc3RvcmFnZV9wb2xpY3lfanNvbhgFIAEoCRIaChJkZWZhdWx0X3ZhbHVlX2pzb24YBiABKAkSHAoUb3duZXJfY29uZm9ybWFuY2VfaWQYByABKAkiUAoSRW5kcG9pbnRDb25zdHJhaW50EhEKB2F0b21faWQYASABKAlIABIfChVpbnRlcmZhY2VfcmV2aXNpb25faWQYAiABKAlIAEIGCgRraW5kIvsBCgxFZGdlRW5kcG9pbnQSFQoNcHJvamVjdGlvbl9pZBgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkSLgoKY29uc3RyYWludBgDIAEoCzIaLmNhbWluby5FbmRwb2ludENvbnN0cmFpbnQSKAoLY2FyZGluYWxpdHkYBCABKA4yEy5jYW1pbm8uQ2FyZGluYWxpdHkSDwoHb3JkZXJlZBgFIAEoCBIRCglvbl9kZWxldGUYBiABKAkSFAoMcmV0YWluX290aGVyGAcgASgIEhAKCGtleV90eXBlGAggASgJEhgKEHB1YmxpY190cmF2ZXJzYWwYCSABKAgipQEKDkVkZ2VBdHRhY2htZW50EhQKDGVkZ2VfdHlwZV9pZBgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkSIwoFZmlyc3QYAyABKAsyFC5jYW1pbm8uRWRnZUVuZHBvaW50EiQKBnNlY29uZBgEIAEoCzIULmNhbWluby5FZGdlRW5kcG9pbnQSHAoUb3duZXJfY29uZm9ybWFuY2VfaWQYBSABKAkilQIKD1BlcnNpc3RlbmNlUGxhbhIUCgx3b3Jrc3BhY2VfaWQYASABKAkSHQoVd29ya3NwYWNlX3JldmlzaW9uX2lkGAIgASgJEiUKBWF0b21zGAMgAygLMhYuY2FtaW5vLkF0b21EZWZpbml0aW9uEi0KDGNvbmZvcm1hbmNlcxgEIAMoCzIXLmNhbWluby5BdG9tQ29uZm9ybWFuY2USJwoGc3RhdGVzGAUgAygLMhcuY2FtaW5vLlN0YXRlQXR0YWNobWVudBIlCgVlZGdlcxgGIAMoCzIWLmNhbWluby5FZGdlQXR0YWNobWVudBInCgdxdWVyaWVzGAcgAygLMhYuY2FtaW5vLkluc3RhbGxlZFF1ZXJ5Ip4BCg1RdWVyeUFyZ3VtZW50EhIKCHZhcmlhYmxlGAEgASgJSAASFgoMbGl0ZXJhbF9qc29uGAIgASgJSAASKQoEbGlzdBgDIAEoCzIZLmNhbWluby5RdWVyeUFyZ3VtZW50TGlzdEgAEi0KBm9iamVjdBgEIAEoCzIbLmNhbWluby5RdWVyeUFyZ3VtZW50T2JqZWN0SABCBwoFdmFsdWUiOgoRUXVlcnlBcmd1bWVudExpc3QSJQoGdmFsdWVzGAEgAygLMhUuY2FtaW5vLlF1ZXJ5QXJndW1lbnQilAEKE1F1ZXJ5QXJndW1lbnRPYmplY3QSNwoGZmllbGRzGAEgAygLMicuY2FtaW5vLlF1ZXJ5QXJndW1lbnRPYmplY3QuRmllbGRzRW50cnkaRAoLRmllbGRzRW50cnkSCwoDa2V5GAEgASgJEiQKBXZhbHVlGAIgASgLMhUuY2FtaW5vLlF1ZXJ5QXJndW1lbnQ6AjgBIkcKDlF1ZXJ5Q29uZGl0aW9uEg8KB2luY2x1ZGUYASABKAgSJAoFdmFsdWUYAiABKAsyFS5jYW1pbm8uUXVlcnlBcmd1bWVudCLUAwoOUXVlcnlTZWxlY3Rpb24SDAoEbmFtZRgBIAEoCRILCgNrZXkYAiABKAkSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAMgASgJEhEKCW1lbWJlcl9pZBgEIAEoCRIkChx0YXJnZXRfaW50ZXJmYWNlX3JldmlzaW9uX2lkGAUgASgJEioKCmNvbmRpdGlvbnMYBiADKAsyFi5jYW1pbm8uUXVlcnlDb25kaXRpb24SOAoJYXJndW1lbnRzGAcgAygLMiUuY2FtaW5vLlF1ZXJ5U2VsZWN0aW9uLkFyZ3VtZW50c0VudHJ5EikKCXNlbGVjdGlvbhgIIAMoCzIWLmNhbWluby5RdWVyeVNlbGVjdGlvbhIvCgpyZWxhdGlvbmFsGAkgASgLMhsuY2FtaW5vLlF1ZXJ5UmVsYXRpb25hbFBsYW4SKQoJcHJlZGljYXRlGAogASgLMhYuY2FtaW5vLlF1ZXJ5UHJlZGljYXRlEhkKEWxpdmVfc2VsZWN0aW9uX2lkGAsgASgJGkcKDkFyZ3VtZW50c0VudHJ5EgsKA2tleRgBIAEoCRIkCgV2YWx1ZRgCIAEoCzIVLmNhbWluby5RdWVyeUFyZ3VtZW50OgI4ASJqCg1RdWVyeVBhdGhTdGVwEhAKBnNvdXJjZRgBIAEoCEgAEhAKBnRhcmdldBgCIAEoCEgAEi0KCHJlbGF0aW9uGAMgASgLMhkuY2FtaW5vLlF1ZXJ5UmVsYXRpb25QYXRoSABCBgoEc3RlcCJ9ChFRdWVyeVJlbGF0aW9uUGF0aBIdChVpbnRlcmZhY2VfcmV2aXNpb25faWQYASABKAkSEQoJbWVtYmVyX2lkGAIgASgJEiQKHHRhcmdldF9pbnRlcmZhY2VfcmV2aXNpb25faWQYAyABKAkSEAoIb3B0aW9uYWwYBCABKAgiWQoRUXVlcnlGaWVsZE9wZXJhbmQSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAEgASgJEhEKCW1lbWJlcl9pZBgCIAEoCRISCgp2YWx1ZV9wYXRoGAMgAygJIrYBCg9RdWVyeUV4cHJlc3Npb24SIwoEcGF0aBgBIAMoCzIVLmNhbWluby5RdWVyeVBhdGhTdGVwEioKBWZpZWxkGAIgASgLMhkuY2FtaW5vLlF1ZXJ5RmllbGRPcGVyYW5kSAASDQoDcmVmGAMgASgJSAASDwoFZW50cnkYBCABKAhIABIRCgdtYXBfa2V5GAUgASgISAASFwoPdmFsdWVfdHlwZV9qc29uGAYgASgJQgYKBGxlYWYihQEKDlF1ZXJ5UmVkdWN0aW9uEjAKCG9wZXJhdG9yGAEgASgOMh4uY2FtaW5vLlF1ZXJ5QWdncmVnYXRlT3BlcmF0b3ISKAoHb3BlcmFuZBgCIAEoCzIXLmNhbWluby5RdWVyeUV4cHJlc3Npb24SFwoPdmFsdWVfdHlwZV9qc29uGAMgASgJInUKDFF1ZXJ5T3BlcmFuZBItCgpleHByZXNzaW9uGAEgASgLMhcuY2FtaW5vLlF1ZXJ5RXhwcmVzc2lvbkgAEisKCXJlZHVjdGlvbhgCIAEoCzIWLmNhbWluby5RdWVyeVJlZHVjdGlvbkgAQgkKB29wZXJhbmQikQEKD1F1ZXJ5Q29tcGFyaXNvbhIlCgdvcGVyYW5kGAEgASgLMhQuY2FtaW5vLlF1ZXJ5T3BlcmFuZBIxCghvcGVyYXRvchgCIAEoDjIfLmNhbWluby5RdWVyeUNvbXBhcmlzb25PcGVyYXRvchIkCgV2YWx1ZRgDIAEoCzIVLmNhbWluby5RdWVyeUFyZ3VtZW50Ij4KElF1ZXJ5UHJlZGljYXRlTGlzdBIoCghjaGlsZHJlbhgBIAMoCzIWLmNhbWluby5RdWVyeVByZWRpY2F0ZSL1AQoWUXVlcnlSZWxhdGlvblByZWRpY2F0ZRIjCgRwYXRoGAEgAygLMhUuY2FtaW5vLlF1ZXJ5UGF0aFN0ZXASKwoIcmVsYXRpb24YAiABKAsyGS5jYW1pbm8uUXVlcnlSZWxhdGlvblBhdGgSOAoIb3BlcmF0b3IYAyABKA4yJi5jYW1pbm8uUXVlcnlSZWxhdGlvblByZWRpY2F0ZU9wZXJhdG9yEikKCXByZWRpY2F0ZRgEIAEoCzIWLmNhbWluby5RdWVyeVByZWRpY2F0ZRIkCgV2YWx1ZRgFIAEoCzIVLmNhbWluby5RdWVyeUFyZ3VtZW50IroBChdRdWVyeVJlZHVjdGlvblByZWRpY2F0ZRIjCgRwYXRoGAEgAygLMhUuY2FtaW5vLlF1ZXJ5UGF0aFN0ZXASKwoIcmVsYXRpb24YAiABKAsyGS5jYW1pbm8uUXVlcnlSZWxhdGlvblBhdGgSJQoFd2hlcmUYAyABKAsyFi5jYW1pbm8uUXVlcnlQcmVkaWNhdGUSJgoGaGF2aW5nGAQgASgLMhYuY2FtaW5vLlF1ZXJ5UHJlZGljYXRlImMKF1F1ZXJ5SW50ZXJmYWNlUHJlZGljYXRlEh0KFWludGVyZmFjZV9yZXZpc2lvbl9pZBgBIAEoCRIpCglwcmVkaWNhdGUYAiABKAsyFi5jYW1pbm8uUXVlcnlQcmVkaWNhdGUi2wIKDlF1ZXJ5UHJlZGljYXRlEikKA2FuZBgBIAEoCzIaLmNhbWluby5RdWVyeVByZWRpY2F0ZUxpc3RIABIoCgJvchgCIAEoCzIaLmNhbWluby5RdWVyeVByZWRpY2F0ZUxpc3RIABIlCgNub3QYAyABKAsyFi5jYW1pbm8uUXVlcnlQcmVkaWNhdGVIABIqCgdjb21wYXJlGAQgASgLMhcuY2FtaW5vLlF1ZXJ5Q29tcGFyaXNvbkgAEjIKCHJlbGF0aW9uGAUgASgLMh4uY2FtaW5vLlF1ZXJ5UmVsYXRpb25QcmVkaWNhdGVIABIxCgZyZWR1Y2UYBiABKAsyHy5jYW1pbm8uUXVlcnlSZWR1Y3Rpb25QcmVkaWNhdGVIABItCgJhcxgHIAEoCzIfLmNhbWluby5RdWVyeUludGVyZmFjZVByZWRpY2F0ZUgAQgsKCXByZWRpY2F0ZSJhCghRdWVyeVJvdxIoCgZvYmplY3QYASABKAsyFi5jYW1pbm8uUXVlcnlPYmplY3RSb3dIABIkCgRwYWlyGAIgASgLMhQuY2FtaW5vLlF1ZXJ5UGFpclJvd0gAQgUKA3JvdyJVCg5RdWVyeU9iamVjdFJvdxIdChVpbnRlcmZhY2VfcmV2aXNpb25faWQYASABKAkSEgoKbWVtYmVyc2hpcBgCIAEoCBIQCghrZXlfdHlwZRgDIAEoCSJSCgxRdWVyeVBhaXJSb3cSIAoGc291cmNlGAEgASgLMhAuY2FtaW5vLlF1ZXJ5Um93EiAKBnRhcmdldBgCIAEoCzIQLmNhbWluby5RdWVyeVJvdyJFCghRdWVyeUtleRIMCgRwYXRoGAEgAygJEisKCmV4cHJlc3Npb24YAiABKAsyFy5jYW1pbm8uUXVlcnlFeHByZXNzaW9uIi8KDVF1ZXJ5RGlzdGluY3QSHgoEa2V5cxgBIAMoCzIQLmNhbWluby5RdWVyeUtleSJiCg5RdWVyeUV4cGFuc2lvbhIjCgRwYXRoGAEgAygLMhUuY2FtaW5vLlF1ZXJ5UGF0aFN0ZXASKwoIcmVsYXRpb24YAiABKAsyGS5jYW1pbm8uUXVlcnlSZWxhdGlvblBhdGgimAIKFFF1ZXJ5UmVsYXRpb25hbFN0YWdlEgoKAmlkGAEgASgNEhIKBWlucHV0GAIgASgNSAGIAQESHQoDcm93GAMgASgLMhAuY2FtaW5vLlF1ZXJ5Um93EisKBnNvdXJjZRgEIAEoCzIZLmNhbWluby5RdWVyeVJlbGF0aW9uUGF0aEgAEigKBmZpbHRlchgFIAEoCzIWLmNhbWluby5RdWVyeVByZWRpY2F0ZUgAEigKBmV4cGFuZBgGIAEoCzIWLmNhbWluby5RdWVyeUV4cGFuc2lvbkgAEikKCGRpc3RpbmN0GAcgASgLMhUuY2FtaW5vLlF1ZXJ5RGlzdGluY3RIAEILCglvcGVyYXRpb25CCAoGX2lucHV0IlMKGFF1ZXJ5UmVkdWN0aW9uUHJvamVjdGlvbhIMCgRwYXRoGAEgAygJEikKCXJlZHVjdGlvbhgCIAEoCzIWLmNhbWluby5RdWVyeVJlZHVjdGlvbiJQChNRdWVyeUFnZ3JlZ2F0ZU9yZGVyEiUKB29wZXJhbmQYASABKAsyFC5jYW1pbm8uUXVlcnlPcGVyYW5kEhIKCmRlc2NlbmRpbmcYAiABKAgi0gMKF1F1ZXJ5UmVsYXRpb25hbFRlcm1pbmFsEg0KBXN0YWdlGAEgASgNEgwKBHBhdGgYAiADKAkSDgoGZ3JvdXBzGAMgASgIEh4KBGtleXMYBCADKAsyEC5jYW1pbm8uUXVlcnlLZXkSNAoKcmVkdWN0aW9ucxgFIAMoCzIgLmNhbWluby5RdWVyeVJlZHVjdGlvblByb2plY3Rpb24SJQoFd2hlcmUYBiABKAsyFi5jYW1pbm8uUXVlcnlQcmVkaWNhdGUSJgoGaGF2aW5nGAcgASgLMhYuY2FtaW5vLlF1ZXJ5UHJlZGljYXRlEioKBW9yZGVyGAggAygLMhsuY2FtaW5vLlF1ZXJ5QWdncmVnYXRlT3JkZXISJAoFZmlyc3QYCSABKAsyFS5jYW1pbm8uUXVlcnlBcmd1bWVudBIiCgNhbGwYCiABKAsyFS5jYW1pbm8uUXVlcnlBcmd1bWVudBIkCgVhZnRlchgLIAEoCzIVLmNhbWluby5RdWVyeUFyZ3VtZW50EikKCXNlbGVjdGlvbhgMIAMoCzIWLmNhbWluby5RdWVyeVNlbGVjdGlvbhIQCghyZXNpZHVhbBgNIAEoCBIMCgRyb3dzGA4gASgIIncKE1F1ZXJ5UmVsYXRpb25hbFBsYW4SLAoGc3RhZ2VzGAEgAygLMhwuY2FtaW5vLlF1ZXJ5UmVsYXRpb25hbFN0YWdlEjIKCXRlcm1pbmFscxgCIAMoCzIfLmNhbWluby5RdWVyeVJlbGF0aW9uYWxUZXJtaW5hbCLpAgoQUXVlcnlSZWFkQmluZGluZxIPCgdhdG9tX2lkGAEgASgJEh0KFWludGVyZmFjZV9yZXZpc2lvbl9pZBgCIAEoCRIRCgltZW1iZXJfaWQYAyABKAkSGwoTZ2V0dGVyX29wZXJhdGlvbl9pZBgEIAEoCRIPCgdzbG90X2lkGAUgASgJEhQKDGVkZ2VfdHlwZV9pZBgGIAEoCRIVCg1wcm9qZWN0aW9uX2lkGAcgASgJEgsKA3JwYxgIIAEoCBIgChh3YXRjaF9zdGFydF9vcGVyYXRpb25faWQYCSABKAkSHwoXd2F0Y2hfc3RvcF9vcGVyYXRpb25faWQYCiABKAkSEgoKZmllbGRfbmFtZRgLIAEoCRIXCg92YWx1ZV90eXBlX2pzb24YDCABKAkSKAoLY2FyZGluYWxpdHkYDSABKA4yEy5jYW1pbm8uQ2FyZGluYWxpdHkSEAoIa2V5X3R5cGUYDiABKAkirwEKElF1ZXJ5RmllbGRDb250cmFjdBIdChVpbnRlcmZhY2VfcmV2aXNpb25faWQYASABKAkSEQoJbWVtYmVyX2lkGAIgASgJEhIKCmZpZWxkX25hbWUYAyABKAkSFwoPdmFsdWVfdHlwZV9qc29uGAQgASgJEigKC2NhcmRpbmFsaXR5GAUgASgOMhMuY2FtaW5vLkNhcmRpbmFsaXR5EhAKCGtleV90eXBlGAYgASgJIpIBCgxRdWVyeUJ1ZGdldHMSDAoEcm93cxgBIAEoDRINCgVkZXB0aBgCIAEoDRIUCgxyZXN1bHRfYnl0ZXMYAyABKA0SEgoKY2FuZGlkYXRlcxgEIAEoDRIRCglycGNfY2FsbHMYBSABKA0SEwoLY29uY3VycmVuY3kYBiABKA0SEwoLZGVhZGxpbmVfbXMYByABKA0iTAoOUXVlcnlQbGFjZW1lbnQSEQoHcmVwbGljYRgBIAEoCEgAEh8KFWxvY2FsX3NvdXJjZV9xdWVyeV9pZBgCIAEoCUgAQgYKBGtpbmQimgUKDkluc3RhbGxlZFF1ZXJ5EgoKAmlkGAEgASgJEhkKEWRlZmluaXRpb25fZGlnZXN0GAIgASgJEhYKDmJpbmRpbmdfZGlnZXN0GAMgASgJEhoKEG9iamVjdF9pbnRlcmZhY2UYBCABKAlIABIdChN3b3Jrc3BhY2VfaW50ZXJmYWNlGA4gASgJSAASKQoJc2VsZWN0aW9uGAUgAygLMhYuY2FtaW5vLlF1ZXJ5U2VsZWN0aW9uEioKCGJpbmRpbmdzGAYgAygLMhguY2FtaW5vLlF1ZXJ5UmVhZEJpbmRpbmcSJQoHYnVkZ2V0cxgHIAEoCzIULmNhbWluby5RdWVyeUJ1ZGdldHMSGwoTdmFyaWFibGVzX3R5cGVfanNvbhgIIAEoCRIYChBvdXRwdXRfdHlwZV9qc29uGAkgASgJEkcKEXZhcmlhYmxlX2RlZmF1bHRzGAogAygLMiwuY2FtaW5vLkluc3RhbGxlZFF1ZXJ5LlZhcmlhYmxlRGVmYXVsdHNFbnRyeRINCgV3YXRjaBgLIAEoCBIbChNwb2xsaW5nX2ludGVydmFsX21zGAwgASgNEh4KFnJwY19wcmVkaWNhdGVfb3Jfb3JkZXIYDSABKAgSFQoNcm9vdF9hdG9tX2lkcxgPIAMoCRIqCgZmaWVsZHMYECADKAsyGi5jYW1pbm8uUXVlcnlGaWVsZENvbnRyYWN0EikKCXBsYWNlbWVudBgRIAEoCzIWLmNhbWluby5RdWVyeVBsYWNlbWVudBpOChVWYXJpYWJsZURlZmF1bHRzRW50cnkSCwoDa2V5GAEgASgJEiQKBXZhbHVlGAIgASgLMhUuY2FtaW5vLlF1ZXJ5QXJndW1lbnQ6AjgBQgYKBHJvb3QqaAoLQ2FyZGluYWxpdHkSGwoXQ0FSRElOQUxJVFlfVU5TUEVDSUZJRUQQABIQCgxPUFRJT05BTF9PTkUQARIPCgtFWEFDVExZX09ORRACEggKBE1BTlkQAxIPCgtNQU5ZX1VOSVFVRRAEKpoCChZRdWVyeUFnZ3JlZ2F0ZU9wZXJhdG9yEigKJFFVRVJZX0FHR1JFR0FURV9PUEVSQVRPUl9VTlNQRUNJRklFRBAAEiIKHlFVRVJZX0FHR1JFR0FURV9PUEVSQVRPUl9DT1VOVBABEioKJlFVRVJZX0FHR1JFR0FURV9PUEVSQVRPUl9DT1VOVF9QUkVTRU5UEAISIAocUVVFUllfQUdHUkVHQVRFX09QRVJBVE9SX1NVTRADEiAKHFFVRVJZX0FHR1JFR0FURV9PUEVSQVRPUl9BVkcQBBIgChxRVUVSWV9BR0dSRUdBVEVfT1BFUkFUT1JfTUlOEAUSIAocUVVFUllfQUdHUkVHQVRFX09QRVJBVE9SX01BWBAGKu0CChdRdWVyeUNvbXBhcmlzb25PcGVyYXRvchIpCiVRVUVSWV9DT01QQVJJU09OX09QRVJBVE9SX1VOU1BFQ0lGSUVEEAASIAocUVVFUllfQ09NUEFSSVNPTl9PUEVSQVRPUl9FURABEiAKHFFVRVJZX0NPTVBBUklTT05fT1BFUkFUT1JfSU4QAhIlCiFRVUVSWV9DT01QQVJJU09OX09QRVJBVE9SX0lTX05VTEwQAxIgChxRVUVSWV9DT01QQVJJU09OX09QRVJBVE9SX0xUEAQSIQodUVVFUllfQ09NUEFSSVNPTl9PUEVSQVRPUl9MVEUQBRIgChxRVUVSWV9DT01QQVJJU09OX09QRVJBVE9SX0dUEAYSIQodUVVFUllfQ09NUEFSSVNPTl9PUEVSQVRPUl9HVEUQBxIyCi5RVUVSWV9DT01QQVJJU09OX09QRVJBVE9SX0NPTlRBSU5TX0lOU0VOU0lUSVZFEAgqhAIKHlF1ZXJ5UmVsYXRpb25QcmVkaWNhdGVPcGVyYXRvchIxCi1RVUVSWV9SRUxBVElPTl9QUkVESUNBVEVfT1BFUkFUT1JfVU5TUEVDSUZJRUQQABIqCiZRVUVSWV9SRUxBVElPTl9QUkVESUNBVEVfT1BFUkFUT1JfU09NRRABEioKJlFVRVJZX1JFTEFUSU9OX1BSRURJQ0FURV9PUEVSQVRPUl9OT05FEAISKAokUVVFUllfUkVMQVRJT05fUFJFRElDQVRFX09QRVJBVE9SX0lTEAMSLQopUVVFUllfUkVMQVRJT05fUFJFRElDQVRFX09QRVJBVE9SX0lTX05VTEwQBGIGcHJvdG8z"); /** * @generated from message camino.AtomDefinition @@ -773,6 +773,28 @@ export type QueryReductionPredicate = Message<"camino.QueryReductionPredicate"> export const QueryReductionPredicateSchema: GenMessage = /*@__PURE__*/ messageDesc(file_camino_schema, 21); +/** + * @generated from message camino.QueryInterfacePredicate + */ +export type QueryInterfacePredicate = Message<"camino.QueryInterfacePredicate"> & { + /** + * @generated from field: string interface_revision_id = 1; + */ + interfaceRevisionId: string; + + /** + * @generated from field: camino.QueryPredicate predicate = 2; + */ + predicate?: QueryPredicate | undefined; +}; + +/** + * Describes the message camino.QueryInterfacePredicate. + * Use `create(QueryInterfacePredicateSchema)` to create a new message. + */ +export const QueryInterfacePredicateSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_camino_schema, 22); + /** * @generated from message camino.QueryPredicate */ @@ -816,6 +838,12 @@ export type QueryPredicate = Message<"camino.QueryPredicate"> & { */ value: QueryReductionPredicate; case: "reduce"; + } | { + /** + * @generated from field: camino.QueryInterfacePredicate as = 7; + */ + value: QueryInterfacePredicate; + case: "as"; } | { case: undefined; value?: undefined }; }; @@ -824,7 +852,7 @@ export type QueryPredicate = Message<"camino.QueryPredicate"> & { * Use `create(QueryPredicateSchema)` to create a new message. */ export const QueryPredicateSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_camino_schema, 22); + messageDesc(file_camino_schema, 23); /** * @generated from message camino.QueryRow @@ -853,7 +881,7 @@ export type QueryRow = Message<"camino.QueryRow"> & { * Use `create(QueryRowSchema)` to create a new message. */ export const QueryRowSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_camino_schema, 23); + messageDesc(file_camino_schema, 24); /** * @generated from message camino.QueryObjectRow @@ -880,7 +908,7 @@ export type QueryObjectRow = Message<"camino.QueryObjectRow"> & { * Use `create(QueryObjectRowSchema)` to create a new message. */ export const QueryObjectRowSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_camino_schema, 24); + messageDesc(file_camino_schema, 25); /** * @generated from message camino.QueryPairRow @@ -902,7 +930,7 @@ export type QueryPairRow = Message<"camino.QueryPairRow"> & { * Use `create(QueryPairRowSchema)` to create a new message. */ export const QueryPairRowSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_camino_schema, 25); + messageDesc(file_camino_schema, 26); /** * @generated from message camino.QueryKey @@ -924,7 +952,7 @@ export type QueryKey = Message<"camino.QueryKey"> & { * Use `create(QueryKeySchema)` to create a new message. */ export const QueryKeySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_camino_schema, 26); + messageDesc(file_camino_schema, 27); /** * @generated from message camino.QueryDistinct @@ -941,7 +969,7 @@ export type QueryDistinct = Message<"camino.QueryDistinct"> & { * Use `create(QueryDistinctSchema)` to create a new message. */ export const QueryDistinctSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_camino_schema, 27); + messageDesc(file_camino_schema, 28); /** * @generated from message camino.QueryExpansion @@ -963,7 +991,7 @@ export type QueryExpansion = Message<"camino.QueryExpansion"> & { * Use `create(QueryExpansionSchema)` to create a new message. */ export const QueryExpansionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_camino_schema, 28); + messageDesc(file_camino_schema, 29); /** * @generated from message camino.QueryRelationalStage @@ -1019,7 +1047,7 @@ export type QueryRelationalStage = Message<"camino.QueryRelationalStage"> & { * Use `create(QueryRelationalStageSchema)` to create a new message. */ export const QueryRelationalStageSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_camino_schema, 29); + messageDesc(file_camino_schema, 30); /** * @generated from message camino.QueryReductionProjection @@ -1041,7 +1069,7 @@ export type QueryReductionProjection = Message<"camino.QueryReductionProjection" * Use `create(QueryReductionProjectionSchema)` to create a new message. */ export const QueryReductionProjectionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_camino_schema, 30); + messageDesc(file_camino_schema, 31); /** * @generated from message camino.QueryAggregateOrder @@ -1063,7 +1091,7 @@ export type QueryAggregateOrder = Message<"camino.QueryAggregateOrder"> & { * Use `create(QueryAggregateOrderSchema)` to create a new message. */ export const QueryAggregateOrderSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_camino_schema, 31); + messageDesc(file_camino_schema, 32); /** * @generated from message camino.QueryRelationalTerminal @@ -1147,7 +1175,7 @@ export type QueryRelationalTerminal = Message<"camino.QueryRelationalTerminal"> * Use `create(QueryRelationalTerminalSchema)` to create a new message. */ export const QueryRelationalTerminalSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_camino_schema, 32); + messageDesc(file_camino_schema, 33); /** * @generated from message camino.QueryRelationalPlan @@ -1169,7 +1197,7 @@ export type QueryRelationalPlan = Message<"camino.QueryRelationalPlan"> & { * Use `create(QueryRelationalPlanSchema)` to create a new message. */ export const QueryRelationalPlanSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_camino_schema, 33); + messageDesc(file_camino_schema, 34); /** * @generated from message camino.QueryReadBinding @@ -1251,7 +1279,7 @@ export type QueryReadBinding = Message<"camino.QueryReadBinding"> & { * Use `create(QueryReadBindingSchema)` to create a new message. */ export const QueryReadBindingSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_camino_schema, 34); + messageDesc(file_camino_schema, 35); /** * Checked schema survives even when no atom currently implements an interface. @@ -1295,7 +1323,7 @@ export type QueryFieldContract = Message<"camino.QueryFieldContract"> & { * Use `create(QueryFieldContractSchema)` to create a new message. */ export const QueryFieldContractSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_camino_schema, 35); + messageDesc(file_camino_schema, 36); /** * @generated from message camino.QueryBudgets @@ -1342,7 +1370,7 @@ export type QueryBudgets = Message<"camino.QueryBudgets"> & { * Use `create(QueryBudgetsSchema)` to create a new message. */ export const QueryBudgetsSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_camino_schema, 36); + messageDesc(file_camino_schema, 37); /** * @generated from message camino.QueryPlacement @@ -1371,7 +1399,7 @@ export type QueryPlacement = Message<"camino.QueryPlacement"> & { * Use `create(QueryPlacementSchema)` to create a new message. */ export const QueryPlacementSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_camino_schema, 37); + messageDesc(file_camino_schema, 38); /** * @generated from message camino.InstalledQuery @@ -1475,7 +1503,7 @@ export type InstalledQuery = Message<"camino.InstalledQuery"> & { * Use `create(InstalledQuerySchema)` to create a new message. */ export const InstalledQuerySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_camino_schema, 38); + messageDesc(file_camino_schema, 39); /** * @generated from enum camino.Cardinality diff --git a/src/query/compile.ts b/src/query/compile.ts index d9b6ec1..bf754a1 100644 --- a/src/query/compile.ts +++ b/src/query/compile.ts @@ -3,6 +3,7 @@ import { Source, validate, specifiedRules, + PossibleFragmentSpreadsRule, print, printSchema, visit, @@ -89,7 +90,7 @@ export async function compileQuery( definitions.push(...document.definitions); } const fragmentContracts = await fragmentSources(declaration, definitions, resolveFragment); - const document: DocumentNode = { kind: "Document" as DocumentNode["kind"], definitions }; + let document: DocumentNode = { kind: "Document" as DocumentNode["kind"], definitions }; if (definitions.length > 256 || Buffer.byteLength(print(document)) > 262144) fail("QUERY_SOURCE_LIMIT", "Expanded fragment sources exceed the query source budget"); visit(document, { @@ -103,7 +104,44 @@ export async function compileQuery( }, }); const generated = querySchema(declaration, interfaces, document); - const operations = definitions.filter((entry) => entry.kind === "OperationDefinition"); + // Identity belongs to the checked object result, including fragment results. + // Schema membership deliberately excludes structured values and query wrappers. + const identity = parse("query Identity { _camino { ref } }", { noLocation: true }).definitions[0]!; + if (identity.kind !== "OperationDefinition") throw new Error("Invalid identity selection"); + const identitySelection = identity.selectionSet.selections[0]!; + const identityTypes = new TypeInfo(generated.schema); + document = visit( + document, + visitWithTypeInfo(identityTypes, { + SelectionSet: { + leave(node, _key, parentNode) { + // An inline projection is part of its containing object. Its identity + // remains that object's base interface, not the projected interface. + if (parentNode && !Array.isArray(parentNode) && "kind" in parentNode && parentNode.kind === "InlineFragment") + return; + const parent = identityTypes.getParentType(); + const contract = parent && generated.byName.get(parent.name); + if (!contract || contract.revisionId === workspaceQueryRootId) return; + const hasIdentity = node.selections.some( + (field) => + field.kind === "Field" && + field.name.value === "_camino" && + !field.alias && + !field.directives?.length && + field.selectionSet?.selections.some( + (child) => + child.kind === "Field" && child.name.value === "ref" && !child.alias && !child.directives?.length, + ), + ); + // GraphQL validation caches comparisons by AST identity. Each nominal + // object must get its own metadata subtree, even though the spelling is + // identical (its reference scalar can be different). + if (!hasIdentity) return { ...node, selections: [...node.selections, structuredClone(identitySelection)] }; + }, + }, + }), + ); + const operations = document.definitions.filter((entry) => entry.kind === "OperationDefinition"); if ( operations.length !== 1 || operations[0]!.operation !== "query" || @@ -120,18 +158,56 @@ export async function compileQuery( if (!["include", "skip"].includes(node.name.value)) fail("QUERY_UNSUPPORTED_FEATURE", `Unsupported directive ${node.name.value}`, node); }, - InlineFragment(node) { - fail("QUERY_UNSUPPORTED_FEATURE", "Use named fragments on the exact selected type", node); - }, }); - const errors = validate(generated.schema, document, specifiedRules, { maxErrors: 20 }); + // Capability interfaces are nominal views of one graph object, not mutually + // exclusive GraphQL object variants. Check overlap using declared requirements. + const projectionTypes = new TypeInfo(generated.schema); + const requires = (from: InterfaceRevisionId, to: InterfaceRevisionId, seen = new Set()): boolean => { + if (from === to) return true; + if (seen.has(from)) return false; + seen.add(from); + return generated.contracts.get(from)?.requiredInterfaces?.some((id) => requires(id, to, seen)) ?? false; + }; + visit( + document, + visitWithTypeInfo(projectionTypes, { + InlineFragment(node) { + if (!node.typeCondition) return; + const from = projectionTypes.getParentType(); + const a = from && generated.byName.get(from.name); + const b = generated.byName.get(node.typeCondition.name.value); + if (!a || !b || !requires(a.revisionId, b.revisionId)) + fail( + "QUERY_UNSUPPORTED_FEATURE", + "Inline interface projections currently require a declared interface requirement", + node, + ); + }, + FragmentSpread(node) { + const fragment = document.definitions.find( + (entry) => entry.kind === "FragmentDefinition" && entry.name.value === node.name.value, + ); + if ( + fragment?.kind === "FragmentDefinition" && + fragment.typeCondition.name.value !== projectionTypes.getParentType()?.name + ) + fail("QUERY_UNSUPPORTED_FEATURE", "Named fragments currently require the exact nominal root interface", node); + }, + }), + ); + const errors = validate( + generated.schema, + document, + specifiedRules.filter((rule) => rule !== PossibleFragmentSpreadsRule), + { maxErrors: 20 }, + ); if (errors.length) { const error = errors[0]!; fail("QUERY_VALIDATION", error.message, error.nodes?.[0]); } const effects = new Map(); const fragments = new Map( - definitions + document.definitions .filter((entry): entry is FragmentDefinitionNode => entry.kind === "FragmentDefinition") .map((entry) => [entry.name.value, entry]), ); @@ -313,16 +389,28 @@ export async function compileQuery( if (isListType(type)) return valueType.list(output(type.ofType, selections, depth, conditional)); if (isObjectType(type)) { const fields: Record = {}; - const add = (set: SelectionSetNode, conditional = false) => { + const add = (set: SelectionSetNode, conditional = false, selectedType = type) => { for (const selection of set.selections) { if (++expandedFields > 10000) fail("QUERY_WORK_LIMIT", "Expanded query exceeds 10000 fields", selection); if (selection.kind === "FragmentSpread") { - add(fragments.get(selection.name.value)!.selectionSet, conditional || !!selection.directives?.length); + add( + fragments.get(selection.name.value)!.selectionSet, + conditional || !!selection.directives?.length, + selectedType, + ); + continue; + } + if (selection.kind === "InlineFragment") { + const projected = selection.typeCondition + ? generated.schema.getType(selection.typeCondition.name.value) + : selectedType; + if (!isObjectType(projected)) return fail("QUERY_TYPE", "Expected an object projection", selection); + add(selection.selectionSet, conditional || !!selection.directives?.length, projected); continue; } if (selection.kind !== "Field") continue; const key = selection.alias?.value ?? selection.name.value; - const field = type.getFields()[selection.name.value]!; + const field = selectedType.getFields()[selection.name.value]!; const previous = fields[key]; fields[key] = output( field.type, @@ -338,7 +426,9 @@ export async function compileQuery( if (previous) fields[key] = mergeOutput(previous, fields[key]!); } }; - if (selections) add(selections, conditional); + // A condition on this object makes the object optional, not every child + // of the object when it is present. Fragment conditions are handled by add. + if (selections) add(selections); return { kind: "record", fields }; } return ( @@ -409,8 +499,13 @@ export async function compileQuery( set: SelectionSetNode, parent: import("graphql").GraphQLObjectType, inherited: QuerySelection["conditions"] = [], - ): QuerySelection[] => - set.selections.flatMap((node): QuerySelection[] => { + ): QuerySelection[] => { + const selections = set.selections.flatMap((node): QuerySelection[] => { + if (node.kind === "InlineFragment") { + const projected = node.typeCondition ? generated.schema.getType(node.typeCondition.name.value) : parent; + if (!isObjectType(projected)) return fail("QUERY_TYPE", "Expected an object projection", node); + return selection(node.selectionSet, projected, [...inherited, ...conditions(node.directives)]); + } if (node.kind === "FragmentSpread") { const inheritedConditions = [...inherited, ...conditions(node.directives)]; const contract = fragmentContracts.get(node.name.value); @@ -436,6 +531,7 @@ 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 referenceType = type && generated.scalarTypes.get(type.name); const role = generated.relational.roles.get(parent.name); const relationship = role?.kind === "relations" @@ -455,6 +551,11 @@ export async function compileQuery( key: node.alias?.value ?? node.name.value, ...(member && contract ? { interfaceRevisionId: contract.revisionId, memberId: member.id } : {}), ...(member?.kind === "relationship" ? { targetInterfaceRevisionId: generated.target(member) } : {}), + ...(node.name.value === "ref" && + referenceType?.kind === "object-ref" && + referenceType.expectation.kind === "interface" + ? { targetInterfaceRevisionId: referenceType.expectation.interfaceRevisionId } + : {}), conditions: [...inherited, ...conditions(node.directives)], arguments: Object.fromEntries( (node.arguments ?? []).map((entry) => [entry.name.value, argument(entry.value)]), @@ -465,13 +566,45 @@ export async function compileQuery( }, ]; }); + const contract = generated.byName.get(parent.name); + if (!contract || contract.revisionId === workspaceQueryRootId) return selections; + // Fragment expansion may repeat metadata. Combine its children so a later + // implicit reference cannot overwrite an explicitly selected aggregate. + const metadata = selections.filter((field) => field.key === "_camino" && field.name === "_camino"); + const responseFields = new Map(); + for (const field of selections.filter((field) => field.memberId)) { + const previous = responseFields.get(field.key); + if ( + previous && + (previous.interfaceRevisionId !== field.interfaceRevisionId || previous.memberId !== field.memberId) + ) + fail("QUERY_PROJECTION_CONFLICT", `Alias distinct interface fields selected as ${field.key}`); + responseFields.set(field.key, field); + } + if (!metadata.length) return selections; + const children = metadata.flatMap((field) => + field.selection.map((child) => ({ + ...child, + conditions: [...field.conditions, ...child.conditions], + })), + ); + const ref = children.find((child) => child.key === "ref" && child.name === "ref")!; + return [ + ...selections.filter((field) => !metadata.includes(field)), + { + ...metadata[0]!, + conditions: [], + selection: [...children.filter((child) => child.key !== "ref"), { ...ref, conditions: [] }], + }, + ]; + }; const normalized = print(document), schema = printSchema(generated.schema); const selections = selection(operations[0]!.selectionSet, generated.schema.getQueryType()!); const definitionDigest = createHash("sha256") .update( canonicalJson({ - semantics: 3, + semantics: 4, declaration, normalized, // Source provenance belongs to the checked installation. Identical public diff --git a/src/query/link.ts b/src/query/link.ts index 720e771..fa81959 100644 --- a/src/query/link.ts +++ b/src/query/link.ts @@ -147,10 +147,22 @@ export function linkQueries(workspace: WorkspaceRevision): LinkedQuery[] { (entry) => entry.interfaceRevisionId === declaration.root.interface, ); const rootAtoms = [...new Set(rootConformances.map((entry) => entry.atomId))].sort(); + const qualifiedInterfaces = new Set(); + const qualifications = (node: unknown): void => { + if (!node || typeof node !== "object") return; + if ("kind" in node && node.kind === "as" && "interfaceRevisionId" in node) + qualifiedInterfaces.add(String(node.interfaceRevisionId)); + for (const child of Object.values(node)) qualifications(child); + }; + qualifications(checked.selection); const bindingDigest = createHash("sha256") .update( canonicalJson({ definition: checked.definitionDigest, + qualifiedConformances: workspace.conformances + .filter((entry) => qualifiedInterfaces.has(entry.interfaceRevisionId)) + .map((entry) => ({ atomId: entry.atomId, id: entry.id, interfaceRevisionId: entry.interfaceRevisionId })) + .sort((a, b) => canonicalJson(a).localeCompare(canonicalJson(b))), rootConformances: rootConformances .map((entry) => ({ atomId: entry.atomId, id: entry.id, interfaceRevisionId: entry.interfaceRevisionId })) .sort((a, b) => canonicalJson(a).localeCompare(canonicalJson(b))), diff --git a/src/query/relational-compile.ts b/src/query/relational-compile.ts index db1fb6a..3860b58 100644 --- a/src/query/relational-compile.ts +++ b/src/query/relational-compile.ts @@ -337,6 +337,23 @@ export function relationalCompiler( visit(current[name], field.value, [...base, name]); else if (name === "_camino") { for (const meta of pairs(field.value)) { + if (meta.name.value === "as") { + if (current.kind !== "object" || base.length || available) + fail( + "QUERY_UNSUPPORTED_FEATURE", + "Interface predicates require an object row before distinct/grouping", + meta, + ); + for (const branch of pairs(meta.value)) { + const contract = env.byName.get(branch.name.value)!; + children.push({ + kind: "as", + interfaceRevisionId: contract.revisionId, + predicate: predicate(objectRow(contract.revisionId), branch.value), + }); + } + continue; + } if (meta.name.value !== "relations") { const expr = expression(row, [...base, "_camino", meta.name.value], ["predicate"], meta, available); children.push( @@ -385,6 +402,19 @@ export function relationalCompiler( } } } else { + const member = current.kind === "object" && relationships(current).find((m) => m.displayName === name); + if (member) { + const expr = expression(row, [...base, name, "_camino", "ref"], ["predicate"], field, available); + children.push( + ...(comparisons(field.value, (operator, value) => ({ + kind: "compare", + expression: expr, + operator, + value, + })) as QueryPredicate[]), + ); + continue; + } const valuePredicate = (names: string[], value: ValueNode) => { const expr = expression(row, names, ["predicate"], field, available); const type = expr.type.kind === "optional" ? expr.type.value : expr.type; diff --git a/src/query/relational-proto.ts b/src/query/relational-proto.ts index 157cb65..5cd12f8 100644 --- a/src/query/relational-proto.ts +++ b/src/query/relational-proto.ts @@ -50,6 +50,13 @@ export function relationalWire( }); const predicate = (p: QueryPredicate | QueryAggregatePredicate): wire.QueryPredicate => { switch (p.kind) { + case "as": + return create(wire.QueryPredicateSchema, { + predicate: { + case: "as", + value: { interfaceRevisionId: p.interfaceRevisionId, predicate: predicate(p.predicate) }, + }, + }); case "and": case "or": return create(wire.QueryPredicateSchema, { diff --git a/src/query/relational-schema.ts b/src/query/relational-schema.ts index 04612ba..7b0f41e 100644 --- a/src/query/relational-schema.ts +++ b/src/query/relational-schema.ts @@ -8,6 +8,7 @@ import { GraphQLInputObjectType, GraphQLList, GraphQLNonNull, + visit, type GraphQLInputType, type GraphQLOutputType, type GraphQLInputFieldConfigMap, @@ -54,6 +55,16 @@ export interface RelationalSchemaEnvironment { } export function relationalSchema(env: RelationalSchemaEnvironment, document?: DocumentNode) { + const requestedInterfaces = new Set(); + if (document) + visit(document, { + ObjectField(node) { + if (node.name.value !== "_camino" || node.value.kind !== "ObjectValue") return; + const qualified = node.value.fields.find((field) => field.name.value === "as"); + if (qualified?.value.kind === "ObjectValue") + for (const field of qualified.value.fields) requestedInterfaces.add(field.name.value); + }, + }); const outputCache = new Map(); const inputCache = new Map(); const referenceCache = new Map(); @@ -303,14 +314,40 @@ export function relationalSchema(env: RelationalSchemaEnvironment, document?: Do 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}`); + if (row.kind === "object" && field.kind === "row") { + result[name] = { + type: compare( + valueType.interfaceRef( + field.row.kind === "object" + ? field.row.interfaceRevisionId + : (() => { + throw new Error("Expected relationship object"); + })(), + ), + ), + }; + continue; + } if (field.kind === "leaf" && !comparableValue(field.type)) continue; result[name] = { type: field.kind === "row" ? where(field.row) : compare(field.type) }; } result._camino = { type: inp("WhereMeta", row, () => { const meta: GraphQLInputFieldConfigMap = {}; + if (row.kind === "object" && requestedInterfaces.size) + meta.as = { + type: inp("Interfaces", row, () => + Object.fromEntries( + [...env.contracts.values()] + .filter((contract) => !contract.template && requestedInterfaces.has(env.name(contract.revisionId))) + .map((contract) => [ + env.name(contract.revisionId), + { type: where(objectRow(contract.revisionId)) }, + ]), + ), + ), + }; for (const [name, type] of leafFields(row)) meta[name] = { type: compare(type) }; const edges = relations(row); if (edges.length) diff --git a/src/query/relational.ts b/src/query/relational.ts index 5f1886a..0e7b2a8 100644 --- a/src/query/relational.ts +++ b/src/query/relational.ts @@ -60,6 +60,7 @@ export interface QueryReduction { type: ValueType; } export type QueryPredicate = + | { kind: "as"; interfaceRevisionId: InterfaceRevisionId; predicate: QueryPredicate } | { kind: "and" | "or"; children: QueryPredicate[] } | { kind: "not"; child: QueryPredicate } | { diff --git a/src/query/schema.ts b/src/query/schema.ts index ea9e2a1..948ab25 100644 --- a/src/query/schema.ts +++ b/src/query/schema.ts @@ -12,6 +12,7 @@ import { GraphQLNonNull, GraphQLSchema, specifiedDirectives, + visit, type GraphQLOutputType, type GraphQLInputType, type GraphQLFieldConfigMap, @@ -261,6 +262,19 @@ export function querySchema( }; const schema = new GraphQLSchema({ directives: specifiedDirectives, + // Nominal projections can mention an interface not reached by a relation. + types: (() => { + const requested = new Set(); + if (document) + visit(document, { + NamedType(node) { + requested.add(node.name.value); + }, + }); + return [...contracts.values()] + .filter((contract) => !contract.template && requested.has(name(contract.revisionId))) + .map((contract) => object(contract.revisionId)); + })(), query: new GraphQLObjectType({ name: "QxQuery", fields: { root: { type: new GraphQLNonNull(object(root.interface)) } }, diff --git a/test/controller-registry.test.ts b/test/controller-registry.test.ts index 177b0af..de7cc84 100644 --- a/test/controller-registry.test.ts +++ b/test/controller-registry.test.ts @@ -38,11 +38,11 @@ test("controller discovery requires complete bounded accepted-state membership", .length, 1, ); + assert.equal((await check("query Enabled {root {objects(all:64) {entries {node {enabled}}}}}")).length, 1); for (const document of [ "query Enabled {root {objects(first:64) {entries {node {_camino {ref}}}}}}", "query Enabled {root {objects(all:65) {entries {node {_camino {ref}}}}}}", "query Enabled {root {devices:objects(all:64) {entries {node {_camino {ref}}}}}}", - "query Enabled {root {objects(all:64) {entries {node {enabled}}}}}", "query Enabled($enabled:Boolean!) {root {objects(all:64,where:{enabled:{eq:$enabled}}) {entries {node {_camino {ref}}}}}}", ]) await assert.rejects(check(document), /registry|Controller/); diff --git a/test/query-compiler.test.ts b/test/query-compiler.test.ts index cbdc52d..9304918 100644 --- a/test/query-compiler.test.ts +++ b/test/query-compiler.test.ts @@ -182,11 +182,36 @@ test("ordinary selections preserve aliases, fragment conditions and wire identit const node = checked.selection[0]!.selection[0]!.selection[0]!.selection[0]!; assert.deepEqual( node.selection.map((s) => s.key), - ["plain", "label", "due"], + ["plain", "label", "due", "_camino"], ); assert.equal(node.selection[1]!.conditions.length, 1); assert.ok(JSON.stringify(checked.selection.map(querySelectionToWire)).includes("label")); }); +test("object references are unconditional, canonical and idempotent through fragments", async () => { + const checked = await compile( + `query Upcoming($show: Boolean!) {root {items(first: 3) {entries {node {...Row @include(if: $show)}}}}}`, + `fragment Row on TaskFacts {label: title identity: _camino {alias: ref} _camino @include(if: $show) {ref}}`, + ); + const root = checked.selection[0]!; + const node = root.selection.find((s) => s.name === "items")!.selection[0]!.selection[0]!; + for (const object of [root, node]) { + const metadata = object.selection.filter((s) => s.key === "_camino"); + assert.equal(metadata.length, 1); + assert.deepEqual(metadata[0]!.conditions, []); + assert.deepEqual( + metadata[0]!.selection.filter((s) => s.key === "ref").map((s) => s.conditions), + [[]], + ); + } + assert.equal(node.selection.find((s) => s.key === "identity")!.conditions.length, 1); + const recompiled = await compileQuery( + { ...checked.declaration, fragments: [] }, + [collection, facts], + async () => checked.document, + ); + assert.deepEqual(recompiled.selection, checked.selection); + assert.deepEqual(recompiled.output, checked.output); +}); test("retired live syntax fails with migration guidance", async () => { for (const selected of ["score @live", "_camino @live {ref}", "title @live title", "title @live(unchecked: true)"]) await assert.rejects( @@ -199,6 +224,68 @@ test("retired live syntax fails with migration guidance", async () => { ); }); +test("implicit references reject aliases which occupy the canonical metadata path", async () => { + for (const invalid of ["_camino: title", "_camino {ref: __typename}"]) + await assert.rejects(compile(`query Upcoming {root {items(first: 3) {entries {node {...Row}}}}}`, `fragment Row on TaskFacts {${invalid}}`), /QUERY_VALIDATION|Introspection/); +}); + +test("implicit reference merges with conditional aggregate metadata", async () => { + const checked = await compileQuery({ ...fixture(""), fragments: [] }, [collection, facts], async () => + `query Upcoming($show: Boolean!) {root {_camino @include(if: $show) {relations {items {aggregate {count}}}}}}`); + const metadata = checked.selection[0]!.selection.filter((field) => field.key === "_camino"); + assert.equal(metadata.length, 1); + assert.deepEqual(metadata[0]!.conditions, []); + assert.equal(metadata[0]!.selection.find((field) => field.name === "relations")!.conditions.length, 1); + assert.deepEqual(metadata[0]!.selection.find((field) => field.name === "ref")!.conditions, []); + assert.equal(metadata[0]!.selection.find((field) => field.name === "relations")!.selection[0]!.selection[0]!.selection.some((field) => field.name === "_camino"), false); +}); + +test("qualified interface predicates and guaranteed inline projections retain nominal reads", async () => { + const owner = iface( + `interface Owner id "owner" revision "owner@1" { queryable value name id "name" : string {get id "name:get";} }`, + ); + const owned = iface( + `import interface Owner; + interface Owned id "owned" revision "owned@1" { + queryable relation owner id "owner" : optional-one interface Owner {resolve id "owner:resolve";} + queryable value label id "owned:label" : string {get id "owned:label:get";} + }`, + [owner], + ); + const item = iface( + `import interface Owned; + interface Item id "item" revision "item@1" requires Owned { + queryable value label id "item:label" : string {get id "item:label:get";} + }`, + [owned, owner], + ); + const declaration = { + ...fixture(""), + root: { kind: "workspace-interface" as const, interface: item.revisionId }, + fragments: [], + }; + const document = `query Upcoming($owner: QxRef_Owner!) {root {objects(all: 10, where: {_camino: {as: {Owned: {owner: {eq: $owner}}}}}) {entries {node {label ... on Owned {owner {name} ownerLabel: label}}}}}}`; + const checked = await compileQuery(declaration, [item, owned, owner], async () => document); + assert.ok( + checked.effects.some( + (effect) => + effect.interfaceRevisionId === owned.revisionId && + effect.memberId === "owner" && + effect.uses.includes("predicate"), + ), + ); + const objects = checked.selection[0]!.selection[0]!; + assert.equal(objects.predicate?.kind, "and"); + assert.ok(JSON.stringify(objects.predicate).includes('"kind":"as"')); + const node = objects.selection[0]!.selection[0]!; + assert.equal(node.selection.find((s) => s.key === "ownerLabel")!.interfaceRevisionId, owned.revisionId); + assert.equal(node.selection.filter((s) => s.key === "_camino").length, 1); + await assert.rejects( + compileQuery(declaration, [item, owned, owner], async () => document.replace("ownerLabel: label", "label")), + /QUERY_PROJECTION_CONFLICT|QUERY_VALIDATION/, + ); +}); + test("query dependency ports resolve exact exports without declaration ordering constraints", () => { const result = compileCapabilityResourceSource( `import interface Tasks; diff --git a/test/query-structured.test.ts b/test/query-structured.test.ts index ea8f30f..7b3697f 100644 --- a/test/query-structured.test.ts +++ b/test/query-structured.test.ts @@ -61,6 +61,17 @@ test("record/list query values preserve nested projections and leaf predicates", } } const raw = JSON.stringify(checked.output); + const node = extent.selection[0]!.selection[0]!; + assert.equal(node.selection.filter((field) => field.key === "_camino").length, 1); + const assertValuesOnly = (fields: typeof node.selection): void => { + for (const field of fields) { + assert.notEqual(field.name, "_camino", "Structured values do not acquire graph identity"); + assertValuesOnly(field.selection); + } + }; + assertValuesOnly(node.selection.filter((field) => field.key !== "_camino")); + assert.equal(checked.selection[0]!.selection.some((field) => field.key === "_camino"), false); + assert.equal(extent.selection.some((field) => field.key === "_camino"), false); assert.match(raw, /"begin"/); assert.match(raw, /"tags":\{"kind":"list"/); assert.equal(checked.effects.filter((e) => e.memberId === "range").length, 1);