Kyrux
Back

NoSQL

🧪 Experimental — part of the core layer (kyrux/core), labeled "experimental" by the framework's own USE.md. Coexists with the traditional bootstrap/fw.DB (SQL) without conflict — it's an addition, not a replacement — but the API may change between versions without notice.

Five NoSQL databases have their own client under core/nosql/<database>: MongoDB, Redis (general-purpose — see note below), Cassandra, Elasticsearch, and DynamoDB.

Why there's no single orm.Query[T] for all of them

The SQL ORM (core/orm) has one fluent API (Where/OrderBy/Limit) because relational databases share the same data model. NoSQL doesn't — a Mongo filter, a Cassandra CQL query, and an Elasticsearch Query DSL have nothing genuinely in common. Forcing all of them behind the same interface would fake a portability that doesn't exist. Each client below has its own API, in that database's native shape — Go generics show up only to give a typed handle (Coll[T], Index[T], Table[T]), not to pretend there's a universal query language.

None of them are read from .env

Unlike SQL (DB_NAME/DB_DRIVER/DB_DSN, read automatically by bootstrap), no NoSQL client reads environment variables on its own — you read it yourself (environment.Get("MONGO_URI")) and pass it to the constructor. On purpose: most projects use no NoSQL at all, and the framework doesn't force a configuration scheme onto databases most people will never touch.

# MongoDB (core/nosql/mongo) — NOT read by the framework.
# MONGO_URI=mongodb://localhost:27017
# MONGO_DATABASE=mydb

No migrations, no CLI — schema is your own responsibility (schemaless in Mongo/Elasticsearch/DynamoDB, manual CQL in Cassandra).

MongoDB — core/nosql/mongo

Model = plain Go struct with bson:"..." tags (the official driver's own convention, not a kyrux:"..." tag).

import "kyrux/core/nosql/mongo"

client, err := mongo.New(uri, "mydb") // Connect + Ping (10s)
defer client.Close(ctx)

type Post struct {
    ID    string `bson:"_id,omitempty"`
    Title string `bson:"title"`
}

posts := mongo.CollectionOf[Post](client, "posts")

err = posts.InsertOne(ctx, &Post{Title: "Hello"})
list, err := posts.Find(ctx, mongo.M{"title": "Hello"})
one, err := posts.FindOne(ctx, mongo.M{"_id": id}) // mongo.ErrNoDocuments if not found
n, err := posts.UpdateOne(ctx, mongo.M{"_id": id}, mongo.M{"$set": mongo.M{"title": "New"}})
n, err = posts.DeleteOne(ctx, mongo.M{"_id": id})
total, err := posts.Count(ctx, mongo.M{})

mongo.M is an alias for bson.M (map[string]any) — filters and updates in MongoDB's native format ($set, $inc, etc.). client.Raw() returns the driver's original *mongo.Client for anything outside the typed API.

Redis — core/nosql/redis

A general-purpose Redis client — different from core/cache (k/v+TTL only) and core/queue (queue only), this one exposes Redis's actual data structures: strings, hashes, lists, sets, sorted sets, and pub/sub.

import "kyrux/core/nosql/redis"

client, err := redis.New(addr, password, 0) // PINGs before returning

client.Set(ctx, "key", "value", time.Hour)
client.SetJSON(ctx, "obj", myStruct, time.Hour)
client.HSet(ctx, "hash", "field", "value")
client.LPush(ctx, "queue", "item")
client.SAdd(ctx, "set", "member")
client.ZAdd(ctx, "leaderboard", redis.ZMember{Score: 10, Member: "player1"})

ps := client.Subscribe(ctx, "channel")
for msg := range ps.Channel() {
    fmt.Println(msg.Payload)
}

redis.ErrNil is go-redis's Nil re-exported (key not found). client.Raw() returns the original *redis.Client.

Cassandra — core/nosql/cassandra

import "kyrux/core/nosql/cassandra"

client, err := cassandra.New([]string{"127.0.0.1"}, "my_keyspace")

err = client.Exec(ctx, "INSERT INTO posts (id, title) VALUES (?, ?)", id, "Hello")
rows, err := client.SelectMap(ctx, "SELECT * FROM posts WHERE id = ?", id)

type Post struct{ ID string; Title string }
posts, err := cassandra.Select[Post](ctx, client, "SELECT * FROM posts")

⚠️ CQL only filters efficiently by partition/clustering key — arbitrary WHERE requires ALLOW FILTERING (avoid it; this client doesn't use it). No JOINs, no migrations — schema is applied manually in CQL.

Elasticsearch — core/nosql/elasticsearch

Document = struct with json:"..." tags (reuses encoding/json).

import "kyrux/core/nosql/elasticsearch"

client, err := elasticsearch.New([]string{"http://localhost:9200"})

type Product struct {
    Name string `json:"name"`
}

idx := elasticsearch.IndexOf[Product](client, "products")

id, err := idx.Put(ctx, "", &Product{Name: "Mug"}) // "" = auto-generated ID
product, found, err := idx.Get(ctx, id)
err = idx.Delete(ctx, id)

results, err := idx.Search(ctx, map[string]any{
    "query": map[string]any{"match": map[string]any{"name": "mug"}},
})
total, err := idx.Count(ctx, map[string]any{"match_all": map[string]any{}})

Search/Count take Elasticsearch's native Query DSL, untranslated. idx.Refresh(ctx) forces an index refresh — use sparingly in production (Elasticsearch's own performance cost).

DynamoDB — core/nosql/dynamodb

Model = struct with dynamodbav:"..." tags (aws-sdk-go-v2).

import "kyrux/core/nosql/dynamodb"

// endpoint == "" → real AWS, SDK's default credential chain
// endpoint != "" → e.g. DynamoDB Local, requires accessKey/secretKey
client, err := dynamodb.New(ctx, "us-east-1", "", "", "")

type Order struct {
    ID     string `dynamodbav:"id"`
    Status string `dynamodbav:"status"`
}

orders := dynamodb.TableOf[Order](client, "orders")

err = orders.Put(ctx, &Order{ID: "1", Status: "new"})
order, found, err := orders.Get(ctx, map[string]any{"id": "1"})
list, err := orders.Query(ctx, "id = :id", map[string]any{":id": "1"})
err = orders.Delete(ctx, map[string]any{"id": "1"})

⚠️ Scan reads the entire table — never in the hot path in production. Prefer Query with a key condition.

SQL vs NoSQL — different purposes

SQL (core/orm) NoSQL (core/nosql/*)
Query API One, fluent (Where/OrderBy/All), for any driver One per database, in its native shape — no shared abstraction
Model tag kyrux:"..." Native to each driver (bson, json, dynamodbav)
Migrations makemigrations/migrate (CLI) None — schemaless or manual CQL
Configuration DB_* in .env, read by bootstrap None — you read it and pass it to New(...)
Multiple connections fw.DB.Use("name") One client instance per New(...), stored wherever you like