From b13e6bed4bf8c52aeee21d57d8464a512639ca4f Mon Sep 17 00:00:00 2001 From: Matthew McNeely Date: Fri, 16 May 2025 15:48:00 -0400 Subject: [PATCH 1/4] Update copyright headers --- admin_test.go | 15 ++------------- api/types.go | 5 +++++ api/types_test.go | 5 +++++ delete_test.go | 15 ++------------- engine.go | 1 + examples/basic/main.go | 5 +++++ examples/readme/main.go | 5 +++++ insert_test.go | 15 ++------------- unit_test/conn_test.go | 15 ++------------- update_test.go | 15 ++------------- util_test.go | 5 +++++ 11 files changed, 36 insertions(+), 65 deletions(-) diff --git a/admin_test.go b/admin_test.go index be8e134..d8c25ef 100644 --- a/admin_test.go +++ b/admin_test.go @@ -1,17 +1,6 @@ /* - * Copyright 2025 Hypermode Inc. and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * SPDX-FileCopyrightText: © Hypermode Inc. + * SPDX-License-Identifier: Apache-2.0 */ package modusgraph_test diff --git a/api/types.go b/api/types.go index e524381..6d4c8cb 100644 --- a/api/types.go +++ b/api/types.go @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: © Hypermode Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package api type Point struct { diff --git a/api/types_test.go b/api/types_test.go index dd229c7..7b199cb 100644 --- a/api/types_test.go +++ b/api/types_test.go @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: © Hypermode Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package api import ( diff --git a/delete_test.go b/delete_test.go index a988990..8212bc3 100644 --- a/delete_test.go +++ b/delete_test.go @@ -1,17 +1,6 @@ /* - * Copyright 2025 Hypermode Inc. and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * SPDX-FileCopyrightText: © Hypermode Inc. + * SPDX-License-Identifier: Apache-2.0 */ package modusgraph_test diff --git a/engine.go b/engine.go index b8948f5..637841d 100644 --- a/engine.go +++ b/engine.go @@ -277,6 +277,7 @@ func (engine *Engine) queryWithLock(ctx context.Context, return nil, ErrClosedEngine } + engine.logger.V(2).Info("Querying namespace", "namespaceID", ns.ID(), "query", q) ctx = x.AttachNamespace(ctx, ns.ID()) return (&edgraph.Server{}).QueryNoAuth(ctx, &api.Request{ ReadOnly: true, diff --git a/examples/basic/main.go b/examples/basic/main.go index a1274e4..d1ac658 100644 --- a/examples/basic/main.go +++ b/examples/basic/main.go @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: © Hypermode Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package main import ( diff --git a/examples/readme/main.go b/examples/readme/main.go index 461d3b9..20f3948 100644 --- a/examples/readme/main.go +++ b/examples/readme/main.go @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: © Hypermode Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package main import ( diff --git a/insert_test.go b/insert_test.go index 5b9ab0f..4009ddd 100644 --- a/insert_test.go +++ b/insert_test.go @@ -1,17 +1,6 @@ /* - * Copyright 2025 Hypermode Inc. and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * SPDX-FileCopyrightText: © Hypermode Inc. + * SPDX-License-Identifier: Apache-2.0 */ package modusgraph_test diff --git a/unit_test/conn_test.go b/unit_test/conn_test.go index f61c76f..f65b4ad 100644 --- a/unit_test/conn_test.go +++ b/unit_test/conn_test.go @@ -1,17 +1,6 @@ /* - * Copyright 2025 Hypermode Inc. and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * SPDX-FileCopyrightText: © Hypermode Inc. + * SPDX-License-Identifier: Apache-2.0 */ package unit_test diff --git a/update_test.go b/update_test.go index 3275c9f..be7291b 100644 --- a/update_test.go +++ b/update_test.go @@ -1,17 +1,6 @@ /* - * Copyright 2025 Hypermode Inc. and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * SPDX-FileCopyrightText: © Hypermode Inc. + * SPDX-License-Identifier: Apache-2.0 */ package modusgraph_test diff --git a/util_test.go b/util_test.go index 3e0a9b9..958b802 100644 --- a/util_test.go +++ b/util_test.go @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: © Hypermode Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package modusgraph_test import ( From b783f50b08d0433e955426f739bc0fdfe91ca69a Mon Sep 17 00:00:00 2001 From: Matthew McNeely Date: Fri, 16 May 2025 15:48:40 -0400 Subject: [PATCH 2/4] Add vars map to QueryRaw --- client.go | 17 +++++++++++------ cmd/query/main.go | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/client.go b/client.go index 3013c0c..971faca 100644 --- a/client.go +++ b/client.go @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: © Hypermode Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + package modusgraph import ( @@ -27,7 +32,7 @@ type Client interface { GetSchema(context.Context) (string, error) DropAll(context.Context) error DropData(context.Context) error - QueryRaw(context.Context, string) ([]byte, error) + QueryRaw(context.Context, string, map[string]string) ([]byte, error) DgraphClient() (*dgo.Dgraph, func(), error) } @@ -288,7 +293,7 @@ func (c client) Query(ctx context.Context, model any) *dg.Query { } defer c.pool.put(client) - txn := dg.NewTxn(client) + txn := dg.NewReadOnlyTxnContext(ctx, client) return txn.Get(model) } @@ -342,11 +347,11 @@ func (c client) DropData(ctx context.Context) error { return client.Alter(ctx, &api.Operation{DropOp: api.Operation_DATA}) } -// QueryRaw implements raw querying (DQL syntax). -func (c client) QueryRaw(ctx context.Context, q string) ([]byte, error) { +// QueryRaw implements raw querying (DQL syntax) and optional variables. +func (c client) QueryRaw(ctx context.Context, q string, vars map[string]string) ([]byte, error) { if c.engine != nil { ns := c.engine.GetDefaultNamespace() - resp, err := ns.Query(ctx, q) + resp, err := ns.QueryWithVars(ctx, q, vars) if err != nil { return nil, err } @@ -361,7 +366,7 @@ func (c client) QueryRaw(ctx context.Context, q string) ([]byte, error) { defer c.pool.put(client) txn := dg.NewReadOnlyTxnContext(ctx, client) - resp, err := txn.Txn().Query(ctx, q) + resp, err := txn.Txn().QueryWithVars(ctx, q, vars) if err != nil { return nil, err } diff --git a/cmd/query/main.go b/cmd/query/main.go index f1d8767..9eb046f 100644 --- a/cmd/query/main.go +++ b/cmd/query/main.go @@ -97,7 +97,7 @@ func main() { start := time.Now() // Execute the query - resp, err := client.QueryRaw(ctx, query) + resp, err := client.QueryRaw(ctx, query, nil) if err != nil { logger.Error(err, "Query execution failed") os.Exit(1) From 57998521e5c633725f5523c63b0b55678e6902eb Mon Sep 17 00:00:00 2001 From: Matthew McNeely Date: Fri, 16 May 2025 15:48:55 -0400 Subject: [PATCH 3/4] Add additional tests --- client_test.go | 210 +++++++++++++++++++++++++++++++++++++++++++++++++ query_test.go | 89 +++++++++++++++++---- 2 files changed, 282 insertions(+), 17 deletions(-) create mode 100644 client_test.go diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..9b7184f --- /dev/null +++ b/client_test.go @@ -0,0 +1,210 @@ +/* + * SPDX-FileCopyrightText: © Hypermode Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +package modusgraph_test + +import ( + "context" + "fmt" + "os" + "sync" + "testing" + "time" + + mg "github.com/hypermodeinc/modusgraph" + "github.com/stretchr/testify/require" +) + +func TestClientPool(t *testing.T) { + testCases := []struct { + name string + uri string + skip bool + }{ + { + name: "ClientPoolWithFileURI", + uri: "file://" + t.TempDir(), + }, + { + name: "ClientPoolWithDgraphURI", + 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.Skip("Skipping test as MODUSGRAPH_TEST_ADDR is not set") + } + + // Create a client with pool size 10 + client, err := mg.NewClient(tc.uri, mg.WithPoolSize(10)) + require.NoError(t, err) + defer client.Close() + + // Test concurrent client pool usage + const numWorkers = 20 + var wg sync.WaitGroup + var mu sync.Mutex + var clientCount int + + for i := 0; i < numWorkers; i++ { + wg.Add(1) + go func() { + defer wg.Done() + + // Get a client from the pool + client, cleanup, err := client.DgraphClient() + require.NoError(t, err) + require.NotNil(t, client) + txn := client.NewReadOnlyTxn() + ctx := context.Background() + _, err = txn.Query(ctx, "query { q(func: uid(1)) { uid } }") + require.NoError(t, err) + err = txn.Discard(ctx) + require.NoError(t, err) + + // Verify we got a valid Dgraph client + if client != nil { + mu.Lock() + clientCount++ + mu.Unlock() + } + + // Clean up the client + cleanup() + }() + } + + // Wait for all workers to complete + wg.Wait() + + // Verify we got clients from the pool + require.GreaterOrEqual(t, clientCount, 1) + + // Get a client before close + beforeClient, cleanupBefore, err := client.DgraphClient() + require.NoError(t, err) + require.NotNil(t, beforeClient) + + // Close the client pool + client.Close() + time.Sleep(100 * time.Millisecond) // Give some time for cleanup + + // Verify we can still get a new client after close (pool will create a new one) + afterClient, cleanupAfter, err := client.DgraphClient() + require.NoError(t, err) + require.NotNil(t, afterClient) + + // Verify the client is actually new + require.NotEqual(t, fmt.Sprintf("%p", beforeClient), fmt.Sprintf("%p", afterClient)) + + // Clean up the client + cleanupAfter() + + // Also clean up the before client if it wasn't already closed + cleanupBefore() + }) + } + + // Reset singleton at the end of the test to ensure the next test can start fresh + mg.ResetSingleton() +} + +func TestClientPoolStress(t *testing.T) { + testCases := []struct { + name string + uri string + skip bool + }{ + { + name: "ClientPoolStressWithFileURI", + uri: "file://" + t.TempDir(), + }, + { + name: "ClientPoolStressWithDgraphURI", + 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.Skip("Skipping test as MODUSGRAPH_TEST_ADDR is not set") + } + + // Create a client with pool size 10 + client, err := mg.NewClient(tc.uri, mg.WithPoolSize(10)) + require.NoError(t, err) + defer func() { + client.Close() + }() + + // Test concurrent client pool usage with high load + const numWorkers = 20 + const iterations = 10 + var wg sync.WaitGroup + var successCount int + var errorCount int + var mu sync.Mutex + + for i := 0; i < numWorkers; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < iterations; j++ { + dgraphClient, cleanup, err := client.DgraphClient() + if err != nil { + mu.Lock() + errorCount++ + mu.Unlock() + continue + } + + if dgraphClient != nil { + // Test the client works + txn := dgraphClient.NewReadOnlyTxn() + ctx := context.Background() + _, err = txn.Query(ctx, "query { q(func: uid(1)) { uid } }") + if err != nil { + err = txn.Discard(ctx) + if err != nil { + mu.Lock() + errorCount++ + mu.Unlock() + } + cleanup() + continue + } + err = txn.Discard(ctx) + if err != nil { + mu.Lock() + errorCount++ + mu.Unlock() + cleanup() + continue + } + + mu.Lock() + successCount++ + mu.Unlock() + } + + // Clean up the client + cleanup() + } + }() + } + + wg.Wait() + + require.Greater(t, successCount, 0) + }) + + mg.ResetSingleton() + } +} diff --git a/query_test.go b/query_test.go index b8279a0..2292f80 100644 --- a/query_test.go +++ b/query_test.go @@ -1,17 +1,6 @@ /* - * Copyright 2025 Hypermode Inc. and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * SPDX-FileCopyrightText: © Hypermode Inc. + * SPDX-License-Identifier: Apache-2.0 */ package modusgraph_test @@ -73,10 +62,16 @@ func TestClientSimpleGet(t *testing.T) { } } +type GeoLocation struct { + Type string `json:"type"` + Coord []float64 `json:"coordinates"` +} + type QueryTestRecord struct { - Name string `json:"name,omitempty" dgraph:"index=exact,term unique"` - Age int `json:"age,omitempty"` - BirthDate time.Time `json:"birthDate,omitzero"` + Name string `json:"name,omitempty" dgraph:"index=exact,term unique"` + Age int `json:"age,omitempty" dgraph:"index=int"` + BirthDate time.Time `json:"birthDate,omitzero"` + Location *GeoLocation `json:"location,omitempty" dgraph:"type=geo index=geo"` UID string `json:"uid,omitempty"` DType []string `json:"dgraph.type,omitempty"` @@ -100,6 +95,18 @@ func TestClientQuery(t *testing.T) { }, } + locations := [][]float64{ + {-122.4194, 37.7749}, // San Francisco, USA + {2.2945, 48.8584}, // Paris (Eiffel Tower), France + {-74.0060, 40.7128}, // New York City, USA + {-0.1276, 51.5072}, // London, UK + {139.7690, 35.6804}, // Tokyo, Japan + {77.2090, 28.6139}, // New Delhi, India + {31.2357, 30.0444}, // Cairo, Egypt + {151.2093, -33.8688}, // Sydney, Australia + {-43.1729, -22.9068}, // Rio de Janeiro, Brazil + {116.4074, 39.9042}, // Beijing, China + } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { if tc.skip { @@ -117,6 +124,10 @@ func TestClientQuery(t *testing.T) { Name: fmt.Sprintf("Test Entity %d", i), Age: 30 + i, BirthDate: birthDate.AddDate(0, 0, i), + Location: &GeoLocation{ + Type: "Point", + Coord: locations[i], + }, } } ctx := context.Background() @@ -140,6 +151,7 @@ func TestClientQuery(t *testing.T) { require.Equal(t, result[i].Name, fmt.Sprintf("Test Entity %d", i), "Name should match") require.Equal(t, result[i].Age, 30+i, "Age should match") require.Equal(t, result[i].BirthDate, birthDate.AddDate(0, 0, i), "BirthDate should match") + require.Equal(t, result[i].Location.Coord, locations[i], "Location coordinates should match") } }) @@ -156,6 +168,33 @@ func TestClientQuery(t *testing.T) { } }) + t.Run("QueryWithGeoFilters", func(t *testing.T) { + var result []QueryTestRecord + err := client.Query(ctx, QueryTestRecord{}). + Filter(`(near(location, [2.2946, 48.8585], 1000))`). // just a few meters from the Eiffel Tower + Nodes(&result) + require.NoError(t, err, "Query should succeed") + require.Len(t, result, 1, "Should have 1 entity") + require.Equal(t, result[0].Name, "Test Entity 1", "Name should match") + + var rawResult struct { + Data []QueryTestRecord `json:"q"` + } + parisQuery := `query { + q(func: within(location, [[[2.2945, 48.8584], [2.2690, 48.8800], [2.3300, 48.9000], + [2.4100, 48.8800], [2.4150, 48.8300], [2.3650, 48.8150], + [2.3000, 48.8100], [2.2600, 48.8350], [2.2945, 48.8584]]])) { + uid + name + } + }` + resp, err := client.QueryRaw(ctx, parisQuery, nil) + require.NoError(t, err, "Query should succeed") + require.NoError(t, json.Unmarshal(resp, &rawResult), "Failed to unmarshal response") + require.Len(t, rawResult.Data, 1, "Should have 1 entity") + require.Equal(t, rawResult.Data[0].Name, "Test Entity 1", "Name should match") + }) + t.Run("QueryWithPagination", func(t *testing.T) { var result []QueryTestRecord count, err := client.Query(ctx, QueryTestRecord{}).First(5).NodesAndCount(&result) @@ -182,7 +221,8 @@ func TestClientQuery(t *testing.T) { Data []QueryTestRecord `json:"q"` } resp, err := client.QueryRaw(ctx, - `query { q(func: type(QueryTestRecord), orderasc: age) { uid name age birthDate }}`) + `query { q(func: type(QueryTestRecord), orderasc: age) { uid name age birthDate }}`, + nil) require.NoError(t, err, "Query should succeed") require.NoError(t, json.Unmarshal(resp, &result), "Failed to unmarshal response") require.Len(t, result.Data, 10, "Should have 10 entities") @@ -192,6 +232,21 @@ func TestClientQuery(t *testing.T) { require.Equal(t, result.Data[i].BirthDate, birthDate.AddDate(0, 0, i), "BirthDate should match") } }) + + t.Run("QueryRawWithVars", func(t *testing.T) { + var result struct { + Data []QueryTestRecord `json:"q"` + } + resp, err := client.QueryRaw(ctx, + `query older_than_inclusive($1: int) { q(func: ge(age, $1)) { uid name age }}`, + map[string]string{"$1": "38"}) + require.NoError(t, err, "Query should succeed") + require.NoError(t, json.Unmarshal(resp, &result), "Failed to unmarshal response") + require.Len(t, result.Data, 2, "Should have 2 entities") + for i := range 2 { + require.GreaterOrEqual(t, result.Data[i].Age, 38, "Age should be greater than or equal to 38") + } + }) }) } } From 26c8534c289f540ac77a4c4348aa80a2bc877f5a Mon Sep 17 00:00:00 2001 From: Matthew McNeely Date: Sat, 17 May 2025 15:14:16 -0400 Subject: [PATCH 4/4] Update comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- client.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client.go b/client.go index 971faca..4000879 100644 --- a/client.go +++ b/client.go @@ -32,6 +32,9 @@ type Client interface { GetSchema(context.Context) (string, error) DropAll(context.Context) error DropData(context.Context) error + // QueryRaw executes a raw Dgraph query with optional query variables. + // The `query` parameter is the Dgraph query string. + // The `vars` parameter is a map of variable names to their values, used to parameterize the query. QueryRaw(context.Context, string, map[string]string) ([]byte, error) DgraphClient() (*dgo.Dgraph, func(), error)