diff --git a/core/go.mod b/core/go.mod
index 796ebea6..05e2619a 100644
--- a/core/go.mod
+++ b/core/go.mod
@@ -4,6 +4,7 @@ go 1.22.1
require (
github.com/99designs/gqlgen v0.17.48
+ github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874
github.com/elastic/go-elasticsearch/v8 v8.14.0
github.com/go-chi/chi/v5 v5.0.12
github.com/go-chi/cors v1.2.1
diff --git a/core/go.sum b/core/go.sum
index 2541fc58..8d7437d2 100644
--- a/core/go.sum
+++ b/core/go.sum
@@ -10,6 +10,8 @@ github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsVi
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
+github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous=
+github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
diff --git a/core/graph/model/models_gen.go b/core/graph/model/models_gen.go
index 8d9422b1..f80cb431 100644
--- a/core/graph/model/models_gen.go
+++ b/core/graph/model/models_gen.go
@@ -99,6 +99,7 @@ const (
DatabaseTypeRedis DatabaseType = "Redis"
DatabaseTypeElasticSearch DatabaseType = "ElasticSearch"
DatabaseTypeMariaDb DatabaseType = "MariaDB"
+ DatabaseTypeMemcached DatabaseType = "Memcached"
)
var AllDatabaseType = []DatabaseType{
@@ -109,11 +110,12 @@ var AllDatabaseType = []DatabaseType{
DatabaseTypeRedis,
DatabaseTypeElasticSearch,
DatabaseTypeMariaDb,
+ DatabaseTypeMemcached,
}
func (e DatabaseType) IsValid() bool {
switch e {
- case DatabaseTypePostgres, DatabaseTypeMySQL, DatabaseTypeSqlite3, DatabaseTypeMongoDb, DatabaseTypeRedis, DatabaseTypeElasticSearch, DatabaseTypeMariaDb:
+ case DatabaseTypePostgres, DatabaseTypeMySQL, DatabaseTypeSqlite3, DatabaseTypeMongoDb, DatabaseTypeRedis, DatabaseTypeElasticSearch, DatabaseTypeMariaDb, DatabaseTypeMemcached:
return true
}
return false
diff --git a/core/graph/schema.graphqls b/core/graph/schema.graphqls
index c84ee163..514d680d 100644
--- a/core/graph/schema.graphqls
+++ b/core/graph/schema.graphqls
@@ -10,6 +10,7 @@ enum DatabaseType {
Redis,
ElasticSearch,
MariaDB,
+ Memcached,
}
type Column {
diff --git a/core/src/engine/engine.go b/core/src/engine/engine.go
index b2a67150..c77e5d78 100644
--- a/core/src/engine/engine.go
+++ b/core/src/engine/engine.go
@@ -12,6 +12,7 @@ const (
DatabaseType_MongoDB = "MongoDB"
DatabaseType_Redis = "Redis"
DatabaseType_ElasticSearch = "ElasticSearch"
+ DatabaseType_Memcached = "Memcached"
)
type Engine struct {
diff --git a/core/src/plugins/memcached/add.go b/core/src/plugins/memcached/add.go
new file mode 100644
index 00000000..34fbcea6
--- /dev/null
+++ b/core/src/plugins/memcached/add.go
@@ -0,0 +1,15 @@
+package memcached
+
+import (
+ "errors"
+
+ "github.com/clidey/whodb/core/src/engine"
+)
+
+func (p *MemcachedPlugin) AddStorageUnit(config *engine.PluginConfig, schema string, storageUnit string, fields map[string]string) (bool, error) {
+ return false, errors.ErrUnsupported
+}
+
+func (p *MemcachedPlugin) AddRow(config *engine.PluginConfig, schema string, storageUnit string, values []engine.Record) (bool, error) {
+ return false, errors.ErrUnsupported
+}
diff --git a/core/src/plugins/memcached/db.go b/core/src/plugins/memcached/db.go
new file mode 100644
index 00000000..56e7fe71
--- /dev/null
+++ b/core/src/plugins/memcached/db.go
@@ -0,0 +1,22 @@
+package memcached
+
+import (
+ "fmt"
+
+ "github.com/bradfitz/gomemcache/memcache"
+ "github.com/clidey/whodb/core/src/common"
+ "github.com/clidey/whodb/core/src/engine"
+)
+
+func DB(config *engine.PluginConfig) (*memcache.Client, error) {
+ client := memcache.New(getAddress(config))
+ if client == nil {
+ return nil, fmt.Errorf("failed to create Memcached client")
+ }
+ return client, nil
+}
+
+func getAddress(config *engine.PluginConfig) string {
+ port := common.GetRecordValueOrDefault(config.Credentials.Advanced, "Port", "11211")
+ return fmt.Sprintf("%s:%s", config.Credentials.Hostname, port)
+}
diff --git a/core/src/plugins/memcached/delete.go b/core/src/plugins/memcached/delete.go
new file mode 100644
index 00000000..38ea3346
--- /dev/null
+++ b/core/src/plugins/memcached/delete.go
@@ -0,0 +1,21 @@
+package memcached
+
+import (
+ "github.com/bradfitz/gomemcache/memcache"
+ "github.com/clidey/whodb/core/src/engine"
+)
+
+func (p *MemcachedPlugin) DeleteRow(config *engine.PluginConfig, schema string, storageUnit string, values map[string]string) (bool, error) {
+ client, err := DB(config)
+ if err != nil {
+ return false, err
+ }
+ defer client.Close()
+
+ err = client.Delete(storageUnit)
+ if err != nil && err != memcache.ErrCacheMiss {
+ return false, err
+ }
+
+ return true, nil
+}
diff --git a/core/src/plugins/memcached/memcached.go b/core/src/plugins/memcached/memcached.go
new file mode 100644
index 00000000..aa45cf4a
--- /dev/null
+++ b/core/src/plugins/memcached/memcached.go
@@ -0,0 +1,151 @@
+package memcached
+
+import (
+ "bufio"
+ "errors"
+ "fmt"
+ "net"
+ "strings"
+
+ "github.com/bradfitz/gomemcache/memcache"
+ "github.com/clidey/whodb/core/src/engine"
+)
+
+type MemcachedPlugin struct{}
+
+func (p *MemcachedPlugin) IsAvailable(config *engine.PluginConfig) bool {
+ client, err := DB(config)
+ if err != nil {
+ return false
+ }
+ _, err = client.Get("test_key")
+ if err != nil && err != memcache.ErrCacheMiss {
+ return false
+ }
+ return true
+}
+
+func (p *MemcachedPlugin) GetDatabases(config *engine.PluginConfig) ([]string, error) {
+ return nil, errors.New("unsupported operation for Memcached")
+}
+
+func (p *MemcachedPlugin) GetSchema(config *engine.PluginConfig) ([]string, error) {
+ return nil, errors.New("unsupported operation for Memcached")
+}
+
+func (p *MemcachedPlugin) GetStorageUnits(config *engine.PluginConfig, schema string) ([]engine.StorageUnit, error) {
+ client, err := DB(config)
+ if err != nil {
+ return nil, err
+ }
+ defer client.Close()
+
+ rows, err := p.GetRows(config, schema, "", "", 0, 0)
+ if err != nil {
+ return nil, err
+ }
+
+ count := len(rows.Rows)
+
+ return []engine.StorageUnit{
+ {
+ Name: "default",
+ Attributes: []engine.Record{
+ {
+ Key: "Count",
+ Value: fmt.Sprintf("%v", count),
+ },
+ },
+ },
+ }, nil
+}
+
+func (p *MemcachedPlugin) GetRows(config *engine.PluginConfig, schema string, storageUnit string, where string, pageSize int, pageOffset int) (*engine.GetRowsResult, error) {
+ client, err := DB(config)
+ if err != nil {
+ return nil, err
+ }
+ defer client.Close()
+
+ conn, err := net.Dial("tcp", getAddress(config))
+ if err != nil {
+ return nil, fmt.Errorf("failed to connect to Memcached server: %v", err)
+ }
+ defer conn.Close()
+
+ scanner := bufio.NewScanner(conn)
+ slabs := make(map[int]int)
+
+ for scanner.Scan() {
+ line := scanner.Text()
+ if line == "END" {
+ break
+ }
+
+ if strings.HasPrefix(line, "STAT items") {
+ parts := strings.Split(line, ":")
+ if len(parts) > 2 {
+ slabID := 0
+ number := 0
+ fmt.Sscanf(parts[1], "%d", &slabID)
+ fmt.Sscanf(parts[2], "number %d", &number)
+ slabs[slabID] = number
+ }
+ }
+ }
+
+ rows := [][]string{}
+
+ for range slabs {
+ for scanner.Scan() {
+ line := scanner.Text()
+ if line == "END" {
+ break
+ }
+
+ parts := strings.Split(line, " ")
+ if len(parts) > 1 {
+ key := parts[1]
+
+ item, err := client.Get(key)
+ if err != nil {
+ return nil, fmt.Errorf("failed to retrieve value for key %s: %v", key, err)
+ }
+ rows = append(rows, []string{string(item.Value)})
+ }
+ }
+ }
+
+ if scanner.Err() != nil {
+ return nil, fmt.Errorf("error reading from connection: %v", scanner.Err())
+ }
+
+ return &engine.GetRowsResult{
+ Columns: []engine.Column{
+ {
+ Name: "Value",
+ Type: "string",
+ },
+ },
+ Rows: rows,
+ }, nil
+}
+
+func (p *MemcachedPlugin) GetGraph(config *engine.PluginConfig, schema string) ([]engine.GraphUnit, error) {
+ return nil, errors.New("unsupported operation for Memcached")
+}
+
+func (p *MemcachedPlugin) RawExecute(config *engine.PluginConfig, query string) (*engine.GetRowsResult, error) {
+ return nil, errors.New("unsupported operation for Memcached")
+}
+
+func (p *MemcachedPlugin) Chat(config *engine.PluginConfig, schema string, model string, previousConversation string, query string) ([]*engine.ChatMessage, error) {
+ return nil, errors.New("unsupported operation for Memcached")
+}
+
+func NewMemcachedPlugin() *engine.Plugin {
+ return &engine.Plugin{
+ Type: engine.DatabaseType_Memcached,
+ PluginFunctions: &MemcachedPlugin{},
+ }
+}
diff --git a/core/src/plugins/memcached/update.go b/core/src/plugins/memcached/update.go
new file mode 100644
index 00000000..fa3ecbe6
--- /dev/null
+++ b/core/src/plugins/memcached/update.go
@@ -0,0 +1,32 @@
+package memcached
+
+import (
+ "errors"
+
+ "github.com/bradfitz/gomemcache/memcache"
+ "github.com/clidey/whodb/core/src/engine"
+)
+
+func (p *MemcachedPlugin) UpdateStorageUnit(config *engine.PluginConfig, schema string, storageUnit string, values map[string]string) (bool, error) {
+ client, err := DB(config)
+ if err != nil {
+ return false, err
+ }
+ defer client.Close()
+
+ if len(values) != 1 {
+ return false, errors.New("invalid number of fields for Memcached key")
+ }
+
+ value, ok := values["value"]
+ if !ok {
+ return false, errors.New("missing 'value' for update")
+ }
+
+ err = client.Set(&memcache.Item{Key: storageUnit, Value: []byte(value)})
+ if err != nil {
+ return false, err
+ }
+
+ return true, nil
+}
diff --git a/core/src/src.go b/core/src/src.go
index 643bc31c..f04d9464 100644
--- a/core/src/src.go
+++ b/core/src/src.go
@@ -6,6 +6,7 @@ import (
"github.com/clidey/whodb/core/src/engine"
"github.com/clidey/whodb/core/src/env"
"github.com/clidey/whodb/core/src/plugins/elasticsearch"
+ "github.com/clidey/whodb/core/src/plugins/memcached"
"github.com/clidey/whodb/core/src/plugins/mongodb"
"github.com/clidey/whodb/core/src/plugins/mysql"
"github.com/clidey/whodb/core/src/plugins/postgres"
@@ -24,6 +25,7 @@ func InitializeEngine() *engine.Engine {
MainEngine.RegistryPlugin(mongodb.NewMongoDBPlugin())
MainEngine.RegistryPlugin(redis.NewRedisPlugin())
MainEngine.RegistryPlugin(elasticsearch.NewElasticSearchPlugin())
+ MainEngine.RegistryPlugin(memcached.NewMemcachedPlugin())
return MainEngine
}
diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml
index fe10e7ca..0715a18c 100644
--- a/dev/docker-compose.yml
+++ b/dev/docker-compose.yml
@@ -61,6 +61,17 @@ services:
- redis:/bitnami
networks:
- db
+ memcached:
+ image: bitnami/memcached
+ ports:
+ - '11211:11211'
+ environment:
+ MEMCACHED_CACHE_SIZE: 64
+ MEMCACHED_MAX_CONNECTIONS: 1024
+ volumes:
+ - memcached:/bitnami
+ networks:
+ - db
elasticsearch:
container_name: elasticsearch
image: docker.elastic.co/elasticsearch/elasticsearch:8.14.1
@@ -99,4 +110,5 @@ volumes:
mariadb:
mongo:
redis:
- elasticsearch:
\ No newline at end of file
+ elasticsearch:
+ memcached:
\ No newline at end of file
diff --git a/frontend/src/components/icons.tsx b/frontend/src/components/icons.tsx
index 1dea78da..7f7950ae 100644
--- a/frontend/src/components/icons.tsx
+++ b/frontend/src/components/icons.tsx
@@ -126,5 +126,6 @@ export const Icons = {
MongoDB: ,
Redis: ,
ElasticSearch: ,
+ Memcached: ,
},
}
\ No newline at end of file
diff --git a/frontend/src/generated/graphql.tsx b/frontend/src/generated/graphql.tsx
index 8b295e8e..fd81e200 100644
--- a/frontend/src/generated/graphql.tsx
+++ b/frontend/src/generated/graphql.tsx
@@ -39,6 +39,7 @@ export type Column = {
export enum DatabaseType {
ElasticSearch = 'ElasticSearch',
MariaDb = 'MariaDB',
+ Memcached = 'Memcached',
MongoDb = 'MongoDB',
MySql = 'MySQL',
Postgres = 'Postgres',
diff --git a/frontend/src/pages/auth/login.tsx b/frontend/src/pages/auth/login.tsx
index 9bd0af70..570992c8 100644
--- a/frontend/src/pages/auth/login.tsx
+++ b/frontend/src/pages/auth/login.tsx
@@ -59,6 +59,12 @@ const databaseTypeDropdownItems: IDropdownItem>[] = [
icon: Icons.Logos.ElasticSearch,
extra: {"Port": "9200", "SSL Mode": "disable"},
},
+ {
+ id: "Memcached",
+ label: "Memcached",
+ icon: Icons.Logos.Memcached,
+ extra: {"Port": "11211"},
+ },
]
export const LoginPage: FC = () => {
@@ -89,7 +95,7 @@ export const LoginPage: FC = () => {
if (([DatabaseType.MySql, DatabaseType.Postgres].includes(databaseType.id as DatabaseType) && (hostName.length === 0 || database.length === 0 || username.length === 0))
|| (databaseType.id === DatabaseType.Sqlite3 && database.length === 0)
|| (databaseType.id === DatabaseType.MongoDb && (hostName.length === 0 || username.length === 0))
- || (databaseType.id === DatabaseType.Redis && (hostName.length === 0))) {
+ || ([DatabaseType.Redis, DatabaseType.Memcached].includes(databaseType.id as DatabaseType) && (hostName.length === 0))) {
return setError("All fields are required");
}
setError(undefined);
@@ -241,9 +247,9 @@ export const LoginPage: FC = () => {
}
return <>
- { databaseType.id !== DatabaseType.Redis && }
+ { databaseType.id !== DatabaseType.Redis && databaseType.id !== DatabaseType.Memcached && }
- { (databaseType.id !== DatabaseType.MongoDb && databaseType.id !== DatabaseType.Redis && databaseType.id !== DatabaseType.ElasticSearch) && }
+ { (databaseType.id !== DatabaseType.MongoDb && databaseType.id !== DatabaseType.Redis && databaseType.id !== DatabaseType.Memcached && databaseType.id !== DatabaseType.ElasticSearch) && }
>
}, [database, databaseType.id, databasesLoading, foundDatabases?.Database, handleHostNameChange, hostName, password, username]);
diff --git a/frontend/src/pages/storage-unit/explore-storage-unit.tsx b/frontend/src/pages/storage-unit/explore-storage-unit.tsx
index ba0ccd0d..ad4cddef 100644
--- a/frontend/src/pages/storage-unit/explore-storage-unit.tsx
+++ b/frontend/src/pages/storage-unit/explore-storage-unit.tsx
@@ -72,7 +72,12 @@ export const ExploreStorageUnit: FC = () => {
where: whereCondition,
pageSize: Number.parseInt(bufferPageSize),
pageOffset: currentPage,
- }
+ },
+ onCompleted(data) {
+ setRows(data.Row);
+ setPageSize(bufferPageSize);
+ },
+ fetchPolicy: "no-cache",
});
}, [getStorageUnitRows, current?.Type, schema, unitName, whereCondition, bufferPageSize, currentPage]);
@@ -301,7 +306,7 @@ export const ExploreStorageUnit: FC = () => {
if (columns.length === 0) {
columns = rows?.Columns ?? [];
}
- setNewRowForm((columns.map(col => {
+ setNewRowForm((rows?.Columns.map(col => {
const colName = col.Name.toLowerCase();
const isId = colName === "id" && col.Type === "UUID";
const isDate = col.Type === "TIMESTAMPTZ";
@@ -321,7 +326,7 @@ export const ExploreStorageUnit: FC = () => {
},
],
}
- })));
+ }) ?? []));
}
}
setShowAdd(showAddStatus);
diff --git a/frontend/src/pages/storage-unit/storage-unit.tsx b/frontend/src/pages/storage-unit/storage-unit.tsx
index 4f8e7d62..70246cec 100644
--- a/frontend/src/pages/storage-unit/storage-unit.tsx
+++ b/frontend/src/pages/storage-unit/storage-unit.tsx
@@ -211,7 +211,6 @@ export const StorageUnitPage: FC = () => {
return items.map(item => createDropdownItem(item));
}, [current?.Type]);
-
if (loading) {
return
diff --git a/frontend/src/utils/functions.ts b/frontend/src/utils/functions.ts
index 12044037..92c35802 100644
--- a/frontend/src/utils/functions.ts
+++ b/frontend/src/utils/functions.ts
@@ -40,6 +40,7 @@ export function isNoSQL(databaseType: string) {
switch (databaseType) {
case DatabaseType.MongoDb:
case DatabaseType.Redis:
+ case DatabaseType.Memcached:
case DatabaseType.ElasticSearch:
return true;
}