From bd16559b97c3206c9f4897318ed994b6b9c2cb7c Mon Sep 17 00:00:00 2001 From: Michael Welles Date: Thu, 4 Jun 2026 16:11:23 -0400 Subject: [PATCH] feat: AlterSchema, dropPredicate, and embedded DropAttr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds raw schema-DDL primitives that complement UpdateSchema's object-template inference: - Client.AlterSchema(ctx, schema) applies a raw DQL schema string directly, giving full control over predicate types, indexes, and directives — useful for migrations that declare predicates no Go type models yet. - Engine.dropPredicate deletes a single predicate (and its data) from the embedded engine via posting.DeletePredicate. - embedded_client.go routes an Alter carrying DropAttr to dropPredicate, so the embedded path matches a remote Dgraph cluster's DropAttr behavior. TestDropPredicateEmbedded exercises the full declare/insert/drop cycle against the embedded engine. --- client.go | 19 +++++++++++ embedded_client.go | 6 ++++ engine.go | 19 +++++++++++ schema_ddl_test.go | 84 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 128 insertions(+) create mode 100644 schema_ddl_test.go diff --git a/client.go b/client.go index be9813b..2189a3a 100644 --- a/client.go +++ b/client.go @@ -69,6 +69,12 @@ type Client interface { // Pass one or more objects that will be used as templates for the schema. UpdateSchema(context.Context, ...any) error + // AlterSchema applies a raw Dgraph Schema Definition Language string directly, + // bypassing the object-template inference of UpdateSchema. Use it when you need + // full control over predicate types, indexes, and directives — for example, + // schema migrations that declare predicates no Go type models yet. + AlterSchema(ctx context.Context, schema string) error + // GetSchema retrieves the current schema definition from the database. // Returns a string containing the full schema in Dgraph Schema Definition Language. GetSchema(context.Context) (string, error) @@ -585,6 +591,19 @@ func (c client) Query(ctx context.Context, model any) *dg.Query { return txn.Get(model).All(c.options.maxEdgeTraversal) } +// AlterSchema applies a raw DQL schema string directly via Dgraph Alter, +// without the object-template inference performed by UpdateSchema. +func (c client) AlterSchema(ctx context.Context, schema string) error { + dgClient, err := c.pool.get() + if err != nil { + c.logger.Error(err, "Failed to get client from pool") + return err + } + defer c.pool.put(dgClient) + + return dgClient.Alter(ctx, &api.Operation{Schema: schema}) +} + // UpdateSchema implements updating the Dgraph schema. Pass one or more // objects that will be used to generate the schema. // If any object contains SimString fields tagged `dgraph:"embedding"`, the diff --git a/embedded_client.go b/embedded_client.go index 329f4bf..fbe993d 100644 --- a/embedded_client.go +++ b/embedded_client.go @@ -146,6 +146,12 @@ func (c *embeddedDgraphClient) Alter( } return &api.Payload{}, nil } + if in.DropAttr != "" { + if err := c.engine.dropPredicate(ctx, c.ns, in.DropAttr); err != nil { + return nil, err + } + return &api.Payload{}, nil + } if in.Schema != "" { if err := c.engine.alterSchema(ctx, c.ns, in.Schema); err != nil { return nil, err diff --git a/engine.go b/engine.go index d9d236d..34e7f41 100644 --- a/engine.go +++ b/engine.go @@ -271,6 +271,25 @@ func (engine *Engine) dropData(ctx context.Context, ns *Namespace) error { return nil } +// dropPredicate deletes a single predicate (and its data) from the embedded +// engine — the in-process equivalent of a gRPC Alter with DropAttr set. +func (engine *Engine) dropPredicate(ctx context.Context, ns *Namespace, pred string) error { + engine.mutex.Lock() + defer engine.mutex.Unlock() + + if !engine.isOpen.Load() { + return ErrClosedEngine + } + + startTs, err := engine.z.nextTs() + if err != nil { + return err + } + + nsAttr := x.NamespaceAttr(ns.ID(), pred) + return posting.DeletePredicate(ctx, nsAttr, startTs) +} + func (engine *Engine) alterSchema(ctx context.Context, ns *Namespace, sch string) error { engine.mutex.Lock() defer engine.mutex.Unlock() diff --git a/schema_ddl_test.go b/schema_ddl_test.go new file mode 100644 index 0000000..8315dd2 --- /dev/null +++ b/schema_ddl_test.go @@ -0,0 +1,84 @@ +/* + * SPDX-FileCopyrightText: © 2017-2026 Istari Digital, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +package modusgraph_test + +import ( + "context" + "os" + "testing" + + "github.com/dgraph-io/dgo/v250/protos/api" + "github.com/stretchr/testify/require" +) + +// TestDropPredicateEmbedded exercises the schema-DDL surface end-to-end: +// Client.AlterSchema declares a raw predicate, and a gRPC Alter with DropAttr +// routes through embedded_client.go's DropAttr arm into engine.dropPredicate. +func TestDropPredicateEmbedded(t *testing.T) { + testCases := []struct { + name string + uri string + skip bool + }{ + { + name: "DropPredicateWithFileURI", + uri: "file://" + GetTempDir(t), + }, + { + name: "DropPredicateWithDgraphURI", + uri: "dgraph://" + os.Getenv("MODUSGRAPH_TEST_ADDR"), + skip: os.Getenv("MODUSGRAPH_TEST_ADDR") == "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if tc.skip { + t.Skipf("Skipping %s: MODUSGRAPH_TEST_ADDR not set", tc.name) + return + } + + client, cleanup := CreateTestClient(t, tc.uri) + defer cleanup() + + ctx := context.Background() + + // Declare an indexed string predicate and insert a node carrying it. + err := client.AlterSchema(ctx, "dropme: string @index(exact) .") + require.NoError(t, err, "AlterSchema should succeed") + + dg, dgCleanup, err := client.DgraphClient() + require.NoError(t, err, "DgraphClient should succeed") + defer dgCleanup() + + _, err = dg.NewTxn().Mutate(ctx, &api.Mutation{ + SetJson: []byte(`[{"dropme":"hello"}]`), + CommitNow: true, + }) + require.NoError(t, err, "mutate should succeed") + + // Confirm the predicate is present before the drop. + raw, err := client.QueryRaw(ctx, `{ q(func: has(dropme)) { c: count(uid) } }`, nil) + require.NoError(t, err, "count query should succeed") + require.Contains(t, string(raw), `"c":1`, "predicate present before drop") + + // Drop the predicate via the public path; this exercises + // embedded_client.go's DropAttr arm + engine.dropPredicate. + err = dg.Alter(ctx, &api.Operation{DropAttr: "dropme"}) + require.NoError(t, err, "DropAttr should succeed") + + // Confirm the data is gone (no nodes have the predicate). + raw, err = client.QueryRaw(ctx, `{ q(func: has(dropme)) { c: count(uid) } }`, nil) + require.NoError(t, err, "count query should succeed after drop") + require.Contains(t, string(raw), `"c":0`, "predicate values gone after drop") + + // Confirm the schema entry is gone. + raw, err = client.QueryRaw(ctx, `schema(pred: [dropme]) { type }`, nil) + require.NoError(t, err, "schema query should succeed after drop") + require.NotContains(t, string(raw), "dropme", "predicate schema entry gone after drop") + }) + } +}