NoSQL
🧪 Experimental — part of the
corelayer (kyrux/core), labeled "experimental" by the framework's ownUSE.md. Coexists with the traditionalbootstrap/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
WHERErequiresALLOW 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"})
⚠️
Scanreads the entire table — never in the hot path in production. PreferQuerywith 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 |