@@ -13,6 +13,7 @@ import (
1313 "google.golang.org/grpc/codes"
1414 "google.golang.org/grpc/status"
1515
16+ "github.com/authzed/spicedb/internal/caveats"
1617 "github.com/authzed/spicedb/internal/dispatch"
1718 "github.com/authzed/spicedb/internal/graph"
1819 log "github.com/authzed/spicedb/internal/logging"
@@ -24,6 +25,8 @@ import (
2425 "github.com/authzed/spicedb/pkg/middleware/nodeid"
2526 core "github.com/authzed/spicedb/pkg/proto/core/v1"
2627 v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1"
28+ "github.com/authzed/spicedb/pkg/query"
29+ "github.com/authzed/spicedb/pkg/schema/v2"
2730 "github.com/authzed/spicedb/pkg/tuple"
2831)
2932
@@ -471,12 +474,166 @@ func (ld *localDispatcher) DispatchLookupSubjects(
471474 )
472475}
473476
474- // DispatchQueryPlan implements dispatch.Plan interface
477+ // DispatchQueryPlan implements dispatch.Plan interface.
478+ // It loads the schema, compiles the plan, finds the subtree by canonical key,
479+ // and executes it locally. The Impl method is called directly on the found
480+ // iterator to avoid re-triggering the executor's dispatch decision on the
481+ // same alias boundary that was already dispatched by the caller.
475482func (ld * localDispatcher ) DispatchQueryPlan (
476483 req * v1.DispatchQueryPlanRequest ,
477484 stream dispatch.PlanStream ,
478485) error {
479- return errors .New ("DispatchQueryPlan not yet implemented" )
486+ ctx := stream .Context ()
487+
488+ planCtx := req .PlanContext
489+ if planCtx == nil {
490+ return errors .New ("DispatchQueryPlan: missing plan_context" )
491+ }
492+
493+ revision , err := ld .parseRevision (ctx , planCtx .Revision )
494+ if err != nil {
495+ return err
496+ }
497+
498+ // Load schema at the requested revision.
499+ // TODO: use cached compiled plans (Phase 7) instead of recompiling each time.
500+ it , err := ld .findIteratorByCanonicalKey (ctx , revision , query .CanonicalKey (req .CanonicalKey ))
501+ if err != nil {
502+ return err
503+ }
504+
505+ // Build execution context with DispatchExecutor so nested alias boundaries
506+ // in the subtree can re-dispatch through the full dispatch chain.
507+ dl := datalayer .MustFromContext (ctx )
508+ executor := dispatch .NewDispatchExecutor (ld , planCtx )
509+ qctx := & query.Context {
510+ Context : ctx ,
511+ Executor : executor ,
512+ Reader : query .NewQueryDatastoreReader (dl .SnapshotReader (revision )),
513+ CaveatRunner : caveats .NewCaveatRunner (caveattypes .Default .TypeSet ),
514+ CaveatContext : dispatch .CaveatContextFromPlanContext (planCtx ),
515+ }
516+
517+ resource := query.Object {ObjectType : req .Resource .Namespace , ObjectID : req .Resource .ObjectId }
518+ subject := query.ObjectAndRelation {
519+ ObjectType : req .Subject .Namespace ,
520+ ObjectID : req .Subject .ObjectId ,
521+ Relation : req .Subject .Relation ,
522+ }
523+
524+ // Call Impl directly — the dispatch boundary has already been crossed.
525+ switch req .Operation {
526+ case v1 .PlanOperation_PLAN_OPERATION_CHECK :
527+ path , err := it .CheckImpl (qctx , resource , subject )
528+ if err != nil {
529+ return err
530+ }
531+ if path != nil {
532+ return stream .Publish (& v1.DispatchQueryPlanResponse {
533+ Paths : []* v1.ResultPath {dispatch .QueryPathToResultPath (path )},
534+ })
535+ }
536+ return nil
537+
538+ case v1 .PlanOperation_PLAN_OPERATION_LOOKUP_RESOURCES :
539+ // TODO: implement caching for LookupResources results
540+ pathSeq , err := it .IterResourcesImpl (qctx , subject , query .NoObjectFilter ())
541+ if err != nil {
542+ return err
543+ }
544+ for path , err := range pathSeq {
545+ if err != nil {
546+ return err
547+ }
548+ if err := stream .Publish (& v1.DispatchQueryPlanResponse {
549+ Paths : []* v1.ResultPath {dispatch .QueryPathToResultPath (path )},
550+ }); err != nil {
551+ return err
552+ }
553+ }
554+ return nil
555+
556+ case v1 .PlanOperation_PLAN_OPERATION_LOOKUP_SUBJECTS :
557+ // TODO: implement caching for LookupSubjects results
558+ pathSeq , err := it .IterSubjectsImpl (qctx , resource , query .NoObjectFilter ())
559+ if err != nil {
560+ return err
561+ }
562+ for path , err := range pathSeq {
563+ if err != nil {
564+ return err
565+ }
566+ if err := stream .Publish (& v1.DispatchQueryPlanResponse {
567+ Paths : []* v1.ResultPath {dispatch .QueryPathToResultPath (path )},
568+ }); err != nil {
569+ return err
570+ }
571+ }
572+ return nil
573+
574+ default :
575+ return fmt .Errorf ("DispatchQueryPlan: unknown operation %v" , req .Operation )
576+ }
577+ }
578+
579+ // findIteratorByCanonicalKey loads the schema at the given revision, compiles
580+ // all permissions, and returns the iterator subtree matching the canonical key.
581+ func (ld * localDispatcher ) findIteratorByCanonicalKey (ctx context.Context , revision datastore.Revision , targetKey query.CanonicalKey ) (query.Iterator , error ) {
582+ dl := datalayer .MustFromContext (ctx )
583+ reader := dl .SnapshotReader (revision )
584+
585+ sr , err := reader .ReadSchema (ctx )
586+ if err != nil {
587+ return nil , fmt .Errorf ("DispatchQueryPlan: failed to read schema: %w" , err )
588+ }
589+
590+ nsDefs , err := sr .ListAllTypeDefinitions (ctx )
591+ if err != nil {
592+ return nil , fmt .Errorf ("DispatchQueryPlan: failed to list type definitions: %w" , err )
593+ }
594+
595+ caveatDefs , err := sr .ListAllCaveatDefinitions (ctx )
596+ if err != nil {
597+ return nil , fmt .Errorf ("DispatchQueryPlan: failed to list caveat definitions: %w" , err )
598+ }
599+
600+ fullSchema , err := schema .BuildSchemaFromDefinitions (
601+ datastore .DefinitionsOf (nsDefs ),
602+ datastore .DefinitionsOf (caveatDefs ),
603+ )
604+ if err != nil {
605+ return nil , fmt .Errorf ("DispatchQueryPlan: failed to build schema: %w" , err )
606+ }
607+
608+ for nsName , def := range fullSchema .Definitions () {
609+ for permName := range def .Permissions () {
610+ co , err := query .BuildOutlineFromSchema (fullSchema , nsName , permName )
611+ if err != nil {
612+ continue
613+ }
614+ it , err := co .Compile ()
615+ if err != nil {
616+ continue
617+ }
618+ if found := findByCanonicalKey (it , targetKey ); found != nil {
619+ return found , nil
620+ }
621+ }
622+ }
623+ return nil , fmt .Errorf ("DispatchQueryPlan: no iterator found for canonical key %q" , targetKey )
624+ }
625+
626+ // findByCanonicalKey recursively searches an iterator tree for a node matching the key.
627+ func findByCanonicalKey (it query.Iterator , key query.CanonicalKey ) query.Iterator {
628+ if it .CanonicalKey () == key {
629+ return it
630+ }
631+ for _ , sub := range it .Subiterators () {
632+ if found := findByCanonicalKey (sub , key ); found != nil {
633+ return found
634+ }
635+ }
636+ return nil
480637}
481638
482639func (ld * localDispatcher ) Close () error {
0 commit comments