Skip to content
This repository was archived by the owner on Sep 5, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 2 additions & 15 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,9 @@

## UNRELEASED

- feat: add readfrom json tag to support reverse edges
[#49](https://github.com/hypermodeinc/modusgraph/pull/49)
- feat: add a Shutdown function to close the active engine

- chore: Refactoring package management [#51](https://github.com/hypermodeinc/modusgraph/pull/51)

- fix: alter schema on reverse edge after querying schema
[#55](https://github.com/hypermodeinc/modusgraph/pull/55)

- feat: update interface to engine and namespace
[#57](https://github.com/hypermodeinc/modusgraph/pull/57)

- chore: Update dgraph dependency [#62](https://github.com/hypermodeinc/modusgraph/pull/62)

- fix: add context to api functions [#69](https://github.com/hypermodeinc/modusgraph/pull/69)

## 2025-01-02 - Version 0.1.0
## 2025-05-21 - Version 0.1.0

Baseline for the changelog.

Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,18 @@ Version 2.0. See the [LICENSE](./LICENSE) file for a complete copy of the licens
questions about modus licensing, or need an alternate license or other arrangement, please contact
us at <hello@hypermode.com>.

## Windows Users

modusGraph (and its dependencies) are designed to work on POSIX-compliant operating systems, and are
not guaranteed to work on Windows.

Tests at the top level folder (`go test .`) on Windows are maintained to pass, but other tests in
subfolders may not work as expected.

Temporary folders created during tests may not be cleaned up properly on Windows. Users should
periodically clean up these folders. The temporary folders are created in the Windows temp
directory, `C:\Users\<username>\AppData\Local\Temp\modusgraph_test*`.

## Acknowledgements

modusGraph builds heavily upon packages from the open source projects of
Expand Down
6 changes: 3 additions & 3 deletions admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func TestDropData(t *testing.T) {
}{
{
name: "DropDataWithFileURI",
uri: "file://" + t.TempDir(),
uri: "file://" + GetTempDir(t),
},
{
name: "DropDataWithDgraphURI",
Expand Down Expand Up @@ -81,7 +81,7 @@ func TestDropAll(t *testing.T) {
}{
{
name: "DropAllWithFileURI",
uri: "file://" + t.TempDir(),
uri: "file://" + GetTempDir(t),
},
{
name: "DropAllWithDgraphURI",
Expand Down Expand Up @@ -159,7 +159,7 @@ func TestCreateSchema(t *testing.T) {
}{
{
name: "CreateSchemaWithFileURI",
uri: "file://" + t.TempDir(),
uri: "file://" + GetTempDir(t),
},
{
name: "CreateSchemaWithDgraphURI",
Expand Down
10 changes: 5 additions & 5 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func TestClientPool(t *testing.T) {
}{
{
name: "ClientPoolWithFileURI",
uri: "file://" + t.TempDir(),
uri: "file://" + GetTempDir(t),
},
{
name: "ClientPoolWithDgraphURI",
Expand Down Expand Up @@ -110,8 +110,8 @@ func TestClientPool(t *testing.T) {
})
}

// Reset singleton at the end of the test to ensure the next test can start fresh
mg.ResetSingleton()
// Shutdown at the end of the test to ensure the next test can start fresh
mg.Shutdown()
}

func TestClientPoolStress(t *testing.T) {
Expand All @@ -122,7 +122,7 @@ func TestClientPoolStress(t *testing.T) {
}{
{
name: "ClientPoolStressWithFileURI",
uri: "file://" + t.TempDir(),
uri: "file://" + GetTempDir(t),
},
{
name: "ClientPoolStressWithDgraphURI",
Expand Down Expand Up @@ -205,6 +205,6 @@ func TestClientPoolStress(t *testing.T) {
require.Greater(t, successCount, 0)
})

mg.ResetSingleton()
mg.Shutdown()
}
}
2 changes: 1 addition & 1 deletion delete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func TestClientDelete(t *testing.T) {
}{
{
name: "DeleteWithFileURI",
uri: "file://" + t.TempDir(),
uri: "file://" + GetTempDir(t),
},
{
name: "DeleteWithDgraphURI",
Expand Down
32 changes: 23 additions & 9 deletions engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import (
"errors"
"fmt"
"path"
"runtime"
"strconv"
"sync"
"sync/atomic"
"time"

"github.com/dgraph-io/badger/v4"
"github.com/dgraph-io/dgo/v240"
Expand All @@ -34,19 +36,15 @@ import (
var (
// This ensures that we only have one instance of modusDB in this process.
singleton atomic.Bool
// activeEngine tracks the current Engine instance for global access
activeEngine *Engine

ErrSingletonOnly = errors.New("only one instance of modusDB can exist in a process")
ErrEmptyDataDir = errors.New("data directory is required")
ErrClosedEngine = errors.New("modusDB engine is closed")
ErrNonExistentDB = errors.New("namespace does not exist")
)

// ResetSingleton resets the singleton state for testing purposes.
// This should ONLY be called during testing, typically in cleanup functions.
func ResetSingleton() {
singleton.Store(false)
}

// Engine is an instance of modusDB.
// For now, we only support one instance of modusDB per process.
type Engine struct {
Expand Down Expand Up @@ -105,7 +103,8 @@ func NewEngine(conf Config) (*Engine, error) {
engine.logger.Error(err, "Failed to reset database")
return nil, fmt.Errorf("error resetting db: %w", err)
}

// Store the engine as the active instance
activeEngine = engine
x.UpdateHealthStatus(true)

engine.db0 = &Namespace{id: 0, engine: engine}
Expand All @@ -114,6 +113,16 @@ func NewEngine(conf Config) (*Engine, error) {
return engine, nil
}

// Shutdown closes the active Engine instance and resets the singleton state.
func Shutdown() {
if activeEngine != nil {
activeEngine.Close()
activeEngine = nil
}
// Reset the singleton state so a new engine can be created if needed
singleton.Store(false)
}

func (engine *Engine) GetClient() (*dgo.Dgraph, error) {
engine.logger.V(2).Info("Getting Dgraph client from engine")
client, err := createDgraphClient(context.Background(), engine.listener)
Expand Down Expand Up @@ -378,7 +387,7 @@ func (engine *Engine) LoadData(inCtx context.Context, dataDir string) error {
return engine.db0.LoadData(inCtx, dataDir)
}

// Close closes the modusDB instance.
// Close closes the modusGraph instance.
func (engine *Engine) Close() {
engine.mutex.Lock()
defer engine.mutex.Unlock()
Expand All @@ -388,13 +397,18 @@ func (engine *Engine) Close() {
}

if !singleton.CompareAndSwap(true, false) {
panic("modusDB instance was not properly opened")
panic("modusGraph instance was not properly opened")
}

engine.isOpen.Store(false)
x.UpdateHealthStatus(false)
posting.Cleanup()
worker.State.Dispose()

if runtime.GOOS == "windows" {
runtime.GC()
time.Sleep(200 * time.Millisecond)
}
}

func (ns *Engine) reset() error {
Expand Down
6 changes: 3 additions & 3 deletions insert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func TestClientInsert(t *testing.T) {
}{
{
name: "InsertWithFileURI",
uri: "file://" + t.TempDir(),
uri: "file://" + GetTempDir(t),
},
{
name: "InsertWithDgraphURI",
Expand Down Expand Up @@ -95,7 +95,7 @@ func TestClientInsertMultipleEntities(t *testing.T) {
}{
{
name: "InsertMultipleWithFileURI",
uri: "file://" + t.TempDir(),
uri: "file://" + GetTempDir(t),
},
{
name: "InsertMultipleWithDgraphURI",
Expand Down Expand Up @@ -157,7 +157,7 @@ func TestDepthQuery(t *testing.T) {
}{
{
name: "InsertWithFileURI",
uri: "file://" + t.TempDir(),
uri: "file://" + GetTempDir(t),
},
{
name: "InsertWithDgraphURI",
Expand Down
6 changes: 3 additions & 3 deletions query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func TestClientSimpleGet(t *testing.T) {
}{
{
name: "GetWithFileURI",
uri: "file://" + t.TempDir(),
uri: "file://" + GetTempDir(t),
},
{
name: "GetWithDgraphURI",
Expand Down Expand Up @@ -86,7 +86,7 @@ func TestClientQuery(t *testing.T) {
}{
{
name: "QueryWithFileURI",
uri: "file://" + t.TempDir(),
uri: "file://" + GetTempDir(t),
},
{
name: "QueryWithDgraphURI",
Expand Down Expand Up @@ -268,7 +268,7 @@ func TestVectorSimilaritySearch(t *testing.T) {
}{
{
name: "VectorSimilaritySearchWithFileURI",
uri: "file://" + t.TempDir(),
uri: "file://" + GetTempDir(t),
},
/*
{
Expand Down
17 changes: 0 additions & 17 deletions unit_test/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1102,20 +1102,3 @@ func TestMultiPolygon(t *testing.T) {
require.Equal(t, "Jane Doe", geomStruct.Name)
require.Equal(t, multiPolygon.Coordinates, geomStruct.MultiArea.Coordinates)
}

func TestUserStore(t *testing.T) {
ctx := context.Background()
engine, err := modusgraph.NewEngine(modusgraph.NewDefaultConfig("./foo"))
require.NoError(t, err)
defer engine.Close()

user := User{
Name: "John Doe",
Age: 30,
}
gid, user, err := modusgraph.Create(ctx, engine, user)
require.NoError(t, err)
require.NotZero(t, gid)
require.Equal(t, "John Doe", user.Name)
require.Equal(t, 30, user.Age)
}
2 changes: 1 addition & 1 deletion update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func TestClientUpdate(t *testing.T) {
}{
{
name: "UpdateWithFileURI",
uri: "file://" + t.TempDir(),
uri: "file://" + GetTempDir(t),
},
{
name: "UpdateWithDgraphURI",
Expand Down
42 changes: 40 additions & 2 deletions util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ import (
"context"
"log"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"testing"
"time"

"github.com/go-logr/stdr"
mg "github.com/hypermodeinc/modusgraph"
Expand Down Expand Up @@ -45,13 +49,47 @@ func CreateTestClient(t *testing.T, uri string) (mg.Client, func()) {
}
client.Close()

// Reset the singleton state so the next test can create a new engine
mg.ResetSingleton()
// Properly shutdown the engine and reset the singleton state
mg.Shutdown()
}

return client, cleanup
}

// GetTempDir returns a temporary directory for testing purposes.
// It creates a unique directory for each test and registers a cleanup function to remove it.
// On Windows, it uses the standard temp directory and creates a unique directory for each test.
// On other platforms, it uses the standard toolchain TempDir function.
func GetTempDir(t *testing.T) string {
if runtime.GOOS == "windows" {
baseDir := os.TempDir()
testName := t.Name()
testName = strings.ReplaceAll(testName, "/", "_")
testName = strings.ReplaceAll(testName, "\\", "_")
testName = strings.ReplaceAll(testName, ":", "_")

tempDir := filepath.Join(baseDir, "modusgraph_test_"+testName)

err := os.MkdirAll(tempDir, 0755)
if err != nil {
t.Logf("Failed to create temp directory %s: %v, falling back to standard temp dir", tempDir, err)
return os.TempDir()
}

t.Cleanup(func() {
runtime.GC()
time.Sleep(200 * time.Millisecond)

if err := os.RemoveAll(tempDir); err != nil {
t.Logf("Warning: failed to remove temp directory %s: %v", tempDir, err)
}
})

return tempDir
}
return t.TempDir()
}

// SetupTestEnv configures the environment variables for tests.
// This is particularly useful when debugging tests in an IDE.
func SetupTestEnv(logLevel int) {
Expand Down
Loading