From 315b236d05d5053f344c617c948388fc4fed1d0a Mon Sep 17 00:00:00 2001
From: Tolga Ozen
Date: Sat, 22 Nov 2025 14:08:24 +0300
Subject: [PATCH 1/2] feat(docs): update README badges for commit activity and
workflow status
---
README.md | 6 +-
go.work.sum | 5 +
internal/schema/linked_schema_test.go | 477 ++++++++++++++++++++++++++
3 files changed, 485 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index b193b0bc9..3c40b96b3 100644
--- a/README.md
+++ b/README.md
@@ -39,10 +39,10 @@
-
-
+
+
-
+
diff --git a/go.work.sum b/go.work.sum
index bbccad77c..22cc06c52 100644
--- a/go.work.sum
+++ b/go.work.sum
@@ -119,6 +119,7 @@ filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4
github.com/ClickHouse/ch-go v0.67.0/go.mod h1:2MSAeyVmgt+9a2k2SQPPG1b4qbTPzdGDpf1+bcHh+18=
github.com/ClickHouse/clickhouse-go/v2 v2.40.1/go.mod h1:GDzSBLVhladVm8V01aEB36IoBOVLLICfyeuiIp/8Ezc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
github.com/IBM/sarama v1.43.1/go.mod h1:GG5q1RURtDNPz8xxJs3mgX6Ytak8Z9eLhAkJPObe2xE=
github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@@ -139,11 +140,13 @@ github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFP
github.com/elastic/go-sysinfo v1.15.4/go.mod h1:ZBVXmqS368dOn/jvijV/zHLfakWTYHBZPk3G244lHrU=
github.com/elastic/go-windows v1.0.2/go.mod h1:bGcDpBzXgYSqM0Gx3DM4+UxFj300SZLixie9u9ixLM8=
github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
+github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo=
+github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
@@ -198,6 +201,7 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
+github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
github.com/timandy/routine v1.1.6/go.mod h1:kXslgIosdY8LW0byTyPnenDgn4/azt2euufAq9rK51w=
github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d/go.mod h1:l8xTsYB90uaVdMHXMCxKKLSgw5wLYBwBKKefNIUnm9s=
github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po=
@@ -210,6 +214,7 @@ github.com/ydb-platform/ydb-go-sdk/v3 v3.108.1/go.mod h1:l5sSv153E18VvYcsmr51hok
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k=
+go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE=
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE=
diff --git a/internal/schema/linked_schema_test.go b/internal/schema/linked_schema_test.go
index 44e1206b2..9c99db360 100644
--- a/internal/schema/linked_schema_test.go
+++ b/internal/schema/linked_schema_test.go
@@ -1822,4 +1822,481 @@ var _ = Describe("linked schema", func() {
}))
})
})
+
+ Context("BuildRelationPathChain", func() {
+ It("should find direct relation path", func() {
+ sch, err := parser.NewParser(`
+ entity user {}
+ entity document {
+ relation viewer @user
+ }
+ `).Parse()
+
+ Expect(err).ShouldNot(HaveOccurred())
+
+ c := compiler.NewCompiler(true, sch)
+ a, _, _ := c.Compile()
+
+ g := NewLinkedGraph(NewSchemaFromEntityAndRuleDefinitions(a, nil))
+
+ path, err := g.BuildRelationPathChain("document", "user")
+
+ Expect(err).ShouldNot(HaveOccurred())
+ Expect(path).Should(HaveLen(1))
+ Expect(path[0].Type).Should(Equal("document"))
+ Expect(path[0].Relation).Should(Equal("viewer"))
+ })
+
+ It("should find multi-hop path", func() {
+ sch, err := parser.NewParser(`
+ entity user {}
+ entity organization {
+ relation admin @user
+ }
+ entity document {
+ relation org @organization
+ }
+ `).Parse()
+
+ Expect(err).ShouldNot(HaveOccurred())
+
+ c := compiler.NewCompiler(true, sch)
+ a, _, _ := c.Compile()
+
+ g := NewLinkedGraph(NewSchemaFromEntityAndRuleDefinitions(a, nil))
+
+ path, err := g.BuildRelationPathChain("document", "user")
+
+ Expect(err).ShouldNot(HaveOccurred())
+ Expect(path).Should(HaveLen(2))
+ Expect(path[0].Type).Should(Equal("document"))
+ Expect(path[0].Relation).Should(Equal("org"))
+ Expect(path[1].Type).Should(Equal("organization"))
+ Expect(path[1].Relation).Should(Equal("admin"))
+ })
+
+ It("should return error when no path found", func() {
+ sch, err := parser.NewParser(`
+ entity user {}
+ entity document {
+ relation viewer @user
+ }
+ entity isolated {
+ relation owner @user
+ }
+ `).Parse()
+
+ Expect(err).ShouldNot(HaveOccurred())
+
+ c := compiler.NewCompiler(true, sch)
+ a, _, _ := c.Compile()
+
+ g := NewLinkedGraph(NewSchemaFromEntityAndRuleDefinitions(a, nil))
+
+ path, err := g.BuildRelationPathChain("document", "isolated")
+
+ Expect(err).Should(HaveOccurred())
+ Expect(err.Error()).Should(Equal("no path found between entity types"))
+ Expect(path).Should(BeNil())
+ })
+
+ It("should find path in complex multi-level schema", func() {
+ sch, err := parser.NewParser(`
+ entity user {}
+ entity organization {
+ relation admin @user
+ }
+ entity container {
+ relation parent @organization
+ }
+ entity document {
+ relation container @container
+ }
+ `).Parse()
+
+ Expect(err).ShouldNot(HaveOccurred())
+
+ c := compiler.NewCompiler(true, sch)
+ a, _, _ := c.Compile()
+
+ g := NewLinkedGraph(NewSchemaFromEntityAndRuleDefinitions(a, nil))
+
+ path, err := g.BuildRelationPathChain("document", "user")
+
+ Expect(err).ShouldNot(HaveOccurred())
+ Expect(path).Should(HaveLen(3))
+ Expect(path[0].Type).Should(Equal("document"))
+ Expect(path[0].Relation).Should(Equal("container"))
+ Expect(path[1].Type).Should(Equal("container"))
+ Expect(path[1].Relation).Should(Equal("parent"))
+ Expect(path[2].Type).Should(Equal("organization"))
+ Expect(path[2].Relation).Should(Equal("admin"))
+ })
+
+ It("should handle self-referential relation (same source and target entity type)", func() {
+ sch, err := parser.NewParser(`
+ entity user {}
+ entity account {
+ relation admin @user @account#admin
+ }
+ `).Parse()
+
+ Expect(err).ShouldNot(HaveOccurred())
+
+ c := compiler.NewCompiler(true, sch)
+ a, _, _ := c.Compile()
+
+ g := NewLinkedGraph(NewSchemaFromEntityAndRuleDefinitions(a, nil))
+
+ path, err := g.BuildRelationPathChain("account", "account")
+
+ Expect(err).ShouldNot(HaveOccurred())
+ Expect(path).Should(HaveLen(1))
+ Expect(path[0].Type).Should(Equal("account"))
+ Expect(path[0].Relation).Should(Equal("admin"))
+ })
+
+ It("should handle non-existent source entity", func() {
+ sch, err := parser.NewParser(`
+ entity user {}
+ entity document {
+ relation viewer @user
+ }
+ `).Parse()
+
+ Expect(err).ShouldNot(HaveOccurred())
+
+ c := compiler.NewCompiler(true, sch)
+ a, _, _ := c.Compile()
+
+ g := NewLinkedGraph(NewSchemaFromEntityAndRuleDefinitions(a, nil))
+
+ path, err := g.BuildRelationPathChain("nonexistent", "user")
+
+ Expect(err).Should(HaveOccurred())
+ Expect(err.Error()).Should(Equal("no path found between entity types"))
+ Expect(path).Should(BeNil())
+ })
+
+ It("should handle non-existent target entity", func() {
+ sch, err := parser.NewParser(`
+ entity user {}
+ entity document {
+ relation viewer @user
+ }
+ `).Parse()
+
+ Expect(err).ShouldNot(HaveOccurred())
+
+ c := compiler.NewCompiler(true, sch)
+ a, _, _ := c.Compile()
+
+ g := NewLinkedGraph(NewSchemaFromEntityAndRuleDefinitions(a, nil))
+
+ path, err := g.BuildRelationPathChain("document", "nonexistent")
+
+ Expect(err).Should(HaveOccurred())
+ Expect(err.Error()).Should(Equal("no path found between entity types"))
+ Expect(path).Should(BeNil())
+ })
+ })
+
+ Context("GetSubjectRelationForPathWalk", func() {
+ It("should return empty string for simple relation without relation name", func() {
+ sch, err := parser.NewParser(`
+ entity user {}
+ entity document {
+ relation viewer @user
+ }
+ `).Parse()
+
+ Expect(err).ShouldNot(HaveOccurred())
+
+ c := compiler.NewCompiler(true, sch)
+ a, _, _ := c.Compile()
+
+ g := NewLinkedGraph(NewSchemaFromEntityAndRuleDefinitions(a, nil))
+
+ relation := g.GetSubjectRelationForPathWalk("document", "viewer", "user")
+
+ Expect(relation).Should(Equal(""))
+ })
+
+ It("should return relation for complex relation with relation name", func() {
+ sch, err := parser.NewParser(`
+ entity user {}
+ entity group {
+ relation member @user
+ }
+ entity document {
+ relation viewer @user @group#member
+ }
+ `).Parse()
+
+ Expect(err).ShouldNot(HaveOccurred())
+
+ c := compiler.NewCompiler(true, sch)
+ a, _, _ := c.Compile()
+
+ g := NewLinkedGraph(NewSchemaFromEntityAndRuleDefinitions(a, nil))
+
+ relation := g.GetSubjectRelationForPathWalk("document", "viewer", "group")
+
+ Expect(relation).Should(Equal("member"))
+ })
+
+ It("should return empty string for non-existent entity", func() {
+ sch, err := parser.NewParser(`
+ entity user {}
+ entity document {
+ relation viewer @user
+ }
+ `).Parse()
+
+ Expect(err).ShouldNot(HaveOccurred())
+
+ c := compiler.NewCompiler(true, sch)
+ a, _, _ := c.Compile()
+
+ g := NewLinkedGraph(NewSchemaFromEntityAndRuleDefinitions(a, nil))
+
+ relation := g.GetSubjectRelationForPathWalk("nonexistent", "viewer", "user")
+
+ Expect(relation).Should(Equal(""))
+ })
+
+ It("should return empty string for non-existent relation", func() {
+ sch, err := parser.NewParser(`
+ entity user {}
+ entity document {
+ relation viewer @user
+ }
+ `).Parse()
+
+ Expect(err).ShouldNot(HaveOccurred())
+
+ c := compiler.NewCompiler(true, sch)
+ a, _, _ := c.Compile()
+
+ g := NewLinkedGraph(NewSchemaFromEntityAndRuleDefinitions(a, nil))
+
+ relation := g.GetSubjectRelationForPathWalk("document", "nonexistent", "user")
+
+ Expect(relation).Should(Equal(""))
+ })
+
+ It("should return empty string when target entity type not found in relation references", func() {
+ sch, err := parser.NewParser(`
+ entity user {}
+ entity organization {
+ relation admin @user
+ }
+ entity document {
+ relation viewer @user
+ }
+ `).Parse()
+
+ Expect(err).ShouldNot(HaveOccurred())
+
+ c := compiler.NewCompiler(true, sch)
+ a, _, _ := c.Compile()
+
+ g := NewLinkedGraph(NewSchemaFromEntityAndRuleDefinitions(a, nil))
+
+ relation := g.GetSubjectRelationForPathWalk("document", "viewer", "organization")
+
+ Expect(relation).Should(Equal(""))
+ })
+
+ It("should return correct relation for multiple relation references", func() {
+ sch, err := parser.NewParser(`
+ entity user {}
+ entity group {
+ relation member @user
+ relation admin @user
+ }
+ entity document {
+ relation viewer @user @group#member @group#admin
+ }
+ `).Parse()
+
+ Expect(err).ShouldNot(HaveOccurred())
+
+ c := compiler.NewCompiler(true, sch)
+ a, _, _ := c.Compile()
+
+ g := NewLinkedGraph(NewSchemaFromEntityAndRuleDefinitions(a, nil))
+
+ relation := g.GetSubjectRelationForPathWalk("document", "viewer", "group")
+
+ Expect(relation).Should(Equal("member"))
+ })
+ })
+
+ Context("Attribute Reference", func() {
+ It("should return AttributeLinkedEntrance for direct attribute reference", func() {
+ sch, err := parser.NewParser(`
+ entity user {}
+ entity document {
+ relation viewer @user
+ attribute public boolean
+ }
+ `).Parse()
+
+ Expect(err).ShouldNot(HaveOccurred())
+
+ c := compiler.NewCompiler(true, sch)
+ a, _, _ := c.Compile()
+
+ schema := NewSchemaFromEntityAndRuleDefinitions(a, nil)
+ // Manually set attribute as reference to test the attribute reference case
+ docDef := schema.EntityDefinitions["document"]
+ docDef.References["public"] = base.EntityDefinition_REFERENCE_ATTRIBUTE
+
+ g := NewLinkedGraph(schema)
+
+ ent, err := g.LinkedEntrances(&base.Entrance{
+ Type: "document",
+ Value: "public",
+ }, &base.Entrance{
+ Type: "user",
+ Value: "",
+ })
+
+ Expect(err).ShouldNot(HaveOccurred())
+ Expect(ent).Should(Equal([]*LinkedEntrance{
+ {
+ Kind: AttributeLinkedEntrance,
+ TargetEntrance: &base.Entrance{
+ Type: "document",
+ Value: "public",
+ },
+ TupleSetRelation: "",
+ },
+ }))
+ })
+
+ It("should return error when attribute not found", func() {
+ sch, err := parser.NewParser(`
+ entity user {}
+ entity document {
+ relation viewer @user
+ attribute public boolean
+ }
+ `).Parse()
+
+ Expect(err).ShouldNot(HaveOccurred())
+
+ c := compiler.NewCompiler(true, sch)
+ a, _, _ := c.Compile()
+
+ schema := NewSchemaFromEntityAndRuleDefinitions(a, nil)
+ // Manually set nonexistent attribute as reference to test the attribute reference case
+ // This will trigger the attribute reference case but attribute won't be found
+ docDef := schema.EntityDefinitions["document"]
+ docDef.References["nonexistent_attribute"] = base.EntityDefinition_REFERENCE_ATTRIBUTE
+
+ g := NewLinkedGraph(schema)
+
+ ent, err := g.LinkedEntrances(&base.Entrance{
+ Type: "document",
+ Value: "nonexistent_attribute",
+ }, &base.Entrance{
+ Type: "user",
+ Value: "",
+ })
+
+ Expect(err).Should(HaveOccurred())
+ Expect(err.Error()).Should(Equal("attribute not found"))
+ Expect(ent).Should(BeNil())
+ })
+
+ It("should return AttributeLinkedEntrance for multiple attributes", func() {
+ sch, err := parser.NewParser(`
+ entity user {}
+ entity document {
+ relation viewer @user
+ attribute public boolean
+ attribute published boolean
+ attribute archived boolean
+ }
+ `).Parse()
+
+ Expect(err).ShouldNot(HaveOccurred())
+
+ c := compiler.NewCompiler(true, sch)
+ a, _, _ := c.Compile()
+
+ schema := NewSchemaFromEntityAndRuleDefinitions(a, nil)
+ // Manually set attribute as reference to test the attribute reference case
+ docDef := schema.EntityDefinitions["document"]
+ docDef.References["published"] = base.EntityDefinition_REFERENCE_ATTRIBUTE
+
+ g := NewLinkedGraph(schema)
+
+ ent, err := g.LinkedEntrances(&base.Entrance{
+ Type: "document",
+ Value: "published",
+ }, &base.Entrance{
+ Type: "user",
+ Value: "",
+ })
+
+ Expect(err).ShouldNot(HaveOccurred())
+ Expect(ent).Should(Equal([]*LinkedEntrance{
+ {
+ Kind: AttributeLinkedEntrance,
+ TargetEntrance: &base.Entrance{
+ Type: "document",
+ Value: "published",
+ },
+ TupleSetRelation: "",
+ },
+ }))
+ })
+
+ It("should return AttributeLinkedEntrance for different attribute types", func() {
+ sch, err := parser.NewParser(`
+ entity user {}
+ entity document {
+ relation viewer @user
+ attribute public boolean
+ attribute count integer
+ attribute name string
+ }
+ `).Parse()
+
+ Expect(err).ShouldNot(HaveOccurred())
+
+ c := compiler.NewCompiler(true, sch)
+ a, _, _ := c.Compile()
+
+ schema := NewSchemaFromEntityAndRuleDefinitions(a, nil)
+ // Manually set attribute as reference to test the attribute reference case
+ docDef := schema.EntityDefinitions["document"]
+ docDef.References["count"] = base.EntityDefinition_REFERENCE_ATTRIBUTE
+
+ g := NewLinkedGraph(schema)
+
+ ent, err := g.LinkedEntrances(&base.Entrance{
+ Type: "document",
+ Value: "count",
+ }, &base.Entrance{
+ Type: "user",
+ Value: "",
+ })
+
+ Expect(err).ShouldNot(HaveOccurred())
+ Expect(ent).Should(Equal([]*LinkedEntrance{
+ {
+ Kind: AttributeLinkedEntrance,
+ TargetEntrance: &base.Entrance{
+ Type: "document",
+ Value: "count",
+ },
+ TupleSetRelation: "",
+ },
+ }))
+ })
+ })
})
From 9c1ca12b2077e6f9c5f453c1622b9279dc83b6ff Mon Sep 17 00:00:00 2001
From: Tolga Ozen
Date: Sat, 22 Nov 2025 14:27:21 +0300
Subject: [PATCH 2/2] docs: update badge links in README for release and code
coverage
---
README.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index b04f3b99a..3210a115a 100644
--- a/README.md
+++ b/README.md
@@ -38,12 +38,12 @@
-
+
-
-
+
+
