[{"data":1,"prerenderedAt":7924},["ShallowReactive",2],{"search-sections-grub":3,"nav-grub":2302,"content-tree-grub":2359,"footer-resources":2385,"content-/v1.0.18/guides/lifecycle":4086,"surround-/v1.0.18/guides/lifecycle":7921},[4,10,14,20,25,30,36,41,46,51,55,60,65,70,75,80,84,89,94,98,103,108,113,118,123,128,133,137,142,147,152,157,162,167,172,177,182,187,192,197,202,207,212,217,222,227,232,237,242,247,252,257,262,267,272,277,282,287,291,295,300,304,309,314,318,323,328,333,338,343,348,352,356,361,366,371,375,380,385,389,394,399,404,408,412,417,421,426,431,435,440,445,450,454,459,464,469,474,478,483,488,493,498,502,507,512,517,522,526,531,536,541,546,551,556,561,565,569,574,579,584,589,594,599,604,608,612,617,621,625,629,633,637,641,645,649,654,659,664,669,673,677,682,687,692,696,701,705,710,715,720,725,729,734,739,744,748,752,757,762,766,771,775,779,784,788,793,798,803,808,812,817,822,827,831,836,841,846,851,856,860,865,869,874,879,883,888,893,898,903,908,913,917,921,925,930,935,940,945,950,954,958,963,968,973,978,982,987,992,997,1000,1005,1010,1015,1019,1024,1029,1034,1039,1043,1048,1053,1058,1062,1067,1072,1077,1081,1086,1091,1096,1101,1105,1110,1115,1120,1125,1129,1134,1138,1143,1148,1152,1157,1162,1167,1172,1176,1181,1186,1190,1195,1200,1204,1209,1213,1218,1223,1227,1232,1237,1242,1246,1250,1255,1260,1265,1270,1275,1280,1285,1289,1294,1299,1304,1309,1313,1318,1323,1327,1332,1337,1341,1346,1351,1356,1361,1366,1371,1376,1381,1385,1390,1395,1400,1403,1408,1413,1418,1423,1428,1432,1437,1442,1447,1452,1457,1462,1467,1472,1477,1482,1486,1491,1496,1500,1505,1510,1514,1519,1523,1527,1531,1535,1539,1543,1548,1553,1558,1563,1566,1570,1574,1578,1582,1586,1590,1595,1600,1603,1607,1611,1615,1619,1624,1629,1634,1639,1644,1649,1654,1659,1663,1668,1673,1678,1683,1688,1693,1698,1703,1708,1713,1718,1723,1728,1733,1738,1742,1747,1752,1757,1760,1765,1770,1774,1778,1783,1788,1792,1797,1801,1805,1809,1813,1818,1823,1828,1833,1838,1843,1848,1852,1857,1862,1867,1872,1877,1882,1887,1892,1897,1902,1907,1912,1917,1920,1925,1930,1934,1939,1943,1947,1951,1956,1961,1966,1971,1976,1980,1984,1988,1992,1996,2001,2006,2010,2014,2018,2022,2026,2030,2035,2039,2043,2047,2051,2055,2059,2063,2068,2072,2076,2080,2084,2088,2092,2096,2100,2104,2108,2112,2116,2120,2124,2128,2132,2136,2141,2145,2149,2153,2158,2162,2166,2171,2176,2180,2184,2188,2192,2196,2201,2205,2209,2213,2217,2221,2225,2229,2233,2237,2242,2246,2250,2254,2258,2262,2266,2270,2274,2278,2282,2286,2290,2294,2298],{"id":5,"title":6,"titles":7,"content":8,"level":9},"/v1.0.18/overview","Overview",[],"Provider-agnostic storage for Go with type-safe CRUD operations",1,{"id":11,"title":6,"titles":12,"content":13,"level":9},"/v1.0.18/overview#overview",[],"Storage in Go often means choosing between vendor lock-in or writing abstraction layers from scratch. Grub offers a third path: type-safe storage operations across key-value stores, blob storage, SQL databases, and vector similarity search with a consistent API. // Same code, different backends\nusers := grub.NewStore[User](redis.New(client))\nusers := grub.NewStore[User](badger.New(db))\nusers := grub.NewStore[User](bolt.New(db, \"users\"))\n\n// Type-safe operations\nuser, _ := users.Get(ctx, \"user:123\")\nusers.Set(ctx, \"user:456\", &User{Name: \"Alice\"}, time.Hour) Swap providers without touching business logic.",{"id":15,"title":16,"titles":17,"content":18,"level":19},"/v1.0.18/overview#architecture","Architecture",[6],"┌──────────────────────────────────────────────────────────────────────────┐\n│                           Your Application                                │\n├──────────────────────────────────────────────────────────────────────────┤\n│  Store[T]          Bucket[T]         Database[T]         Index[T]        │\n│  (Key-Value)       (Blob Storage)    (SQL)               (Vector)        │\n├──────────────────────────────────────────────────────────────────────────┤\n│                         Codec (JSON/Gob)                                  │\n├──────────────────────────────────────────────────────────────────────────┤\n│  StoreProvider     BucketProvider    edamame.Factory     VectorProvider  │\n├────────┬────────┬────────┬────────┬────────┬────────┬────────┬───────────┤\n│ Redis  │ Badger │ Bolt   │ S3     │ MinIO  │ GCS    │ Azure  │  Qdrant   │\n└────────┴────────┴────────┴────────┴────────┴────────┴────────┴───────────┘ Each wrapper maintains type safety through generics while providers handle raw bytes. The codec layer handles serialization, defaulting to JSON.",2,{"id":21,"title":22,"titles":23,"content":24,"level":19},"/v1.0.18/overview#philosophy","Philosophy",[6],"Grub draws inspiration from Go's database/sql package: define interfaces, let implementations vary. One interface per storage mode: StoreProvider for key-value operationsBucketProvider for blob storageSQL databases use edamame factories directlyVectorProvider for similarity search Errors are semantic: if errors.Is(err, grub.ErrNotFound) {\n    // Handle missing record consistently across all providers\n}",{"id":26,"title":27,"titles":28,"content":29,"level":19},"/v1.0.18/overview#four-storage-modes","Four Storage Modes",[6],"",{"id":31,"title":32,"titles":33,"content":34,"level":35},"/v1.0.18/overview#key-value-store","Key-Value Store",[6,27],"For sessions, cache, configuration, and any keyed data with optional TTL. store := grub.NewStore[Session](redis.New(client))\n\nstore.Set(ctx, \"session:abc\", &session, 24*time.Hour)\nsession, err := store.Get(ctx, \"session:abc\")\nkeys, _ := store.List(ctx, \"session:\", 100) Providers: Redis, BadgerDB, BoltDB",3,{"id":37,"title":38,"titles":39,"content":40,"level":35},"/v1.0.18/overview#blob-storage","Blob Storage",[6,27],"For files, media, documents, and any object with metadata. bucket := grub.NewBucket[Document](s3.New(client, \"my-bucket\"))\n\nbucket.Put(ctx, &grub.Object[Document]{\n    Key:         \"docs/report.json\",\n    ContentType: \"application/json\",\n    Data:        Document{Title: \"Q4 Report\"},\n})\nobj, _ := bucket.Get(ctx, \"docs/report.json\") Providers: AWS S3, MinIO, Google Cloud Storage, Azure Blob Storage",{"id":42,"title":43,"titles":44,"content":45,"level":35},"/v1.0.18/overview#sql-database","SQL Database",[6,27],"For structured records with query capabilities. db := grub.NewDatabase[User](sqlxDB, \"users\", sqlite.New())\n\ndb.Set(ctx, \"1\", &User{ID: 1, Email: \"alice@example.com\"})\nuser, _ := db.Get(ctx, \"1\")\nusers, _ := db.Query().Where(\"status\", \"=\", \"active\").Exec(ctx, map[string]any{\"active\": \"enabled\"}) Drivers: PostgreSQL, MariaDB, SQLite, SQL Server",{"id":47,"title":48,"titles":49,"content":50,"level":35},"/v1.0.18/overview#vector-search","Vector Search",[6,27],"For similarity search with typed metadata and filtering. index := grub.NewIndex[Embedding](qdrant.New(client, qdrant.Config{\n    Collection: \"documents\",\n}))\n\nindex.Upsert(ctx, \"doc:1\", embedding, &Embedding{Category: \"tech\"})\nresults, _ := index.Search(ctx, queryVector, 10, nil) Providers: Milvus, Pinecone, Qdrant, Weaviate",{"id":52,"title":53,"titles":54,"content":29,"level":19},"/v1.0.18/overview#priorities","Priorities",[6],{"id":56,"title":57,"titles":58,"content":59,"level":35},"/v1.0.18/overview#type-safety","Type Safety",[6,53],"Generics eliminate runtime type assertions. The compiler catches type mismatches before your code runs. // Compiler enforces User type throughout\nstore := grub.NewStore[User](provider)\nuser, _ := store.Get(ctx, \"key\")  // *User, not interface{}",{"id":61,"title":62,"titles":63,"content":64,"level":35},"/v1.0.18/overview#consistency","Consistency",[6,53],"Same error types across all providers. ErrNotFound means the same thing whether you're using Redis or S3. // Works identically for any provider\nif errors.Is(err, grub.ErrNotFound) {\n    return createDefault()\n}",{"id":66,"title":67,"titles":68,"content":69,"level":35},"/v1.0.18/overview#atomization","Atomization",[6,53],"For framework internals that need field-level access (encryption, pipelines), atomic views expose the underlying structure without breaking type safety. atomicStore := store.Atomic()\natom, _ := atomicStore.Get(ctx, \"key\")\n// Access individual fields via atom.Spec()",{"id":71,"title":72,"titles":73,"content":74,"level":19},"/v1.0.18/overview#when-to-use-grub","When to Use Grub",[6],"Good fit: Applications that may switch storage backendsMulti-tenant systems with different storage per tenantLibraries that need storage abstractionPrototyping with embedded DBs, deploying with cloud servicesRAG applications needing portable vector storage Consider alternatives: Single-provider applications with provider-specific featuresHigh-performance scenarios requiring provider optimizationsApplications using provider-specific query languages extensivelyVector search requiring provider-specific operators (see operator support matrix) html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}",{"id":76,"title":77,"titles":78,"content":79,"level":9},"/v1.0.18/learn/quickstart","Quickstart",[],"Get started with grub in under 5 minutes",{"id":81,"title":77,"titles":82,"content":83,"level":9},"/v1.0.18/learn/quickstart#quickstart",[],"Get type-safe storage working in under 5 minutes.",{"id":85,"title":86,"titles":87,"content":88,"level":19},"/v1.0.18/learn/quickstart#requirements","Requirements",[77],"Go 1.24 or higher",{"id":90,"title":91,"titles":92,"content":93,"level":19},"/v1.0.18/learn/quickstart#installation","Installation",[77],"go get github.com/zoobz-io/grub Install providers as needed: # Key-value stores\ngo get github.com/zoobz-io/grub/redis\ngo get github.com/zoobz-io/grub/badger\ngo get github.com/zoobz-io/grub/bolt\n\n# Blob storage\ngo get github.com/zoobz-io/grub/s3\ngo get github.com/zoobz-io/grub/minio\ngo get github.com/zoobz-io/grub/gcs\ngo get github.com/zoobz-io/grub/azure\n\n# Vector search\ngo get github.com/zoobz-io/grub/qdrant\ngo get github.com/zoobz-io/grub/pinecone\ngo get github.com/zoobz-io/grub/milvus\ngo get github.com/zoobz-io/grub/weaviate",{"id":95,"title":96,"titles":97,"content":29,"level":19},"/v1.0.18/learn/quickstart#basic-usage","Basic Usage",[77],{"id":99,"title":100,"titles":101,"content":102,"level":35},"/v1.0.18/learn/quickstart#key-value-store-with-redis","Key-Value Store with Redis",[77,96],"package main\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"time\"\n\n    \"github.com/zoobz-io/grub\"\n    \"github.com/zoobz-io/grub/redis\"\n    goredis \"github.com/redis/go-redis/v9\"\n)\n\ntype Session struct {\n    UserID    string `json:\"user_id\"`\n    Token     string `json:\"token\"`\n    ExpiresAt int64  `json:\"expires_at\"`\n}\n\nfunc main() {\n    ctx := context.Background()\n\n    // Connect to Redis\n    client := goredis.NewClient(&goredis.Options{Addr: \"localhost:6379\"})\n    defer client.Close()\n\n    // Create type-safe store\n    sessions := grub.NewStore[Session](redis.New(client))\n\n    // Store with TTL\n    session := &Session{\n        UserID:    \"user:123\",\n        Token:     \"abc123\",\n        ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),\n    }\n    _ = sessions.Set(ctx, \"session:abc123\", session, 24*time.Hour)\n\n    // Retrieve\n    s, _ := sessions.Get(ctx, \"session:abc123\")\n    fmt.Println(s.UserID) // user:123\n}",{"id":104,"title":105,"titles":106,"content":107,"level":35},"/v1.0.18/learn/quickstart#embedded-store-with-badgerdb","Embedded Store with BadgerDB",[77,96],"No external services required: package main\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"os\"\n\n    \"github.com/zoobz-io/grub\"\n    \"github.com/zoobz-io/grub/badger\"\n    badgerdb \"github.com/dgraph-io/badger/v4\"\n)\n\ntype Config struct {\n    Debug   bool   `json:\"debug\"`\n    Version string `json:\"version\"`\n}\n\nfunc main() {\n    ctx := context.Background()\n\n    // Open embedded database\n    opts := badgerdb.DefaultOptions(\"./data\")\n    opts.Logger = nil // Silence logs\n    db, _ := badgerdb.Open(opts)\n    defer db.Close()\n\n    // Create store\n    configs := grub.NewStore[Config](badger.New(db))\n\n    // Store (TTL=0 means no expiration)\n    _ = configs.Set(ctx, \"app:config\", &Config{Debug: true, Version: \"1.0\"}, 0)\n\n    // Retrieve\n    cfg, _ := configs.Get(ctx, \"app:config\")\n    fmt.Printf(\"Debug: %v, Version: %s\\n\", cfg.Debug, cfg.Version)\n\n    // Cleanup\n    os.RemoveAll(\"./data\")\n}",{"id":109,"title":110,"titles":111,"content":112,"level":35},"/v1.0.18/learn/quickstart#blob-storage-with-s3","Blob Storage with S3",[77,96],"package main\n\nimport (\n    \"context\"\n    \"fmt\"\n\n    \"github.com/zoobz-io/grub\"\n    \"github.com/zoobz-io/grub/s3\"\n    \"github.com/aws/aws-sdk-go-v2/config\"\n    awss3 \"github.com/aws/aws-sdk-go-v2/service/s3\"\n)\n\ntype Document struct {\n    Title   string `json:\"title\"`\n    Content string `json:\"content\"`\n}\n\nfunc main() {\n    ctx := context.Background()\n\n    // Configure AWS client\n    cfg, _ := config.LoadDefaultConfig(ctx)\n    client := awss3.NewFromConfig(cfg)\n\n    // Create bucket wrapper\n    docs := grub.NewBucket[Document](s3.New(client, \"my-bucket\"))\n\n    // Store object\n    _ = docs.Put(ctx, &grub.Object[Document]{\n        Key:         \"docs/readme.json\",\n        ContentType: \"application/json\",\n        Data:        Document{Title: \"README\", Content: \"Hello, world!\"},\n    })\n\n    // Retrieve\n    obj, _ := docs.Get(ctx, \"docs/readme.json\")\n    fmt.Println(obj.Data.Title) // README\n}",{"id":114,"title":115,"titles":116,"content":117,"level":35},"/v1.0.18/learn/quickstart#vector-search-with-qdrant","Vector Search with Qdrant",[77,96],"package main\n\nimport (\n    \"context\"\n    \"fmt\"\n\n    \"github.com/qdrant/go-client/qdrant\"\n    \"github.com/zoobz-io/grub\"\n    grubqdrant \"github.com/zoobz-io/grub/qdrant\"\n)\n\ntype Embedding struct {\n    Category string `json:\"category\"`\n    Source   string `json:\"source\"`\n}\n\nfunc main() {\n    ctx := context.Background()\n\n    // Connect to Qdrant\n    client, _ := qdrant.NewClient(&qdrant.Config{\n        Host: \"localhost\",\n        Port: 6334,\n    })\n    defer client.Close()\n\n    // Create type-safe index\n    embeddings := grub.NewIndex[Embedding](grubqdrant.New(client, grubqdrant.Config{\n        Collection: \"documents\",\n    }))\n\n    // Store vector with metadata\n    vector := []float32{0.1, 0.2, 0.3}\n    _ = embeddings.Upsert(ctx, \"doc:1\", vector, &Embedding{\n        Category: \"tech\",\n        Source:   \"blog\",\n    })\n\n    // Similarity search\n    query := []float32{0.15, 0.25, 0.35}\n    results, _ := embeddings.Search(ctx, query, 10, nil)\n    for _, r := range results {\n        fmt.Printf(\"ID: %s, Score: %f\\n\", r.ID, r.Score)\n    }\n}",{"id":119,"title":120,"titles":121,"content":122,"level":19},"/v1.0.18/learn/quickstart#whats-happening","What's Happening",[77],"Provider wraps client — redis.New(client) creates a StoreProvider, qdrant.New(client) creates a VectorProviderWrapper adds type safety — grub.NewStore[T], grub.NewBucket[T], grub.NewIndex[T] wrap providersCodec handles serialization — JSON by default, Gob availableOperations return typed values — Get returns *T, Search returns []*Vector[T]",{"id":124,"title":125,"titles":126,"content":127,"level":19},"/v1.0.18/learn/quickstart#next-steps","Next Steps",[77],"Core Concepts — Understand stores, buckets, databases, and indexesProviders Guide — Choose and configure providersAPI Reference — Complete method documentation html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html pre.shiki code .suWN2, html code.shiki .suWN2{--shiki-default:var(--shiki-tag)}",{"id":129,"title":130,"titles":131,"content":132,"level":9},"/v1.0.18/learn/concepts","Core Concepts",[],"Stores, buckets, databases, and the primitives that power grub",{"id":134,"title":130,"titles":135,"content":136,"level":9},"/v1.0.18/learn/concepts#core-concepts",[],"Grub has four storage modes, each with a type-safe wrapper and provider interface.",{"id":138,"title":139,"titles":140,"content":141,"level":19},"/v1.0.18/learn/concepts#storage-modes","Storage Modes",[130],"ModeWrapperProvider InterfaceUse CaseKey-ValueStore[T]StoreProviderSessions, cache, configBlobBucket[T]BucketProviderFiles, media, documentsSQLDatabase[T]edamame.FactoryStructured recordsVectorIndex[T]VectorProviderSimilarity search, RAG",{"id":143,"title":144,"titles":145,"content":146,"level":19},"/v1.0.18/learn/concepts#stores-key-value","Stores (Key-Value)",[130],"Stores map string keys to typed values with optional TTL.",{"id":148,"title":149,"titles":150,"content":151,"level":35},"/v1.0.18/learn/concepts#storeprovider-interface","StoreProvider Interface",[130,144],"Providers implement raw byte operations: type StoreProvider interface {\n    Get(ctx context.Context, key string) ([]byte, error)\n    Set(ctx context.Context, key string, value []byte, ttl time.Duration) error\n    Delete(ctx context.Context, key string) error\n    Exists(ctx context.Context, key string) (bool, error)\n    List(ctx context.Context, prefix string, limit int) ([]string, error)\n    GetBatch(ctx context.Context, keys []string) (map[string][]byte, error)\n    SetBatch(ctx context.Context, items map[string][]byte, ttl time.Duration) error\n}",{"id":153,"title":154,"titles":155,"content":156,"level":35},"/v1.0.18/learn/concepts#storet-wrapper","StoreT Wrapper",[130,144],"The wrapper adds type safety and serialization: store := grub.NewStore[Session](redis.New(client))\n\n// All operations are type-safe\nsession, err := store.Get(ctx, \"key\")      // Returns *Session\nerr = store.Set(ctx, \"key\", &session, ttl) // Accepts *Session\nexists, err := store.Exists(ctx, \"key\")    // Returns bool\nkeys, err := store.List(ctx, \"prefix:\", 0) // Returns []string",{"id":158,"title":159,"titles":160,"content":161,"level":35},"/v1.0.18/learn/concepts#ttl-behavior","TTL Behavior",[130,144],"ttl > 0 — Key expires after durationttl == 0 — No expirationBoltDB does not support TTL (returns ErrTTLNotSupported)",{"id":163,"title":164,"titles":165,"content":166,"level":19},"/v1.0.18/learn/concepts#buckets-blob-storage","Buckets (Blob Storage)",[130],"Buckets store objects with metadata (content type, size, custom headers).",{"id":168,"title":169,"titles":170,"content":171,"level":35},"/v1.0.18/learn/concepts#bucketprovider-interface","BucketProvider Interface",[130,164],"type BucketProvider interface {\n    Get(ctx context.Context, key string) ([]byte, *ObjectInfo, error)\n    Put(ctx context.Context, key string, data []byte, info *ObjectInfo) error\n    Delete(ctx context.Context, key string) error\n    Exists(ctx context.Context, key string) (bool, error)\n    List(ctx context.Context, prefix string, limit int) ([]ObjectInfo, error)\n}",{"id":173,"title":174,"titles":175,"content":176,"level":35},"/v1.0.18/learn/concepts#objectt-structure","ObjectT Structure",[130,164],"Objects combine metadata with typed payload: type Object[T any] struct {\n    Key         string            // Required: object path\n    ContentType string            // MIME type\n    Size        int64             // Computed from encoded data\n    ETag        string            // Version identifier\n    Metadata    map[string]string // Custom headers\n    Data        T                 // Your typed payload\n}",{"id":178,"title":179,"titles":180,"content":181,"level":35},"/v1.0.18/learn/concepts#buckett-wrapper","BucketT Wrapper",[130,164],"bucket := grub.NewBucket[Document](s3.New(client, \"bucket\"))\n\n// Put stores the object\nerr := bucket.Put(ctx, &grub.Object[Document]{\n    Key:         \"docs/file.json\",\n    ContentType: \"application/json\",\n    Metadata:    map[string]string{\"author\": \"alice\"},\n    Data:        Document{Title: \"Hello\"},\n})\n\n// Get returns Object with typed Data\nobj, err := bucket.Get(ctx, \"docs/file.json\")\nfmt.Println(obj.Data.Title)     // \"Hello\"\nfmt.Println(obj.Metadata[\"author\"]) // \"alice\"",{"id":183,"title":184,"titles":185,"content":186,"level":35},"/v1.0.18/learn/concepts#objectinfo-metadata-only","ObjectInfo (Metadata Only)",[130,164],"For listing without loading payload: type ObjectInfo struct {\n    Key         string\n    ContentType string\n    Size        int64\n    ETag        string\n    Metadata    map[string]string\n}\n\n// List returns metadata only\ninfos, _ := bucket.List(ctx, \"docs/\", 100)\nfor _, info := range infos {\n    fmt.Println(info.Key, info.Size)\n}",{"id":188,"title":189,"titles":190,"content":191,"level":19},"/v1.0.18/learn/concepts#databases-sql","Databases (SQL)",[130],"Databases provide CRUD operations on SQL tables with query capabilities.",{"id":193,"title":194,"titles":195,"content":196,"level":35},"/v1.0.18/learn/concepts#databaset-constructor","DatabaseT Constructor",[130,189],"db := grub.NewDatabase[User](\n    sqlxDB,        // *sqlx.DB connection\n    \"users\",       // Table name\n    sqlite.New(),  // SQL dialect renderer\n) The primary key column is derived automatically from struct tags: type User struct {\n    ID    int    `db:\"id\" constraints:\"primarykey\"`\n    Email string `db:\"email\"`\n}",{"id":198,"title":199,"titles":200,"content":201,"level":35},"/v1.0.18/learn/concepts#operations","Operations",[130,189],"// CRUD via key\nuser, err := db.Get(ctx, \"123\")\nerr = db.Set(ctx, \"123\", &User{ID: \"123\", Name: \"Alice\"})\nerr = db.Delete(ctx, \"123\")\nexists, err := db.Exists(ctx, \"123\")\n\n// Query builders for ad-hoc queries\nusers, err := db.Query().Where(\"status\", \"=\", \"active\").Exec(ctx, map[string]any{\"active\": \"enabled\"})\nuser, err := db.Select().Where(\"email\", \"=\", \"email\").Exec(ctx, map[string]any{\"email\": \"alice@example.com\"})\n\n// Pre-defined statements via edamame\nusers, err := db.ExecQuery(ctx, grub.QueryAll, nil)",{"id":203,"title":204,"titles":205,"content":206,"level":35},"/v1.0.18/learn/concepts#set-behavior","Set Behavior",[130,189],"Set performs an upsert (insert or update on conflict): // First call inserts\ndb.Set(ctx, \"1\", &User{ID: \"1\", Name: \"Alice\"})\n\n// Second call updates\ndb.Set(ctx, \"1\", &User{ID: \"1\", Name: \"Alice Smith\"})",{"id":208,"title":209,"titles":210,"content":211,"level":19},"/v1.0.18/learn/concepts#indexes-vector","Indexes (Vector)",[130],"Indexes store vectors with typed metadata for similarity search.",{"id":213,"title":214,"titles":215,"content":216,"level":35},"/v1.0.18/learn/concepts#vectorprovider-interface","VectorProvider Interface",[130,209],"Providers implement vector storage and search: type VectorProvider interface {\n    Upsert(ctx context.Context, id string, vector []float32, metadata map[string]any) error\n    UpsertBatch(ctx context.Context, vectors []VectorRecord) error\n    Get(ctx context.Context, id string) ([]float32, *VectorInfo, error)\n    Delete(ctx context.Context, id string) error\n    DeleteBatch(ctx context.Context, ids []string) error\n    Search(ctx context.Context, vector []float32, k int, filter map[string]any) ([]VectorResult, error)\n    Query(ctx context.Context, vector []float32, k int, filter *vecna.Filter) ([]VectorResult, error)\n    List(ctx context.Context, prefix string, limit int) ([]string, error)\n    Exists(ctx context.Context, id string) (bool, error)\n}",{"id":218,"title":219,"titles":220,"content":221,"level":35},"/v1.0.18/learn/concepts#indext-wrapper","IndexT Wrapper",[130,209],"The wrapper adds type safety for metadata: index := grub.NewIndex[Embedding](qdrant.New(client, qdrant.Config{\n    Collection: \"documents\",\n}))\n\n// All operations are type-safe\nerr := index.Upsert(ctx, \"id\", vector, &Embedding{Category: \"tech\"})\nresult, err := index.Get(ctx, \"id\")        // Returns *Vector[Embedding]\nresults, err := index.Search(ctx, vec, 10, nil)  // Returns []*Vector[Embedding]",{"id":223,"title":224,"titles":225,"content":226,"level":35},"/v1.0.18/learn/concepts#vectort-structure","VectorT Structure",[130,209],"Search results include the vector, score, and typed metadata: type Vector[T any] struct {\n    ID       string    // Vector identifier\n    Vector   []float32 // The embedding\n    Score    float32   // Similarity score (distance)\n    Metadata T         // Your typed payload\n}",{"id":228,"title":229,"titles":230,"content":231,"level":35},"/v1.0.18/learn/concepts#query-with-filters","Query with Filters",[130,209],"Use vecna for type-safe filter building: import \"github.com/zoobz-io/vecna\"\n\n// Build filter\nfilter := vecna.And(\n    vecna.Eq(\"category\", \"tech\"),\n    vecna.Gte(\"score\", 0.8),\n)\n\n// Query with filter\nresults, err := index.Query(ctx, queryVector, 10, filter) Note: Filter operator support varies by provider. See Provider Reference for the operator support matrix.",{"id":233,"title":234,"titles":235,"content":236,"level":19},"/v1.0.18/learn/concepts#lifecycle-hooks","Lifecycle Hooks",[130],"Types can opt into lifecycle hooks by implementing one or more hook interfaces. When present, grub calls these hooks at specific points during CRUD operations across all four storage types.",{"id":238,"title":239,"titles":240,"content":241,"level":35},"/v1.0.18/learn/concepts#hook-interfaces","Hook Interfaces",[130,234],"InterfaceWhen CalledEffect of ErrorBeforeSaveBefore persisting TAborts the writeAfterSaveAfter successful persistSignals post-save failureAfterLoadAfter T is loaded and decodedSignals broken dataBeforeDeleteBefore deletionAborts the deleteAfterDeleteAfter successful deletionSignals post-delete failure All hooks receive context.Context and return error.",{"id":243,"title":244,"titles":245,"content":246,"level":35},"/v1.0.18/learn/concepts#usage","Usage",[130,234],"Implement the interface on your type with a pointer receiver: type User struct {\n    ID    string `db:\"id\" constraints:\"primarykey\"`\n    Name  string `db:\"name\"`\n    Email string `db:\"email\"`\n}\n\nfunc (u *User) BeforeSave(ctx context.Context) error {\n    if u.Email == \"\" {\n        return errors.New(\"email is required\")\n    }\n    return nil\n}\n\nfunc (u *User) AfterLoad(ctx context.Context) error {\n    u.Email = strings.ToLower(u.Email)\n    return nil\n} Hooks are opt-in. Types that don't implement any hook interface work unchanged with zero overhead.",{"id":248,"title":249,"titles":250,"content":251,"level":35},"/v1.0.18/learn/concepts#delete-hooks","Delete Hooks",[130,234],"Delete hooks are invoked on a zero-value T since delete operations only have a key/ID, not a loaded instance. They act as static guards or side-effects on the type: func (u *User) BeforeDelete(ctx context.Context) error {\n    // Guard or side-effect — no instance state available\n    return nil\n}",{"id":253,"title":254,"titles":255,"content":256,"level":35},"/v1.0.18/learn/concepts#scope","Scope",[130,234],"Hooks fire on all typed interactions: StoreT: Get, Set, Delete, GetBatch, SetBatchBucketT: Get, Put, Delete (hooks fire on the T payload, not Object[T])DatabaseT: Get, Set, Delete, and all query builder/statement execution resultsIndexT: Upsert, UpsertBatch, Get, Delete, DeleteBatch, Search, Query, Filter Atomic views do not trigger hooks — they operate below the type-aware layer.",{"id":258,"title":259,"titles":260,"content":261,"level":19},"/v1.0.18/learn/concepts#codecs","Codecs",[130],"Codecs handle serialization between typed values and bytes.",{"id":263,"title":264,"titles":265,"content":266,"level":35},"/v1.0.18/learn/concepts#built-in-codecs","Built-in Codecs",[130,259],"CodecFormatUse CaseJSONCodecJSONDefault, portable, human-readableGobCodecGobGo-specific, more compact",{"id":268,"title":269,"titles":270,"content":271,"level":35},"/v1.0.18/learn/concepts#custom-codec","Custom Codec",[130,259],"// Use Gob instead of JSON\nstore := grub.NewStoreWithCodec[Config](\n    provider,\n    grub.GobCodec{},\n)",{"id":273,"title":274,"titles":275,"content":276,"level":35},"/v1.0.18/learn/concepts#codec-interface","Codec Interface",[130,259],"type Codec interface {\n    Encode(v any) ([]byte, error)\n    Decode(data []byte, v any) error\n}",{"id":278,"title":279,"titles":280,"content":281,"level":19},"/v1.0.18/learn/concepts#semantic-errors","Semantic Errors",[130],"All providers return consistent error types: ErrorMeaningErrNotFoundRecord does not existErrDuplicateKey already existsErrConflictConcurrent modificationErrConstraintConstraint violationErrInvalidKeyKey malformed or emptyErrReadOnlyWrite on read-only connectionErrTTLNotSupportedProvider doesn't support TTLErrDimensionMismatchVector dimension doesn't match indexErrInvalidVectorVector is malformed (nil, empty, NaN)ErrIndexNotReadyIndex not loaded or initializedErrInvalidQueryFilter contains validation errorsErrOperatorNotSupportedProvider doesn't support filter operator Check errors with errors.Is: user, err := store.Get(ctx, \"missing\")\nif errors.Is(err, grub.ErrNotFound) {\n    // Handle missing record\n}",{"id":283,"title":284,"titles":285,"content":286,"level":19},"/v1.0.18/learn/concepts#atomic-views","Atomic Views",[130],"For framework internals needing field-level access: // Get atomic view (lazily cached)\natomicStore := store.Atomic()\natomicIndex := index.Atomic()\n\n// Access raw atom structure\natom, err := atomicStore.Get(ctx, \"key\")\nspec := atomicStore.Spec() // Field metadata Warning: Atomic() panics if T is not atomizable. Check at startup. See Architecture for details on atomic views. html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}",{"id":288,"title":16,"titles":289,"content":290,"level":9},"/v1.0.18/learn/architecture",[],"Internal design, concurrency model, and atomization",{"id":292,"title":16,"titles":293,"content":294,"level":9},"/v1.0.18/learn/architecture#architecture",[],"This document covers grub's internal design for those who need deeper understanding.",{"id":296,"title":297,"titles":298,"content":299,"level":19},"/v1.0.18/learn/architecture#layer-model","Layer Model",[16],"┌─────────────────────────────────────────────────────────────────────────┐\n│                          Application Layer                               │\n│         Store[T], Bucket[T], Database[T], Index[T] wrappers             │\n├─────────────────────────────────────────────────────────────────────────┤\n│                           Codec Layer                                    │\n│                  JSONCodec, GobCodec, custom                            │\n├─────────────────────────────────────────────────────────────────────────┤\n│                          Provider Layer                                  │\n│      StoreProvider, BucketProvider, VectorProvider interfaces           │\n├─────────────────────────────────────────────────────────────────────────┤\n│                        Implementation Layer                              │\n│  redis, badger, bolt, s3, minio, gcs, azure, qdrant, pinecone, etc.   │\n└─────────────────────────────────────────────────────────────────────────┘ Data flow for Get: Application calls store.Get(ctx, \"key\")Wrapper calls provider.Get(ctx, \"key\") → []byteCodec decodes bytes → typed valueIf T implements AfterLoad, the hook is calledWrapper returns *T Data flow for Set: Application calls store.Set(ctx, \"key\", &value, ttl)If T implements BeforeSave, the hook is called (error aborts)Codec encodes value → []byteWrapper calls provider.Set(ctx, \"key\", bytes, ttl)If T implements AfterSave, the hook is called Data flow for Delete: Application calls store.Delete(ctx, \"key\")If T implements BeforeDelete, the hook is called on a zero-value T (error aborts)Wrapper calls provider.Delete(ctx, \"key\")If T implements AfterDelete, the hook is called on a zero-value T Hooks are opt-in. Types that don't implement any hook interface skip these steps with zero overhead. Hooks fire at the wrapper layer — atomic views and raw provider calls do not trigger hooks.",{"id":301,"title":302,"titles":303,"content":29,"level":19},"/v1.0.18/learn/architecture#wrapper-implementation","Wrapper Implementation",[16],{"id":305,"title":306,"titles":307,"content":308,"level":35},"/v1.0.18/learn/architecture#storet","StoreT",[16,302],"type Store[T any] struct {\n    provider StoreProvider\n    codec    Codec\n    atomic   *atomic.Store[T]  // Lazily initialized\n    once     sync.Once\n}\n\nfunc (s *Store[T]) Get(ctx context.Context, key string) (*T, error) {\n    data, err := s.provider.Get(ctx, key)\n    if err != nil {\n        return nil, err\n    }\n    var v T\n    if err := s.codec.Decode(data, &v); err != nil {\n        return nil, err\n    }\n    return &v, nil\n}",{"id":310,"title":311,"titles":312,"content":313,"level":35},"/v1.0.18/learn/architecture#lazy-atomic-initialization","Lazy Atomic Initialization",[16,302],"Atomic views are created on first access and cached: func (s *Store[T]) Atomic() *atomic.Store[T] {\n    s.once.Do(func() {\n        s.atomic = atomic.NewStore[T](s.provider, s.codec)\n    })\n    return s.atomic\n} Panic behavior: If T is not atomizable, Atomic() panics on first call. This is intentional—check atomizability at application startup, not runtime.",{"id":315,"title":316,"titles":317,"content":29,"level":19},"/v1.0.18/learn/architecture#provider-contracts","Provider Contracts",[16],{"id":319,"title":320,"titles":321,"content":322,"level":35},"/v1.0.18/learn/architecture#error-semantics","Error Semantics",[16,316],"OperationMissing Key BehaviorGetReturns ErrNotFoundDeleteReturns ErrNotFoundExistsReturns false, nilGetBatchOmits key from result map",{"id":324,"title":325,"titles":326,"content":327,"level":35},"/v1.0.18/learn/architecture#ttl-handling","TTL Handling",[16,316],"ProviderTTL SupportRedisFull (native)BadgerFull (native)BoltNone (ErrTTLNotSupported)S3/GCS/AzureN/A (blob storage)",{"id":329,"title":330,"titles":331,"content":332,"level":35},"/v1.0.18/learn/architecture#batch-operations","Batch Operations",[16,316],"GetBatch: Missing keys are silently omitted from the result map. No error is returned for missing keys. results, err := store.GetBatch(ctx, []string{\"exists\", \"missing\"})\n// results = {\"exists\": value}  // \"missing\" not present\n// err = nil SetBatch: Atomicity varies by provider: Redis: Pipelined (each operation independent)Badger: WriteBatch (atomic)Bolt: Single transaction (atomic)",{"id":334,"title":335,"titles":336,"content":337,"level":19},"/v1.0.18/learn/architecture#context-handling","Context Handling",[16],"All operations accept context.Context for cancellation and timeouts.",{"id":339,"title":340,"titles":341,"content":342,"level":35},"/v1.0.18/learn/architecture#provider-specific-behavior","Provider-Specific Behavior",[16,335],"ProviderContext CancellationRedisHonored on all operationsBadgerHonored during iteration (List)BoltHonored during iteration (List)S3Honored on all operationsGCSHonored on all operationsAzureHonored on all operations",{"id":344,"title":345,"titles":346,"content":347,"level":35},"/v1.0.18/learn/architecture#transaction-support-database","Transaction Support (Database)",[16,335],"Database[T] provides *Tx method variants for transaction support: // Create database wrapper\ndb := grub.NewDatabase[User](sqlxDB, \"users\", sqlite.New())\n\n// Use *Tx methods within a transaction\ntx, _ := sqlxDB.BeginTxx(ctx, nil)\ndefer tx.Rollback()\n\nuser, _ := db.GetTx(ctx, tx, \"123\")\nuser.Name = \"Updated\"\n_ = db.SetTx(ctx, tx, \"123\", user)\n\ntx.Commit() All operations have *Tx variants: GetTx, SetTx, DeleteTx, ExistsTx, QueryTx, SelectTx, UpdateTx, AggregateTx.",{"id":349,"title":284,"titles":350,"content":351,"level":19},"/v1.0.18/learn/architecture#atomic-views",[16],"Atomic views provide type-agnostic access to field structure, used by framework internals for: Field-level encryptionData pipelinesSchema introspection",{"id":353,"title":16,"titles":354,"content":355,"level":35},"/v1.0.18/learn/architecture#architecture-1",[16,284],"┌─────────────────────────────────────────────────────────┐\n│                    Store[T]                             │\n│                        │                                │\n│                        ▼                                │\n│              ┌─────────────────┐                        │\n│              │  atomic.Store   │ ◄── Lazily created     │\n│              └────────┬────────┘                        │\n│                       │                                 │\n│                       ▼                                 │\n│              ┌─────────────────┐                        │\n│              │   atom.Spec     │ ◄── Field metadata     │\n│              └─────────────────┘                        │\n└─────────────────────────────────────────────────────────┘",{"id":357,"title":358,"titles":359,"content":360,"level":35},"/v1.0.18/learn/architecture#atomic-interfaces","Atomic Interfaces",[16,284],"// AtomicStore - type-agnostic key-value access\ntype AtomicStore interface {\n    Spec() atom.Spec\n    Get(ctx context.Context, key string) (*atom.Atom, error)\n    Set(ctx context.Context, key string, a *atom.Atom, ttl time.Duration) error\n    Delete(ctx context.Context, key string) error\n    Exists(ctx context.Context, key string) (bool, error)\n}\n\n// AtomicBucket - type-agnostic blob access\ntype AtomicBucket interface {\n    Spec() atom.Spec\n    Get(ctx context.Context, key string) (*AtomicObject, error)\n    Put(ctx context.Context, key string, obj *AtomicObject) error\n    Delete(ctx context.Context, key string) error\n    Exists(ctx context.Context, key string) (bool, error)\n}\n\n// AtomicDatabase - type-agnostic SQL access\ntype AtomicDatabase interface {\n    Table() string\n    Spec() atom.Spec\n    Get(ctx context.Context, key string) (*atom.Atom, error)\n    Set(ctx context.Context, key string, a *atom.Atom) error\n    Delete(ctx context.Context, key string) error\n    Exists(ctx context.Context, key string) (bool, error)\n    Query(ctx context.Context, name string, params map[string]any) ([]*atom.Atom, error)\n    Select(ctx context.Context, name string, params map[string]any) (*atom.Atom, error)\n}\n\n// AtomicIndex - type-agnostic vector access\ntype AtomicIndex interface {\n    Spec() atom.Spec\n    Get(ctx context.Context, id string) (*AtomicVector, error)\n    Upsert(ctx context.Context, id string, vector []float32, metadata *atom.Atom) error\n    Delete(ctx context.Context, id string) error\n    Exists(ctx context.Context, id string) (bool, error)\n    Search(ctx context.Context, vector []float32, k int, filter *atom.Atom) ([]AtomicVector, error)\n    Query(ctx context.Context, vector []float32, k int, filter *vecna.Filter) ([]AtomicVector, error)\n}",{"id":362,"title":363,"titles":364,"content":365,"level":35},"/v1.0.18/learn/architecture#atomization-requirements","Atomization Requirements",[16,284],"Types must be atomizable (via atom package): type User struct {\n    ID    string `json:\"id\" atom:\"id\"`\n    Name  string `json:\"name\" atom:\"name\"`\n    Email string `json:\"email\" atom:\"email\"`\n} If T lacks atom tags or has unsupported field types, Atomic() panics.",{"id":367,"title":368,"titles":369,"content":370,"level":19},"/v1.0.18/learn/architecture#concurrency-guarantees","Concurrency Guarantees",[16],"GuaranteeScopeThread-safeAll wrapper methodsNo data racesProvider implementationsAtomic lazy initsync.Once for atomic views Wrappers are safe for concurrent use. Provider thread-safety depends on the underlying client (Redis client, Badger DB, etc.).",{"id":372,"title":373,"titles":374,"content":29,"level":19},"/v1.0.18/learn/architecture#memory-management","Memory Management",[16],{"id":376,"title":377,"titles":378,"content":379,"level":35},"/v1.0.18/learn/architecture#object-pooling","Object Pooling",[16,373],"Grub does not pool objects. Each Get allocates a new value. For high-throughput scenarios, consider: Application-level poolingReusing structs where safeUsing atomic views for field-level operations",{"id":381,"title":382,"titles":383,"content":384,"level":35},"/v1.0.18/learn/architecture#codec-allocation","Codec Allocation",[16,373],"JSONCodec: Allocates per encode/decodeGobCodec: Encoder/decoder created per operation For performance-critical paths, consider custom codecs with pooled buffers.",{"id":386,"title":387,"titles":388,"content":29,"level":19},"/v1.0.18/learn/architecture#dependencies","Dependencies",[16],{"id":390,"title":391,"titles":392,"content":393,"level":35},"/v1.0.18/learn/architecture#core-package","Core Package",[16,387],"github.com/zoobz-io/atom      # Field atomization\ngithub.com/zoobz-io/edamame   # SQL query building\ngithub.com/zoobz-io/soy       # SQL execution\ngithub.com/zoobz-io/astql     # SQL dialect rendering\ngithub.com/jmoiron/sqlx      # SQL toolkit",{"id":395,"title":396,"titles":397,"content":398,"level":35},"/v1.0.18/learn/architecture#provider-packages","Provider Packages",[16,387],"Each provider is a separate module with its own dependencies: grub/redis → github.com/redis/go-redis/v9grub/badger → github.com/dgraph-io/badger/v4grub/bolt → go.etcd.io/bboltgrub/s3 → github.com/aws/aws-sdk-go-v2grub/minio → github.com/minio/minio-go/v7grub/gcs → cloud.google.com/go/storagegrub/azure → github.com/Azure/azure-sdk-for-gogrub/qdrant → github.com/qdrant/go-clientgrub/pinecone → github.com/pinecone-io/go-pinecone/v2grub/milvus → github.com/milvus-io/milvus-sdk-go/v2grub/weaviate → github.com/weaviate/weaviate-go-client/v5 This isolation ensures consumers only pull dependencies they use. html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}",{"id":400,"title":401,"titles":402,"content":403,"level":9},"/v1.0.18/guides/providers","Providers",[],"Choosing and configuring storage providers",{"id":405,"title":401,"titles":406,"content":407,"level":9},"/v1.0.18/guides/providers#providers",[],"This guide covers provider selection, configuration, and provider-specific behaviors.",{"id":409,"title":410,"titles":411,"content":29,"level":19},"/v1.0.18/guides/providers#provider-overview","Provider Overview",[401],{"id":413,"title":414,"titles":415,"content":416,"level":35},"/v1.0.18/guides/providers#key-value-stores","Key-Value Stores",[401,410],"ProviderPackageBest ForRedisgrub/redisDistributed cache, sessions, pub/subBadgerDBgrub/badgerEmbedded, high-performance, SSD-optimizedBoltDBgrub/boltEmbedded, simple, single-file",{"id":418,"title":38,"titles":419,"content":420,"level":35},"/v1.0.18/guides/providers#blob-storage",[401,410],"ProviderPackageBest ForAWS S3grub/s3Standard object storage, AWS ecosystemMinIOgrub/minioSelf-hosted, S3-compatible, lightweightGoogle Cloud Storagegrub/gcsGCP ecosystem, global CDNAzure Blobgrub/azureAzure ecosystem, tiered storage",{"id":422,"title":423,"titles":424,"content":425,"level":35},"/v1.0.18/guides/providers#sql-databases","SQL Databases",[401,410],"DriverPackageDialectPostgreSQLgrub/postgresastql/postgresMariaDBgrub/mariadbastql/mariadbSQLitegrub/sqliteastql/sqliteSQL Servergrub/mssqlastql/mssql",{"id":427,"title":428,"titles":429,"content":430,"level":35},"/v1.0.18/guides/providers#vector-databases","Vector Databases",[401,410],"ProviderPackageBest ForQdrantgrub/qdrantSelf-hosted, full-featured, gRPCPineconegrub/pineconeManaged service, simple APIMilvusgrub/milvusLarge-scale, distributedWeaviategrub/weaviateGraphQL API, semantic search",{"id":432,"title":433,"titles":434,"content":29,"level":19},"/v1.0.18/guides/providers#key-value-provider-setup","Key-Value Provider Setup",[401],{"id":436,"title":437,"titles":438,"content":439,"level":35},"/v1.0.18/guides/providers#redis","Redis",[401,433],"import (\n    \"github.com/zoobz-io/grub\"\n    \"github.com/zoobz-io/grub/redis\"\n    goredis \"github.com/redis/go-redis/v9\"\n)\n\nfunc main() {\n    client := goredis.NewClient(&goredis.Options{\n        Addr:     \"localhost:6379\",\n        Password: \"\",\n        DB:       0,\n    })\n    defer client.Close()\n\n    store := grub.NewStore[Session](redis.New(client))\n} Features: Full TTL support (native)List uses SCAN (cursor-based, safe for large datasets)GetBatch uses MGETSetBatch uses pipelined SET Cluster support: client := goredis.NewClusterClient(&goredis.ClusterOptions{\n    Addrs: []string{\"node1:6379\", \"node2:6379\", \"node3:6379\"},\n})\nstore := grub.NewStore[Session](redis.New(client))",{"id":441,"title":442,"titles":443,"content":444,"level":35},"/v1.0.18/guides/providers#badgerdb","BadgerDB",[401,433],"import (\n    \"github.com/zoobz-io/grub\"\n    \"github.com/zoobz-io/grub/badger\"\n    badgerdb \"github.com/dgraph-io/badger/v4\"\n)\n\nfunc main() {\n    opts := badgerdb.DefaultOptions(\"./data\")\n    opts.Logger = nil // Silence logs\n\n    db, err := badgerdb.Open(opts)\n    if err != nil {\n        log.Fatal(err)\n    }\n    defer db.Close()\n\n    store := grub.NewStore[Config](badger.New(db))\n} Features: Full TTL support (native)Embedded (no external service)SSD-optimized LSM treeSupports encryption at rest In-memory mode: opts := badgerdb.DefaultOptions(\"\").WithInMemory(true)\ndb, _ := badgerdb.Open(opts)",{"id":446,"title":447,"titles":448,"content":449,"level":35},"/v1.0.18/guides/providers#boltdb","BoltDB",[401,433],"import (\n    \"github.com/zoobz-io/grub\"\n    \"github.com/zoobz-io/grub/bolt\"\n    \"go.etcd.io/bbolt\"\n)\n\nfunc main() {\n    db, err := bbolt.Open(\"my.db\", 0600, nil)\n    if err != nil {\n        log.Fatal(err)\n    }\n    defer db.Close()\n\n    // Bucket name scopes the data\n    store := grub.NewStore[Config](bolt.New(db, \"configs\"))\n} Features: Single-file databaseBucket-scoped (multiple stores per DB)ACID transactionsNo TTL support (returns ErrTTLNotSupported) Multiple buckets: configs := grub.NewStore[Config](bolt.New(db, \"configs\"))\nsessions := grub.NewStore[Session](bolt.New(db, \"sessions\"))",{"id":451,"title":452,"titles":453,"content":29,"level":19},"/v1.0.18/guides/providers#blob-provider-setup","Blob Provider Setup",[401],{"id":455,"title":456,"titles":457,"content":458,"level":35},"/v1.0.18/guides/providers#aws-s3","AWS S3",[401,452],"import (\n    \"github.com/zoobz-io/grub\"\n    \"github.com/zoobz-io/grub/s3\"\n    \"github.com/aws/aws-sdk-go-v2/config\"\n    awss3 \"github.com/aws/aws-sdk-go-v2/service/s3\"\n)\n\nfunc main() {\n    cfg, err := config.LoadDefaultConfig(context.Background())\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    client := awss3.NewFromConfig(cfg)\n    bucket := grub.NewBucket[Document](s3.New(client, \"my-bucket\"))\n} Notes: Delete checks Exists first (S3 doesn't error on missing keys)List uses ListObjectsV2 with continuation tokensMetadata preserved in ObjectInfo Custom endpoint (LocalStack): client := awss3.NewFromConfig(cfg, func(o *awss3.Options) {\n    o.BaseEndpoint = aws.String(\"http://localhost:4566\")\n    o.UsePathStyle = true\n})",{"id":460,"title":461,"titles":462,"content":463,"level":35},"/v1.0.18/guides/providers#minio","MinIO",[401,452],"import (\n    \"github.com/zoobz-io/grub\"\n    grubminio \"github.com/zoobz-io/grub/minio\"\n    \"github.com/minio/minio-go/v7\"\n    \"github.com/minio/minio-go/v7/pkg/credentials\"\n)\n\nfunc main() {\n    client, err := minio.New(\"localhost:9000\", &minio.Options{\n        Creds:  credentials.NewStaticV4(\"minioadmin\", \"minioadmin\", \"\"),\n        Secure: false,\n    })\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    bucket := grub.NewBucket[Document](grubminio.New(client, \"my-bucket\"))\n} Notes: Delete checks Exists first (MinIO doesn't error on missing keys)Uses minio-go/v7 — lighter than the AWS SDKList uses channel-based iteration with recursive optionMetadata preserved via UserMetadata",{"id":465,"title":466,"titles":467,"content":468,"level":35},"/v1.0.18/guides/providers#google-cloud-storage","Google Cloud Storage",[401,452],"import (\n    \"github.com/zoobz-io/grub\"\n    \"github.com/zoobz-io/grub/gcs\"\n    \"cloud.google.com/go/storage\"\n)\n\nfunc main() {\n    client, err := storage.NewClient(context.Background())\n    if err != nil {\n        log.Fatal(err)\n    }\n    defer client.Close()\n\n    bucket := grub.NewBucket[Document](gcs.New(client, \"my-bucket\"))\n} Notes: ContentType set via writer.ContentTypeMetadata preservedUses storage.Iterator for List",{"id":470,"title":471,"titles":472,"content":473,"level":35},"/v1.0.18/guides/providers#azure-blob-storage","Azure Blob Storage",[401,452],"import (\n    \"github.com/zoobz-io/grub\"\n    \"github.com/zoobz-io/grub/azure\"\n    \"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob\"\n)\n\nfunc main() {\n    cred, err := azidentity.NewDefaultAzureCredential(nil)\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    client, err := azblob.NewClient(\n        \"https://myaccount.blob.core.windows.net/\",\n        cred,\n        nil,\n    )\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    bucket := grub.NewBucket[Document](azure.New(client, \"my-container\"))\n} Notes: Metadata may require separate GetProperties callUses NewListBlobsFlatPager for List",{"id":475,"title":476,"titles":477,"content":29,"level":19},"/v1.0.18/guides/providers#sql-database-setup","SQL Database Setup",[401],{"id":479,"title":480,"titles":481,"content":482,"level":35},"/v1.0.18/guides/providers#postgresql","PostgreSQL",[401,476],"import (\n    \"github.com/zoobz-io/grub\"\n    _ \"github.com/zoobz-io/grub/postgres\" // Register driver\n    \"github.com/zoobz-io/astql/postgres\"\n    \"github.com/jmoiron/sqlx\"\n)\n\nfunc main() {\n    db, err := sqlx.Connect(\"postgres\", \"postgres://user:pass@localhost/db?sslmode=disable\")\n    if err != nil {\n        log.Fatal(err)\n    }\n    defer db.Close()\n\n    users := grub.NewDatabase[User](db, \"users\", postgres.New())\n} Vector Search with pgvector: PostgreSQL with the pgvector extension is supported via Database[T]. Use soy's SelectExpr() and OrderByExpr() for distance calculations. See the Vector Search cookbook for complete examples.",{"id":484,"title":485,"titles":486,"content":487,"level":35},"/v1.0.18/guides/providers#mariadb","MariaDB",[401,476],"import (\n    _ \"github.com/zoobz-io/grub/mariadb\"\n    \"github.com/zoobz-io/astql/mariadb\"\n)\n\ndb, _ := sqlx.Connect(\"mysql\", \"user:pass@tcp(localhost:3306)/db\")\nusers, _ := grub.NewDatabase[User](db, \"users\", mariadb.New())",{"id":489,"title":490,"titles":491,"content":492,"level":35},"/v1.0.18/guides/providers#sqlite","SQLite",[401,476],"import (\n    _ \"github.com/zoobz-io/grub/sqlite\"\n    \"github.com/zoobz-io/astql/sqlite\"\n)\n\ndb, _ := sqlx.Connect(\"sqlite\", \"./data.db\")\nusers, _ := grub.NewDatabase[User](db, \"users\", sqlite.New())",{"id":494,"title":495,"titles":496,"content":497,"level":35},"/v1.0.18/guides/providers#sql-server","SQL Server",[401,476],"import (\n    _ \"github.com/zoobz-io/grub/mssql\"\n    \"github.com/zoobz-io/astql/mssql\"\n)\n\ndb, _ := sqlx.Connect(\"sqlserver\", \"sqlserver://user:pass@localhost?database=db\")\nusers, _ := grub.NewDatabase[User](db, \"users\", mssql.New())",{"id":499,"title":500,"titles":501,"content":29,"level":19},"/v1.0.18/guides/providers#vector-provider-setup","Vector Provider Setup",[401],{"id":503,"title":504,"titles":505,"content":506,"level":35},"/v1.0.18/guides/providers#qdrant","Qdrant",[401,500],"import (\n    \"github.com/qdrant/go-client/qdrant\"\n    \"github.com/zoobz-io/grub\"\n    grubqdrant \"github.com/zoobz-io/grub/qdrant\"\n)\n\nfunc main() {\n    client, err := qdrant.NewClient(&qdrant.Config{\n        Host: \"localhost\",\n        Port: 6334,\n    })\n    if err != nil {\n        log.Fatal(err)\n    }\n    defer client.Close()\n\n    index := grub.NewIndex[Embedding](grubqdrant.New(client, grubqdrant.Config{\n        Collection: \"documents\",\n    }))\n} Features: Full filter operator supportgRPC-based communicationString IDs (hashed internally to uint64)",{"id":508,"title":509,"titles":510,"content":511,"level":35},"/v1.0.18/guides/providers#pinecone","Pinecone",[401,500],"import (\n    \"github.com/pinecone-io/go-pinecone/v2/pinecone\"\n    \"github.com/zoobz-io/grub\"\n    grubpinecone \"github.com/zoobz-io/grub/pinecone\"\n)\n\nfunc main() {\n    client, err := pinecone.NewClient(pinecone.NewClientParams{\n        ApiKey: os.Getenv(\"PINECONE_API_KEY\"),\n    })\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    indexConn, err := client.Index(pinecone.NewIndexConnParams{\n        Host: \"your-index.svc.environment.pinecone.io\",\n    })\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    index := grub.NewIndex[Embedding](grubpinecone.New(indexConn, grubpinecone.Config{\n        Namespace: \"default\",\n    }))\n} Features: Managed service (no infrastructure)Limited filter operators (Eq, Ne, In, Nin only)",{"id":513,"title":514,"titles":515,"content":516,"level":35},"/v1.0.18/guides/providers#milvus","Milvus",[401,500],"import (\n    \"github.com/milvus-io/milvus-sdk-go/v2/client\"\n    \"github.com/zoobz-io/grub\"\n    grubmilvus \"github.com/zoobz-io/grub/milvus\"\n)\n\nfunc main() {\n    milvusClient, err := client.NewClient(ctx, client.Config{\n        Address: \"localhost:19530\",\n    })\n    if err != nil {\n        log.Fatal(err)\n    }\n    defer milvusClient.Close()\n\n    index := grub.NewIndex[Embedding](grubmilvus.New(milvusClient, grubmilvus.Config{\n        Collection:    \"documents\",\n        IDField:       \"id\",\n        VectorField:   \"embedding\",\n        MetadataField: \"metadata\",\n    }))\n} Features: Full filter operator supportConfigurable field namesDistributed architecture",{"id":518,"title":519,"titles":520,"content":521,"level":35},"/v1.0.18/guides/providers#weaviate","Weaviate",[401,500],"import (\n    \"github.com/weaviate/weaviate-go-client/v5/weaviate\"\n    \"github.com/zoobz-io/grub\"\n    grubweaviate \"github.com/zoobz-io/grub/weaviate\"\n)\n\nfunc main() {\n    cfg := weaviate.Config{\n        Host:   \"localhost:8080\",\n        Scheme: \"http\",\n    }\n    client, err := weaviate.NewClient(cfg)\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    index := grub.NewIndex[Embedding](grubweaviate.New(client, grubweaviate.Config{\n        Class:      \"Document\",\n        Properties: []string{\"category\", \"score\", \"tags\"}, // metadata fields to retrieve\n    }))\n} Features: Full filter operator supportRequires explicit Properties configuration for metadata retrievalGraphQL-based queries",{"id":523,"title":524,"titles":525,"content":29,"level":19},"/v1.0.18/guides/providers#provider-selection-guide","Provider Selection Guide",[401],{"id":527,"title":528,"titles":529,"content":530,"level":35},"/v1.0.18/guides/providers#when-to-use-each-provider","When to Use Each Provider",[401,524],"ScenarioRecommended ProviderDistributed sessionsRedisLocal cacheBadgerDBSimple config storageBoltDBFile uploadsS3/MinIO/GCS/AzureStructured queriesPostgreSQL/MariaDBEmbedded SQLSQLiteDevelopment/testingBadgerDB (in-memory) or SQLiteVector search with existing PostgresDatabaseT with pgvectorSelf-hosted vector searchQdrant or MilvusManaged vector servicePineconeSemantic search with GraphQLWeaviate",{"id":532,"title":533,"titles":534,"content":535,"level":35},"/v1.0.18/guides/providers#feature-comparison-key-value","Feature Comparison (Key-Value)",[401,524],"FeatureRedisBadgerBoltTTL✓✓✗Distributed✓✗✗Embedded✗✓✓Encryption✗✓✗Transactions✓✓✓",{"id":537,"title":538,"titles":539,"content":540,"level":35},"/v1.0.18/guides/providers#feature-comparison-blob","Feature Comparison (Blob)",[401,524],"FeatureS3MinIOGCSAzureVersioning✓✓✓✓Lifecycle✓✓✓✓CDNCloudFront✗Cloud CDNAzure CDNPresigned URLs✓✓✓SAS tokensSelf-hosted✗✓✗✗",{"id":542,"title":543,"titles":544,"content":545,"level":35},"/v1.0.18/guides/providers#feature-comparison-vector","Feature Comparison (Vector)",[401,524],"FeatureQdrantPineconeMilvusWeaviateSelf-hosted✓✗✓✓Managed service✓✓✓✓Distance metricsMultipleFixedMultipleMultipleFull filter support✓✗✓✓",{"id":547,"title":548,"titles":549,"content":550,"level":35},"/v1.0.18/guides/providers#filter-operator-support-vector","Filter Operator Support (Vector)",[401,524],"OperatorQdrantPineconeMilvusWeaviateEq✓✓✓✓Ne✓✓✓✓Gt/Gte/Lt/Lte✓✗✓✓In✓✓✓✓Nin✓✓✓✓Like✓✗✓✓Contains✓✗✓✓And/Or/Not✓✓✓✓ Note: Pinecone returns ErrOperatorNotSupported for unsupported operators.",{"id":552,"title":553,"titles":554,"content":555,"level":19},"/v1.0.18/guides/providers#switching-providers","Switching Providers",[401],"One of grub's benefits is easy provider switching: // Development\nstore := grub.NewStore[Session](badger.New(devDB))\n\n// Production\nstore := grub.NewStore[Session](redis.New(prodClient)) Business logic remains unchanged. Only the provider initialization differs. Caveats: TTL behavior differs (BoltDB doesn't support it)Batch atomicity varies by providerSome providers have unique features not exposed through grub html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}",{"id":557,"title":558,"titles":559,"content":560,"level":9},"/v1.0.18/guides/lifecycle","Lifecycle Operations",[],"CRUD operations, batch processing, and listing",{"id":562,"title":558,"titles":563,"content":564,"level":9},"/v1.0.18/guides/lifecycle#lifecycle-operations",[],"This guide covers the full lifecycle of data: create, read, update, delete, and list operations.",{"id":566,"title":567,"titles":568,"content":29,"level":19},"/v1.0.18/guides/lifecycle#store-operations-key-value","Store Operations (Key-Value)",[558],{"id":570,"title":571,"titles":572,"content":573,"level":35},"/v1.0.18/guides/lifecycle#get","Get",[558,567],"Retrieves a value by key. session, err := store.Get(ctx, \"session:abc123\")\nif errors.Is(err, grub.ErrNotFound) {\n    // Key doesn't exist\n} Returns: (*T, error) ErrNotFound if key doesn't exist",{"id":575,"title":576,"titles":577,"content":578,"level":35},"/v1.0.18/guides/lifecycle#set","Set",[558,567],"Stores a value with optional TTL. // With TTL (expires in 1 hour)\nerr := store.Set(ctx, \"session:abc123\", &session, time.Hour)\n\n// Without TTL (never expires)\nerr := store.Set(ctx, \"config:app\", &config, 0) TTL behavior: ttl > 0: Key expires after durationttl == 0: No expirationBoltDB: Returns ErrTTLNotSupported if ttl > 0",{"id":580,"title":581,"titles":582,"content":583,"level":35},"/v1.0.18/guides/lifecycle#delete","Delete",[558,567],"Removes a key. err := store.Delete(ctx, \"session:abc123\")\nif errors.Is(err, grub.ErrNotFound) {\n    // Key didn't exist\n} Returns: ErrNotFound if key doesn't exist",{"id":585,"title":586,"titles":587,"content":588,"level":35},"/v1.0.18/guides/lifecycle#exists","Exists",[558,567],"Checks if a key exists without loading the value. exists, err := store.Exists(ctx, \"session:abc123\")\nif exists {\n    // Key exists\n} Returns: (bool, error) — never returns ErrNotFound",{"id":590,"title":591,"titles":592,"content":593,"level":35},"/v1.0.18/guides/lifecycle#list","List",[558,567],"Lists keys matching a prefix. // List up to 100 keys with prefix \"session:\"\nkeys, err := store.List(ctx, \"session:\", 100)\n\n// List all keys (no limit)\nkeys, err := store.List(ctx, \"\", 0) Parameters: prefix: Filter keys starting with this string (empty = all)limit: Maximum keys to return (0 = no limit)",{"id":595,"title":596,"titles":597,"content":598,"level":35},"/v1.0.18/guides/lifecycle#getbatch","GetBatch",[558,567],"Retrieves multiple keys at once. keys := []string{\"user:1\", \"user:2\", \"user:3\"}\nresults, err := store.GetBatch(ctx, keys)\n\nfor key, user := range results {\n    fmt.Println(key, user.Name)\n} Behavior: Missing keys are omitted from the result map (no error).",{"id":600,"title":601,"titles":602,"content":603,"level":35},"/v1.0.18/guides/lifecycle#setbatch","SetBatch",[558,567],"Stores multiple values at once. items := map[string]*User{\n    \"user:1\": {Name: \"Alice\"},\n    \"user:2\": {Name: \"Bob\"},\n}\nerr := store.SetBatch(ctx, items, time.Hour) Atomicity varies by provider: Redis: Pipelined (each operation independent)Badger: WriteBatch (atomic)Bolt: Single transaction (atomic)",{"id":605,"title":606,"titles":607,"content":29,"level":19},"/v1.0.18/guides/lifecycle#bucket-operations-blob","Bucket Operations (Blob)",[558],{"id":609,"title":571,"titles":610,"content":611,"level":35},"/v1.0.18/guides/lifecycle#get-1",[558,606],"Retrieves an object with metadata. obj, err := bucket.Get(ctx, \"docs/report.json\")\nif errors.Is(err, grub.ErrNotFound) {\n    // Object doesn't exist\n}\n\n// Access payload\nfmt.Println(obj.Data.Title)\n\n// Access metadata\nfmt.Println(obj.ContentType)\nfmt.Println(obj.Size)\nfmt.Println(obj.Metadata[\"author\"])",{"id":613,"title":614,"titles":615,"content":616,"level":35},"/v1.0.18/guides/lifecycle#put","Put",[558,606],"Stores an object with metadata. err := bucket.Put(ctx, &grub.Object[Document]{\n    Key:         \"docs/report.json\",\n    ContentType: \"application/json\",\n    Metadata:    map[string]string{\"author\": \"alice\", \"version\": \"1.0\"},\n    Data:        Document{Title: \"Q4 Report\", Content: \"...\"},\n}) Note: Size is computed from encoded data, not set manually.",{"id":618,"title":581,"titles":619,"content":620,"level":35},"/v1.0.18/guides/lifecycle#delete-1",[558,606],"Removes an object. err := bucket.Delete(ctx, \"docs/report.json\")\nif errors.Is(err, grub.ErrNotFound) {\n    // Object didn't exist\n}",{"id":622,"title":586,"titles":623,"content":624,"level":35},"/v1.0.18/guides/lifecycle#exists-1",[558,606],"Checks if an object exists. exists, err := bucket.Exists(ctx, \"docs/report.json\")",{"id":626,"title":591,"titles":627,"content":628,"level":35},"/v1.0.18/guides/lifecycle#list-1",[558,606],"Lists objects matching a prefix (metadata only). infos, err := bucket.List(ctx, \"docs/\", 100)\n\nfor _, info := range infos {\n    fmt.Printf(\"%s (%d bytes)\\n\", info.Key, info.Size)\n} Returns: []ObjectInfo — metadata without payload",{"id":630,"title":631,"titles":632,"content":29,"level":19},"/v1.0.18/guides/lifecycle#database-operations-sql","Database Operations (SQL)",[558],{"id":634,"title":571,"titles":635,"content":636,"level":35},"/v1.0.18/guides/lifecycle#get-2",[558,631],"Retrieves a record by primary key. user, err := db.Get(ctx, \"123\")\nif errors.Is(err, grub.ErrNotFound) {\n    // Record doesn't exist\n}",{"id":638,"title":576,"titles":639,"content":640,"level":35},"/v1.0.18/guides/lifecycle#set-1",[558,631],"Upserts a record (insert or update on conflict). // Insert new record\nerr := db.Set(ctx, \"123\", &User{ID: \"123\", Name: \"Alice\"})\n\n// Update existing record (same key)\nerr := db.Set(ctx, \"123\", &User{ID: \"123\", Name: \"Alice Smith\"}) Behavior: Always upserts. Uses INSERT ... ON CONFLICT DO UPDATE.",{"id":642,"title":581,"titles":643,"content":644,"level":35},"/v1.0.18/guides/lifecycle#delete-2",[558,631],"Removes a record by primary key. err := db.Delete(ctx, \"123\")\nif errors.Is(err, grub.ErrNotFound) {\n    // Record didn't exist\n}",{"id":646,"title":586,"titles":647,"content":648,"level":35},"/v1.0.18/guides/lifecycle#exists-2",[558,631],"Checks if a record exists. exists, err := db.Exists(ctx, \"123\")",{"id":650,"title":651,"titles":652,"content":653,"level":35},"/v1.0.18/guides/lifecycle#query-builder","Query Builder",[558,631],"Returns a query builder for fetching multiple records. // Using the query builder\nusers, err := db.Query().\n    Where(\"status\", \"=\", \"active\").\n    Exec(ctx, map[string]any{\"active\": \"enabled\"})\n\n// With parameters\nusers, err := db.Query().\n    Where(\"role\", \"=\", \"role\").\n    Exec(ctx, map[string]any{\"role\": \"admin\"})",{"id":655,"title":656,"titles":657,"content":658,"level":35},"/v1.0.18/guides/lifecycle#select-builder","Select Builder",[558,631],"Returns a select builder for fetching a single record. user, err := db.Select().\n    Where(\"email\", \"=\", \"email\").\n    Exec(ctx, map[string]any{\"email\": \"alice@example.com\"})",{"id":660,"title":661,"titles":662,"content":663,"level":35},"/v1.0.18/guides/lifecycle#modify-builder","Modify Builder",[558,631],"Returns an update builder for modifying records. user, err := db.Modify().\n    Set(\"status\", \"new_status\").\n    Where(\"id\", \"=\", \"user_id\").\n    Exec(ctx, map[string]any{\"new_status\": \"inactive\", \"user_id\": \"123\"})",{"id":665,"title":666,"titles":667,"content":668,"level":35},"/v1.0.18/guides/lifecycle#statement-execution","Statement Execution",[558,631],"Execute pre-defined edamame statements. // Query all records\nusers, err := db.ExecQuery(ctx, grub.QueryAll, nil)\n\n// Count records\ncount, err := db.ExecAggregate(ctx, grub.CountAll, nil)",{"id":670,"title":234,"titles":671,"content":672,"level":19},"/v1.0.18/guides/lifecycle#lifecycle-hooks",[558],"Types can opt into lifecycle hooks that fire automatically during CRUD operations. Implement one or more hook interfaces on your type to add validation, normalization, or side-effects.",{"id":674,"title":239,"titles":675,"content":676,"level":35},"/v1.0.18/guides/lifecycle#hook-interfaces",[558,234],"type BeforeSave interface {\n    BeforeSave(ctx context.Context) error\n}\n\ntype AfterSave interface {\n    AfterSave(ctx context.Context) error\n}\n\ntype AfterLoad interface {\n    AfterLoad(ctx context.Context) error\n}\n\ntype BeforeDelete interface {\n    BeforeDelete(ctx context.Context) error\n}\n\ntype AfterDelete interface {\n    AfterDelete(ctx context.Context) error\n}",{"id":678,"title":679,"titles":680,"content":681,"level":35},"/v1.0.18/guides/lifecycle#example-validation-and-normalization","Example: Validation and Normalization",[558,234],"type User struct {\n    ID    string `db:\"id\" constraints:\"primarykey\"`\n    Name  string `db:\"name\"`\n    Email string `db:\"email\"`\n}\n\n// Validate before any write\nfunc (u *User) BeforeSave(ctx context.Context) error {\n    if u.Email == \"\" {\n        return errors.New(\"email is required\")\n    }\n    return nil\n}\n\n// Normalize after any read\nfunc (u *User) AfterLoad(ctx context.Context) error {\n    u.Email = strings.ToLower(u.Email)\n    return nil\n} These hooks fire automatically on all typed operations: // BeforeSave fires before the write — returns error if email is empty\nerr := store.Set(ctx, \"user:1\", &User{Name: \"Alice\"}, 0)\n// err: \"email is required\"\n\n// AfterLoad fires after decode — email is normalized\nuser, err := store.Get(ctx, \"user:1\")\n// user.Email is lowercase",{"id":683,"title":684,"titles":685,"content":686,"level":35},"/v1.0.18/guides/lifecycle#hook-firing-points","Hook Firing Points",[558,234],"Storage TypeSave HooksLoad HooksDelete HooksStoreTSet, SetBatchGet, GetBatchDeleteBucketTPutGetDeleteDatabaseTSet, SetTx, Insert, InsertFull (all builder paths)Get, GetTx, Query, Select, Modify, ExecQuery, ExecSelect, ExecUpdate (and Tx variants)Delete, DeleteTxIndexTUpsert, UpsertBatchGet, Search, Query, FilterDelete, DeleteBatch",{"id":688,"title":689,"titles":690,"content":691,"level":35},"/v1.0.18/guides/lifecycle#batch-behavior","Batch Behavior",[558,234],"For batch operations, hooks fire per-item: SetBatch: BeforeSave runs on each item before encoding. If any fails, the entire batch is aborted. AfterSave runs on each item after the batch succeeds.GetBatch: AfterLoad runs on each decoded item.UpsertBatch: BeforeSave runs on each metadata item before encoding.",{"id":693,"title":249,"titles":694,"content":695,"level":35},"/v1.0.18/guides/lifecycle#delete-hooks",[558,234],"Delete hooks are invoked on a zero-value T because delete operations only receive a key/ID. They act as static guards or side-effects: func (u *User) BeforeDelete(ctx context.Context) error {\n    // No instance state available — use for static guards\n    return nil\n}",{"id":697,"title":698,"titles":699,"content":700,"level":35},"/v1.0.18/guides/lifecycle#what-hooks-dont-cover","What Hooks Don't Cover",[558,234],"Atomic views do not trigger hooks (they operate below the type-aware layer)ExecAggregate does not trigger hooks (returns float64, not *T)List/Exists operations do not trigger hooks (no T instance involved)Remove (delete builder) does not trigger hooks (returns int64, not *T)",{"id":702,"title":703,"titles":704,"content":29,"level":19},"/v1.0.18/guides/lifecycle#common-patterns","Common Patterns",[558],{"id":706,"title":707,"titles":708,"content":709,"level":35},"/v1.0.18/guides/lifecycle#check-then-act","Check-Then-Act",[558,703],"exists, _ := store.Exists(ctx, key)\nif !exists {\n    // Create default\n    store.Set(ctx, key, defaultValue, 0)\n} Warning: Not atomic. For atomic operations, use provider-specific features.",{"id":711,"title":712,"titles":713,"content":714,"level":35},"/v1.0.18/guides/lifecycle#get-or-create","Get-Or-Create",[558,703],"func GetOrCreate[T any](ctx context.Context, store *grub.Store[T], key string, create func() *T) (*T, error) {\n    val, err := store.Get(ctx, key)\n    if err == nil {\n        return val, nil\n    }\n    if !errors.Is(err, grub.ErrNotFound) {\n        return nil, err\n    }\n\n    val = create()\n    if err := store.Set(ctx, key, val, 0); err != nil {\n        return nil, err\n    }\n    return val, nil\n}",{"id":716,"title":717,"titles":718,"content":719,"level":35},"/v1.0.18/guides/lifecycle#batch-processing","Batch Processing",[558,703],"// Process in batches to avoid memory issues\nconst batchSize = 100\n\nkeys, _ := store.List(ctx, \"user:\", 0)\n\nfor i := 0; i \u003C len(keys); i += batchSize {\n    end := min(i+batchSize, len(keys))\n    batch := keys[i:end]\n\n    results, _ := store.GetBatch(ctx, batch)\n    for key, user := range results {\n        // Process user\n    }\n}",{"id":721,"title":722,"titles":723,"content":724,"level":35},"/v1.0.18/guides/lifecycle#conditional-delete","Conditional Delete",[558,703],"// Delete only if value matches condition\nval, err := store.Get(ctx, key)\nif err != nil {\n    return err\n}\nif val.Status == \"expired\" {\n    return store.Delete(ctx, key)\n}",{"id":726,"title":727,"titles":728,"content":29,"level":19},"/v1.0.18/guides/lifecycle#error-handling","Error Handling",[558],{"id":730,"title":731,"titles":732,"content":733,"level":35},"/v1.0.18/guides/lifecycle#standard-error-checks","Standard Error Checks",[558,727],"val, err := store.Get(ctx, key)\nswitch {\ncase err == nil:\n    // Success\ncase errors.Is(err, grub.ErrNotFound):\n    // Key doesn't exist\ncase errors.Is(err, context.DeadlineExceeded):\n    // Timeout\ncase errors.Is(err, context.Canceled):\n    // Canceled\ndefault:\n    // Provider error (network, etc.)\n}",{"id":735,"title":736,"titles":737,"content":738,"level":35},"/v1.0.18/guides/lifecycle#wrapping-errors","Wrapping Errors",[558,727],"val, err := store.Get(ctx, key)\nif err != nil {\n    return fmt.Errorf(\"loading config %s: %w\", key, err)\n} The wrapped error preserves errors.Is behavior: if errors.Is(err, grub.ErrNotFound) {\n    // Still works\n} html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html pre.shiki code .suWN2, html code.shiki .suWN2{--shiki-default:var(--shiki-tag)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sfm-E, html code.shiki .sfm-E{--shiki-default:var(--shiki-variable)}html pre.shiki code .skxcq, html code.shiki .skxcq{--shiki-default:var(--shiki-builtin)}",{"id":740,"title":741,"titles":742,"content":743,"level":9},"/v1.0.18/guides/pagination","Pagination",[],"Listing and paginating large datasets",{"id":745,"title":741,"titles":746,"content":747,"level":9},"/v1.0.18/guides/pagination#pagination",[],"This guide covers patterns for listing and paginating data across different storage modes.",{"id":749,"title":750,"titles":751,"content":29,"level":19},"/v1.0.18/guides/pagination#store-list-pagination","Store List Pagination",[741],{"id":753,"title":754,"titles":755,"content":756,"level":35},"/v1.0.18/guides/pagination#basic-listing","Basic Listing",[741,750],"// List first 100 keys with prefix\nkeys, err := store.List(ctx, \"user:\", 100)",{"id":758,"title":759,"titles":760,"content":761,"level":35},"/v1.0.18/guides/pagination#manual-pagination","Manual Pagination",[741,750],"Grub's List doesn't provide cursors. For pagination, track the last key: func ListAll(ctx context.Context, store *grub.Store[User], prefix string) ([]string, error) {\n    const pageSize = 100\n    var allKeys []string\n\n    for {\n        keys, err := store.List(ctx, prefix, pageSize)\n        if err != nil {\n            return nil, err\n        }\n\n        allKeys = append(allKeys, keys...)\n\n        if len(keys) \u003C pageSize {\n            break // No more results\n        }\n\n        // Use last key as next prefix (lexicographic ordering)\n        prefix = keys[len(keys)-1] + \"\\x00\"\n    }\n\n    return allKeys, nil\n} Note: This assumes lexicographic key ordering, which varies by provider.",{"id":763,"title":340,"titles":764,"content":765,"level":35},"/v1.0.18/guides/pagination#provider-specific-behavior",[741,750],"ProviderList ImplementationRedisSCAN (cursor-based, safe for production)BadgerIterator (snapshot isolation)BoltCursor (within transaction) Redis SCAN: Grub uses SCAN internally, which is safe for large datasets. The limit parameter maps to COUNT hint.",{"id":767,"title":768,"titles":769,"content":770,"level":35},"/v1.0.18/guides/pagination#batch-loading","Batch Loading",[741,750],"After listing keys, load values in batches: keys, _ := store.List(ctx, \"user:\", 1000)\n\nconst batchSize = 100\nfor i := 0; i \u003C len(keys); i += batchSize {\n    end := min(i+batchSize, len(keys))\n    batch := keys[i:end]\n\n    users, err := store.GetBatch(ctx, batch)\n    if err != nil {\n        return err\n    }\n\n    for _, user := range users {\n        process(user)\n    }\n}",{"id":772,"title":773,"titles":774,"content":29,"level":19},"/v1.0.18/guides/pagination#bucket-list-pagination","Bucket List Pagination",[741],{"id":776,"title":754,"titles":777,"content":778,"level":35},"/v1.0.18/guides/pagination#basic-listing-1",[741,773],"// List first 100 objects with prefix\ninfos, err := bucket.List(ctx, \"docs/\", 100)\n\nfor _, info := range infos {\n    fmt.Printf(\"%s (%d bytes)\\n\", info.Key, info.Size)\n}",{"id":780,"title":781,"titles":782,"content":783,"level":35},"/v1.0.18/guides/pagination#continuation-pattern","Continuation Pattern",[741,773],"Similar to stores, but using key-based continuation: func ListAllObjects(ctx context.Context, bucket *grub.Bucket[Doc], prefix string) ([]grub.ObjectInfo, error) {\n    const pageSize = 100\n    var allInfos []grub.ObjectInfo\n    marker := prefix\n\n    for {\n        infos, err := bucket.List(ctx, marker, pageSize)\n        if err != nil {\n            return nil, err\n        }\n\n        allInfos = append(allInfos, infos...)\n\n        if len(infos) \u003C pageSize {\n            break\n        }\n\n        // Use last key as marker for next page\n        marker = infos[len(infos)-1].Key\n    }\n\n    return allInfos, nil\n}",{"id":785,"title":340,"titles":786,"content":787,"level":35},"/v1.0.18/guides/pagination#provider-specific-behavior-1",[741,773],"ProviderList ImplementationS3ListObjectsV2 with continuation tokensGCSstorage.IteratorAzureNewListBlobsFlatPager",{"id":789,"title":790,"titles":791,"content":792,"level":19},"/v1.0.18/guides/pagination#database-query-pagination","Database Query Pagination",[741],"For SQL databases, use query builders or pre-defined statements.",{"id":794,"title":795,"titles":796,"content":797,"level":35},"/v1.0.18/guides/pagination#using-query-builder","Using Query Builder",[741,790],"// Offset-based pagination with builder\nusers, err := db.Query().\n    OrderBy(\"created_at\", \"DESC\").\n    Limit(20).\n    Offset(40). // Page 3\n    Exec(ctx, nil)",{"id":799,"title":800,"titles":801,"content":802,"level":35},"/v1.0.18/guides/pagination#using-pre-defined-statements","Using Pre-defined Statements",[741,790],"// Define statement with LIMIT/OFFSET params\npaginatedStmt := edamame.NewQueryStatement(\"paginated\", \"Paginated users\", edamame.QuerySpec{\n    OrderBy:     []edamame.OrderBySpec{{Field: \"created_at\", Direction: \"desc\"}},\n    LimitParam:  \"limit\",\n    OffsetParam: \"offset\",\n})\n\nusers, err := db.ExecQuery(ctx, paginatedStmt, map[string]any{\n    \"limit\":  20,\n    \"offset\": 40, // Page 3\n})",{"id":804,"title":805,"titles":806,"content":807,"level":35},"/v1.0.18/guides/pagination#cursor-based-pagination","Cursor-Based Pagination",[741,790],"More efficient for large datasets: // Using builder\nusers, err := db.Query().\n    Where(\"id\", \">\", \"cursor\").\n    OrderBy(\"id\", \"ASC\").\n    Limit(20).\n    Exec(ctx, map[string]any{\"cursor\": lastID})",{"id":809,"title":810,"titles":811,"content":29,"level":19},"/v1.0.18/guides/pagination#performance-considerations","Performance Considerations",[741],{"id":813,"title":814,"titles":815,"content":816,"level":35},"/v1.0.18/guides/pagination#key-design-for-efficient-listing","Key Design for Efficient Listing",[741,810],"Design keys to enable efficient prefix queries: // Good: hierarchical, scannable\n\"tenant:acme:user:123\"\n\"tenant:acme:user:456\"\n\"tenant:beta:user:789\"\n\n// List all users for tenant\nkeys, _ := store.List(ctx, \"tenant:acme:user:\", 100) // Bad: UUIDs as first segment\n\"a1b2c3d4:user:data\"\n\"e5f6g7h8:user:data\"\n\n// Can't efficiently list by type",{"id":818,"title":819,"titles":820,"content":821,"level":35},"/v1.0.18/guides/pagination#limiting-result-sets","Limiting Result Sets",[741,810],"Always use reasonable limits: // Good: bounded\nkeys, _ := store.List(ctx, prefix, 1000)\n\n// Dangerous: unbounded on large datasets\nkeys, _ := store.List(ctx, prefix, 0)",{"id":823,"title":824,"titles":825,"content":826,"level":35},"/v1.0.18/guides/pagination#memory-considerations","Memory Considerations",[741,810],"For large datasets, stream results: const pageSize = 100\nprefix := \"user:\"\n\nfor {\n    keys, err := store.List(ctx, prefix, pageSize)\n    if err != nil {\n        return err\n    }\n\n    for _, key := range keys {\n        user, err := store.Get(ctx, key)\n        if err != nil {\n            continue\n        }\n        // Process immediately, don't accumulate\n        process(user)\n    }\n\n    if len(keys) \u003C pageSize {\n        break\n    }\n    prefix = keys[len(keys)-1] + \"\\x00\"\n}",{"id":828,"title":829,"titles":830,"content":29,"level":19},"/v1.0.18/guides/pagination#patterns-by-use-case","Patterns by Use Case",[741],{"id":832,"title":833,"titles":834,"content":835,"level":35},"/v1.0.18/guides/pagination#export-all-data","Export All Data",[741,829],"func Export[T any](ctx context.Context, store *grub.Store[T], prefix string, emit func(*T)) error {\n    const pageSize = 100\n    marker := prefix\n\n    for {\n        keys, err := store.List(ctx, marker, pageSize)\n        if err != nil {\n            return err\n        }\n\n        if len(keys) == 0 {\n            break\n        }\n\n        values, err := store.GetBatch(ctx, keys)\n        if err != nil {\n            return err\n        }\n\n        for _, v := range values {\n            emit(v)\n        }\n\n        marker = keys[len(keys)-1] + \"\\x00\"\n    }\n\n    return nil\n}",{"id":837,"title":838,"titles":839,"content":840,"level":35},"/v1.0.18/guides/pagination#count-by-prefix","Count by Prefix",[741,829],"func Count[T any](ctx context.Context, store *grub.Store[T], prefix string) (int, error) {\n    const pageSize = 1000\n    count := 0\n    marker := prefix\n\n    for {\n        keys, err := store.List(ctx, marker, pageSize)\n        if err != nil {\n            return 0, err\n        }\n\n        count += len(keys)\n\n        if len(keys) \u003C pageSize {\n            break\n        }\n\n        marker = keys[len(keys)-1] + \"\\x00\"\n    }\n\n    return count, nil\n}",{"id":842,"title":843,"titles":844,"content":845,"level":35},"/v1.0.18/guides/pagination#delete-by-prefix","Delete by Prefix",[741,829],"func DeletePrefix[T any](ctx context.Context, store *grub.Store[T], prefix string) error {\n    const pageSize = 100\n\n    for {\n        keys, err := store.List(ctx, prefix, pageSize)\n        if err != nil {\n            return err\n        }\n\n        if len(keys) == 0 {\n            break\n        }\n\n        for _, key := range keys {\n            if err := store.Delete(ctx, key); err != nil && !errors.Is(err, grub.ErrNotFound) {\n                return err\n            }\n        }\n    }\n\n    return nil\n}",{"id":847,"title":848,"titles":849,"content":850,"level":19},"/v1.0.18/guides/pagination#context-cancellation","Context Cancellation",[741],"List operations respect context cancellation in most providers: ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\ndefer cancel()\n\nkeys, err := store.List(ctx, prefix, 10000)\nif errors.Is(err, context.DeadlineExceeded) {\n    // Timed out during iteration\n} ProviderCancellation SupportRedisPer-operationBadgerDuring iterationBoltDuring iterationS3Per-operationGCSPer-operationAzurePer-operation html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sfm-E, html code.shiki .sfm-E{--shiki-default:var(--shiki-variable)}html pre.shiki code .skxcq, html code.shiki .skxcq{--shiki-default:var(--shiki-builtin)}html pre.shiki code .suWN2, html code.shiki .suWN2{--shiki-default:var(--shiki-tag)}html pre.shiki code .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}",{"id":852,"title":853,"titles":854,"content":855,"level":9},"/v1.0.18/guides/testing","Testing",[],"Testing strategies for different backends",{"id":857,"title":853,"titles":858,"content":859,"level":9},"/v1.0.18/guides/testing#testing",[],"This guide covers testing strategies for applications using grub.",{"id":861,"title":862,"titles":863,"content":864,"level":19},"/v1.0.18/guides/testing#testing-approaches","Testing Approaches",[853],"ApproachSpeedFidelityUse CaseMock providerFastLowUnit testsEmbedded DBMediumMediumIntegration testsReal serviceSlowHighE2E tests",{"id":866,"title":867,"titles":868,"content":29,"level":19},"/v1.0.18/guides/testing#unit-testing-with-mocks","Unit Testing with Mocks",[853],{"id":870,"title":871,"titles":872,"content":873,"level":35},"/v1.0.18/guides/testing#simple-mock-provider","Simple Mock Provider",[853,867],"type MockStore struct {\n    data map[string][]byte\n    mu   sync.RWMutex\n}\n\nfunc NewMockStore() *MockStore {\n    return &MockStore{data: make(map[string][]byte)}\n}\n\nfunc (m *MockStore) Get(ctx context.Context, key string) ([]byte, error) {\n    m.mu.RLock()\n    defer m.mu.RUnlock()\n    if v, ok := m.data[key]; ok {\n        return v, nil\n    }\n    return nil, grub.ErrNotFound\n}\n\nfunc (m *MockStore) Set(ctx context.Context, key string, value []byte, ttl time.Duration) error {\n    m.mu.Lock()\n    defer m.mu.Unlock()\n    m.data[key] = value\n    return nil\n}\n\nfunc (m *MockStore) Delete(ctx context.Context, key string) error {\n    m.mu.Lock()\n    defer m.mu.Unlock()\n    if _, ok := m.data[key]; !ok {\n        return grub.ErrNotFound\n    }\n    delete(m.data, key)\n    return nil\n}\n\nfunc (m *MockStore) Exists(ctx context.Context, key string) (bool, error) {\n    m.mu.RLock()\n    defer m.mu.RUnlock()\n    _, ok := m.data[key]\n    return ok, nil\n}\n\nfunc (m *MockStore) List(ctx context.Context, prefix string, limit int) ([]string, error) {\n    m.mu.RLock()\n    defer m.mu.RUnlock()\n    var keys []string\n    for k := range m.data {\n        if strings.HasPrefix(k, prefix) {\n            keys = append(keys, k)\n            if limit > 0 && len(keys) >= limit {\n                break\n            }\n        }\n    }\n    return keys, nil\n}\n\nfunc (m *MockStore) GetBatch(ctx context.Context, keys []string) (map[string][]byte, error) {\n    m.mu.RLock()\n    defer m.mu.RUnlock()\n    result := make(map[string][]byte)\n    for _, k := range keys {\n        if v, ok := m.data[k]; ok {\n            result[k] = v\n        }\n    }\n    return result, nil\n}\n\nfunc (m *MockStore) SetBatch(ctx context.Context, items map[string][]byte, ttl time.Duration) error {\n    m.mu.Lock()\n    defer m.mu.Unlock()\n    for k, v := range items {\n        m.data[k] = v\n    }\n    return nil\n}",{"id":875,"title":876,"titles":877,"content":878,"level":35},"/v1.0.18/guides/testing#using-the-mock","Using the Mock",[853,867],"func TestUserService_CreateUser(t *testing.T) {\n    store := grub.NewStore[User](NewMockStore())\n    service := NewUserService(store)\n\n    user, err := service.CreateUser(context.Background(), \"alice@example.com\")\n\n    if err != nil {\n        t.Fatalf(\"unexpected error: %v\", err)\n    }\n    if user.Email != \"alice@example.com\" {\n        t.Errorf(\"got email %q, want %q\", user.Email, \"alice@example.com\")\n    }\n}",{"id":880,"title":881,"titles":882,"content":29,"level":19},"/v1.0.18/guides/testing#integration-testing-with-embedded-dbs","Integration Testing with Embedded DBs",[853],{"id":884,"title":885,"titles":886,"content":887,"level":35},"/v1.0.18/guides/testing#badgerdb-in-memory","BadgerDB (In-Memory)",[853,881],"func setupTestStore(t *testing.T) *grub.Store[TestData] {\n    opts := badgerdb.DefaultOptions(\"\").WithInMemory(true)\n    db, err := badgerdb.Open(opts)\n    if err != nil {\n        t.Fatal(err)\n    }\n    t.Cleanup(func() { db.Close() })\n\n    return grub.NewStore[TestData](badger.New(db))\n}\n\nfunc TestStore_SetGet(t *testing.T) {\n    store := setupTestStore(t)\n    ctx := context.Background()\n\n    data := &TestData{Value: \"test\"}\n    if err := store.Set(ctx, \"key\", data, 0); err != nil {\n        t.Fatal(err)\n    }\n\n    got, err := store.Get(ctx, \"key\")\n    if err != nil {\n        t.Fatal(err)\n    }\n    if got.Value != \"test\" {\n        t.Errorf(\"got %q, want %q\", got.Value, \"test\")\n    }\n}",{"id":889,"title":890,"titles":891,"content":892,"level":35},"/v1.0.18/guides/testing#boltdb-temp-file","BoltDB (Temp File)",[853,881],"func setupBoltStore(t *testing.T) *grub.Store[TestData] {\n    f, err := os.CreateTemp(\"\", \"bolt-test-*.db\")\n    if err != nil {\n        t.Fatal(err)\n    }\n    f.Close()\n\n    db, err := bbolt.Open(f.Name(), 0600, nil)\n    if err != nil {\n        t.Fatal(err)\n    }\n\n    t.Cleanup(func() {\n        db.Close()\n        os.Remove(f.Name())\n    })\n\n    return grub.NewStore[TestData](bolt.New(db, \"test\"))\n}",{"id":894,"title":895,"titles":896,"content":897,"level":35},"/v1.0.18/guides/testing#sqlite-in-memory","SQLite (In-Memory)",[853,881],"func setupTestDB(t *testing.T) *grub.Database[User] {\n    db, err := sqlx.Connect(\"sqlite\", \":memory:\")\n    if err != nil {\n        t.Fatal(err)\n    }\n    t.Cleanup(func() { db.Close() })\n\n    // Create table\n    db.MustExec(`CREATE TABLE users (\n        id TEXT PRIMARY KEY,\n        name TEXT,\n        email TEXT\n    )`)\n\n    userDB := grub.NewDatabase[User](db, \"users\", sqlite.New())\n\n    return userDB\n}",{"id":899,"title":900,"titles":901,"content":902,"level":19},"/v1.0.18/guides/testing#using-grubtesting-helpers","Using grub/testing Helpers",[853],"The grub/testing package provides assertion helpers: import grubtesting \"github.com/zoobz-io/grub/testing\"\n\nfunc TestStore_Operations(t *testing.T) {\n    store := setupTestStore(t)\n    ctx := grubtesting.WithTimeout(t, 5*time.Second)\n\n    // Set\n    err := store.Set(ctx, \"key\", &TestData{Value: \"test\"}, 0)\n    grubtesting.AssertNoError(t, err)\n\n    // Get\n    got, err := store.Get(ctx, \"key\")\n    grubtesting.AssertNoError(t, err)\n    grubtesting.AssertEqual(t, got.Value, \"test\")\n\n    // Exists\n    exists, err := store.Exists(ctx, \"key\")\n    grubtesting.AssertNoError(t, err)\n    grubtesting.AssertTrue(t, exists, \"key should exist\")\n\n    // Delete\n    err = store.Delete(ctx, \"key\")\n    grubtesting.AssertNoError(t, err)\n\n    // Verify deleted\n    _, err = store.Get(ctx, \"key\")\n    grubtesting.AssertError(t, err)\n}",{"id":904,"title":905,"titles":906,"content":907,"level":35},"/v1.0.18/guides/testing#available-helpers","Available Helpers",[853,900],"HelperDescriptionWithTimeout(t, d)Context with timeout and cleanupAssertNoError(t, err)Fails if err != nilAssertError(t, err)Fails if err == nilAssertEqual(t, got, want)Compares valuesAssertTrue(t, cond, msg)Asserts conditionAssertFalse(t, cond, msg)Asserts negationAssertNil(t, v)Asserts nilAssertNotNil(t, v)Asserts non-nilAssertLen(t, slice, n)Asserts slice lengthAssertContains(t, slice, item)Asserts membershipAssertMapHasKey(t, m, k)Asserts key exists",{"id":909,"title":910,"titles":911,"content":912,"level":19},"/v1.0.18/guides/testing#integration-tests-with-testcontainers","Integration Tests with Testcontainers",[853],"For testing against real services:",{"id":914,"title":437,"titles":915,"content":916,"level":35},"/v1.0.18/guides/testing#redis",[853,910],"func setupRedisStore(t *testing.T) *grub.Store[TestData] {\n    ctx := context.Background()\n\n    container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{\n        ContainerRequest: testcontainers.ContainerRequest{\n            Image:        \"redis:7\",\n            ExposedPorts: []string{\"6379/tcp\"},\n            WaitingFor:   wait.ForListeningPort(\"6379/tcp\"),\n        },\n        Started: true,\n    })\n    if err != nil {\n        t.Fatal(err)\n    }\n    t.Cleanup(func() { container.Terminate(ctx) })\n\n    host, _ := container.Host(ctx)\n    port, _ := container.MappedPort(ctx, \"6379\")\n\n    client := goredis.NewClient(&goredis.Options{\n        Addr: fmt.Sprintf(\"%s:%s\", host, port.Port()),\n    })\n    t.Cleanup(func() { client.Close() })\n\n    return grub.NewStore[TestData](redis.New(client))\n}",{"id":918,"title":480,"titles":919,"content":920,"level":35},"/v1.0.18/guides/testing#postgresql",[853,910],"func setupPostgresDB(t *testing.T) *grub.Database[User] {\n    ctx := context.Background()\n\n    container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{\n        ContainerRequest: testcontainers.ContainerRequest{\n            Image:        \"postgres:16\",\n            ExposedPorts: []string{\"5432/tcp\"},\n            Env: map[string]string{\n                \"POSTGRES_PASSWORD\": \"test\",\n                \"POSTGRES_DB\":       \"test\",\n            },\n            WaitingFor: wait.ForListeningPort(\"5432/tcp\"),\n        },\n        Started: true,\n    })\n    if err != nil {\n        t.Fatal(err)\n    }\n    t.Cleanup(func() { container.Terminate(ctx) })\n\n    host, _ := container.Host(ctx)\n    port, _ := container.MappedPort(ctx, \"5432\")\n\n    dsn := fmt.Sprintf(\"postgres://postgres:test@%s:%s/test?sslmode=disable\", host, port.Port())\n    db, err := sqlx.Connect(\"postgres\", dsn)\n    if err != nil {\n        t.Fatal(err)\n    }\n    t.Cleanup(func() { db.Close() })\n\n    // Create schema\n    db.MustExec(`CREATE TABLE users (id TEXT PRIMARY KEY, name TEXT, email TEXT)`)\n\n    userDB, _ := grub.NewDatabase[User](db, \"users\", postgres.New())\n    return userDB\n}",{"id":922,"title":923,"titles":924,"content":29,"level":19},"/v1.0.18/guides/testing#testing-best-practices","Testing Best Practices",[853],{"id":926,"title":927,"titles":928,"content":929,"level":35},"/v1.0.18/guides/testing#_1-use-table-driven-tests","1. Use Table-Driven Tests",[853,923],"func TestStore_Get(t *testing.T) {\n    store := setupTestStore(t)\n    ctx := context.Background()\n\n    // Setup\n    store.Set(ctx, \"exists\", &TestData{Value: \"value\"}, 0)\n\n    tests := []struct {\n        name    string\n        key     string\n        want    string\n        wantErr error\n    }{\n        {\"existing key\", \"exists\", \"value\", nil},\n        {\"missing key\", \"missing\", \"\", grub.ErrNotFound},\n    }\n\n    for _, tt := range tests {\n        t.Run(tt.name, func(t *testing.T) {\n            got, err := store.Get(ctx, tt.key)\n\n            if tt.wantErr != nil {\n                if !errors.Is(err, tt.wantErr) {\n                    t.Errorf(\"got error %v, want %v\", err, tt.wantErr)\n                }\n                return\n            }\n\n            if err != nil {\n                t.Fatalf(\"unexpected error: %v\", err)\n            }\n            if got.Value != tt.want {\n                t.Errorf(\"got %q, want %q\", got.Value, tt.want)\n            }\n        })\n    }\n}",{"id":931,"title":932,"titles":933,"content":934,"level":35},"/v1.0.18/guides/testing#_2-isolate-tests","2. Isolate Tests",[853,923],"Use unique prefixes to avoid test interference: func TestConcurrent(t *testing.T) {\n    store := setupTestStore(t)\n    prefix := fmt.Sprintf(\"test-%d:\", time.Now().UnixNano())\n\n    // Use prefix for all keys\n    key := prefix + \"mykey\"\n}",{"id":936,"title":937,"titles":938,"content":939,"level":35},"/v1.0.18/guides/testing#_3-clean-up-in-tcleanup","3. Clean Up in t.Cleanup",[853,923],"func setupStore(t *testing.T) *grub.Store[Data] {\n    db, _ := badgerdb.Open(opts)\n    t.Cleanup(func() { db.Close() })\n\n    return grub.NewStore[Data](badger.New(db))\n}",{"id":941,"title":942,"titles":943,"content":944,"level":35},"/v1.0.18/guides/testing#_4-skip-slow-tests","4. Skip Slow Tests",[853,923],"func TestIntegration_Redis(t *testing.T) {\n    if testing.Short() {\n        t.Skip(\"skipping integration test\")\n    }\n    // ... test with real Redis\n} Run fast tests only: go test -short ./... html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .skxcq, html code.shiki .skxcq{--shiki-default:var(--shiki-builtin)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}",{"id":946,"title":947,"titles":948,"content":949,"level":9},"/v1.0.18/guides/best-practices","Best Practices",[],"Key design, performance tuning, and operational guidelines",{"id":951,"title":947,"titles":952,"content":953,"level":9},"/v1.0.18/guides/best-practices#best-practices",[],"This guide covers recommended patterns for building applications with grub.",{"id":955,"title":956,"titles":957,"content":29,"level":19},"/v1.0.18/guides/best-practices#key-design","Key Design",[947],{"id":959,"title":960,"titles":961,"content":962,"level":35},"/v1.0.18/guides/best-practices#use-hierarchical-keys","Use Hierarchical Keys",[947,956],"Structure keys for efficient listing and logical grouping: // Good: hierarchical, scannable\n\"tenant:acme:user:123\"\n\"tenant:acme:session:abc\"\n\"tenant:beta:user:456\"\n\n// List all for tenant\nstore.List(ctx, \"tenant:acme:\", 100)\n\n// List all users for tenant\nstore.List(ctx, \"tenant:acme:user:\", 100)",{"id":964,"title":965,"titles":966,"content":967,"level":35},"/v1.0.18/guides/best-practices#avoid-dynamic-key-segments-first","Avoid Dynamic Key Segments First",[947,956],"// Bad: random prefix prevents efficient scanning\nkey := fmt.Sprintf(\"%s:user:data\", uuid.New())\n\n// Good: type prefix first\nkey := fmt.Sprintf(\"user:%s:data\", uuid.New())",{"id":969,"title":970,"titles":971,"content":972,"level":35},"/v1.0.18/guides/best-practices#keep-keys-short","Keep Keys Short",[947,956],"// Verbose\n\"application:users:profile:data:user_id_12345\"\n\n// Concise\n\"u:12345:profile\"",{"id":974,"title":975,"titles":976,"content":977,"level":35},"/v1.0.18/guides/best-practices#use-consistent-separators","Use Consistent Separators",[947,956],"// Pick one and stick with it\n\"user:123:session:abc\"  // Colons\n\"user/123/session/abc\"  // Slashes (good for blob storage)\n\"user.123.session.abc\"  // Dots",{"id":979,"title":980,"titles":981,"content":29,"level":19},"/v1.0.18/guides/best-practices#type-design","Type Design",[947],{"id":983,"title":984,"titles":985,"content":986,"level":35},"/v1.0.18/guides/best-practices#keep-types-focused","Keep Types Focused",[947,980],"// Good: single purpose\ntype Session struct {\n    UserID    string    `json:\"user_id\"`\n    Token     string    `json:\"token\"`\n    ExpiresAt time.Time `json:\"expires_at\"`\n}\n\n// Bad: kitchen sink\ntype UserEverything struct {\n    ID       string\n    Profile  Profile\n    Settings Settings\n    Sessions []Session\n    Orders   []Order\n    // ...\n}",{"id":988,"title":989,"titles":990,"content":991,"level":35},"/v1.0.18/guides/best-practices#use-pointers-sparingly","Use Pointers Sparingly",[947,980],"Grub returns pointers to avoid copying. Internal fields don't need to be pointers: // Good: value fields\ntype Config struct {\n    Debug   bool   `json:\"debug\"`\n    Version string `json:\"version\"`\n}\n\n// Unnecessary: pointer fields\ntype Config struct {\n    Debug   *bool   `json:\"debug\"`\n    Version *string `json:\"version\"`\n}",{"id":993,"title":994,"titles":995,"content":996,"level":35},"/v1.0.18/guides/best-practices#design-for-serialization","Design for Serialization",[947,980],"// Good: explicit JSON tags\ntype User struct {\n    ID        string    `json:\"id\"`\n    Email     string    `json:\"email\"`\n    CreatedAt time.Time `json:\"created_at\"`\n}\n\n// Problematic: unexported fields ignored\ntype User struct {\n    id    string // Won't serialize\n    Email string\n}",{"id":998,"title":727,"titles":999,"content":29,"level":19},"/v1.0.18/guides/best-practices#error-handling",[947],{"id":1001,"title":1002,"titles":1003,"content":1004,"level":35},"/v1.0.18/guides/best-practices#always-check-errors","Always Check Errors",[947,727],"// Good\nuser, err := store.Get(ctx, key)\nif err != nil {\n    return fmt.Errorf(\"loading user %s: %w\", key, err)\n}\n\n// Bad: ignoring errors\nuser, _ := store.Get(ctx, key)",{"id":1006,"title":1007,"titles":1008,"content":1009,"level":35},"/v1.0.18/guides/best-practices#use-errorsis-for-semantic-errors","Use errors.Is for Semantic Errors",[947,727],"// Good\nif errors.Is(err, grub.ErrNotFound) {\n    return createDefault()\n}\n\n// Bad: string matching\nif err.Error() == \"grub: record not found\" {\n    return createDefault()\n}",{"id":1011,"title":1012,"titles":1013,"content":1014,"level":35},"/v1.0.18/guides/best-practices#wrap-errors-with-context","Wrap Errors with Context",[947,727],"user, err := store.Get(ctx, key)\nif err != nil {\n    return fmt.Errorf(\"user %s: %w\", key, err)\n}",{"id":1016,"title":1017,"titles":1018,"content":29,"level":19},"/v1.0.18/guides/best-practices#performance","Performance",[947],{"id":1020,"title":1021,"titles":1022,"content":1023,"level":35},"/v1.0.18/guides/best-practices#batch-when-possible","Batch When Possible",[947,1017],"// Good: single round trip\nusers, _ := store.GetBatch(ctx, keys)\n\n// Bad: N round trips\nfor _, key := range keys {\n    user, _ := store.Get(ctx, key)\n}",{"id":1025,"title":1026,"titles":1027,"content":1028,"level":35},"/v1.0.18/guides/best-practices#set-appropriate-ttls","Set Appropriate TTLs",[947,1017],"// Session: expires\nstore.Set(ctx, \"session:abc\", &session, 24*time.Hour)\n\n// Config: no expiration\nstore.Set(ctx, \"config:app\", &config, 0)",{"id":1030,"title":1031,"titles":1032,"content":1033,"level":35},"/v1.0.18/guides/best-practices#use-list-limits","Use List Limits",[947,1017],"// Good: bounded\nkeys, _ := store.List(ctx, prefix, 100)\n\n// Risky: unbounded on large datasets\nkeys, _ := store.List(ctx, prefix, 0)",{"id":1035,"title":1036,"titles":1037,"content":1038,"level":35},"/v1.0.18/guides/best-practices#choose-the-right-codec","Choose the Right Codec",[947,1017],"ScenarioCodecInteroperabilityJSONCodecGo-only, performanceGobCodecCustom needsCustom implementation // Performance-sensitive path\nstore := grub.NewStoreWithCodec[Data](provider, grub.GobCodec{})",{"id":1040,"title":1041,"titles":1042,"content":29,"level":19},"/v1.0.18/guides/best-practices#provider-selection","Provider Selection",[947],{"id":1044,"title":1045,"titles":1046,"content":1047,"level":35},"/v1.0.18/guides/best-practices#match-provider-to-use-case","Match Provider to Use Case",[947,1041],"Use CaseProviderDistributed cacheRedisEmbedded/localBadgerDBSimple configBoltDBFiles/mediaS3/GCS/AzureStructured dataPostgreSQL/MariaDBEmbedded SQLSQLite",{"id":1049,"title":1050,"titles":1051,"content":1052,"level":35},"/v1.0.18/guides/best-practices#plan-for-provider-limitations","Plan for Provider Limitations",[947,1041],"// BoltDB doesn't support TTL\nif ttl > 0 {\n    // Handle expiration at application level\n    data.ExpiresAt = time.Now().Add(ttl)\n}\nstore.Set(ctx, key, data, 0)",{"id":1054,"title":1055,"titles":1056,"content":1057,"level":35},"/v1.0.18/guides/best-practices#test-with-target-provider","Test with Target Provider",[947,1041],"func TestWithProduction Provider(t *testing.T) {\n    if testing.Short() {\n        t.Skip(\"skipping integration test\")\n    }\n    // Test with actual provider\n}",{"id":1059,"title":1060,"titles":1061,"content":29,"level":19},"/v1.0.18/guides/best-practices#concurrency","Concurrency",[947],{"id":1063,"title":1064,"titles":1065,"content":1066,"level":35},"/v1.0.18/guides/best-practices#stores-are-thread-safe","Stores are Thread-Safe",[947,1060],"// Safe: concurrent access\nvar wg sync.WaitGroup\nfor i := 0; i \u003C 10; i++ {\n    wg.Add(1)\n    go func(i int) {\n        defer wg.Done()\n        store.Set(ctx, fmt.Sprintf(\"key:%d\", i), &data, 0)\n    }(i)\n}\nwg.Wait()",{"id":1068,"title":1069,"titles":1070,"content":1071,"level":35},"/v1.0.18/guides/best-practices#avoid-read-modify-write-races","Avoid Read-Modify-Write Races",[947,1060],"// Dangerous: race condition\nval, _ := store.Get(ctx, key)\nval.Counter++\nstore.Set(ctx, key, val, 0)\n\n// Safer: use provider-specific atomics or transactions\n// Or design to avoid conflicts",{"id":1073,"title":1074,"titles":1075,"content":1076,"level":35},"/v1.0.18/guides/best-practices#use-context-timeouts","Use Context Timeouts",[947,1060],"ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\ndefer cancel()\n\nuser, err := store.Get(ctx, key)\nif errors.Is(err, context.DeadlineExceeded) {\n    // Handle timeout\n}",{"id":1078,"title":1079,"titles":1080,"content":29,"level":19},"/v1.0.18/guides/best-practices#operational-guidelines","Operational Guidelines",[947],{"id":1082,"title":1083,"titles":1084,"content":1085,"level":35},"/v1.0.18/guides/best-practices#log-provider-errors","Log Provider Errors",[947,1079],"user, err := store.Get(ctx, key)\nif err != nil && !errors.Is(err, grub.ErrNotFound) {\n    log.Error(\"store error\", \"key\", key, \"error\", err)\n}",{"id":1087,"title":1088,"titles":1089,"content":1090,"level":35},"/v1.0.18/guides/best-practices#monitor-key-metrics","Monitor Key Metrics",[947,1079],"Operation latency (p50, p95, p99)Error rates by typeCache hit/miss ratioConnection pool usage",{"id":1092,"title":1093,"titles":1094,"content":1095,"level":35},"/v1.0.18/guides/best-practices#handle-provider-failures-gracefully","Handle Provider Failures Gracefully",[947,1079],"func GetWithFallback[T any](ctx context.Context, store *grub.Store[T], key string, fallback func() (*T, error)) (*T, error) {\n    val, err := store.Get(ctx, key)\n    if err == nil {\n        return val, nil\n    }\n\n    if errors.Is(err, grub.ErrNotFound) {\n        return fallback()\n    }\n\n    // Provider error - log and fallback\n    log.Warn(\"store unavailable\", \"error\", err)\n    return fallback()\n}",{"id":1097,"title":1098,"titles":1099,"content":1100,"level":35},"/v1.0.18/guides/best-practices#plan-for-schema-evolution","Plan for Schema Evolution",[947,1079],"// Version your types\ntype UserV1 struct {\n    ID   string `json:\"id\"`\n    Name string `json:\"name\"`\n}\n\ntype UserV2 struct {\n    ID        string `json:\"id\"`\n    FirstName string `json:\"first_name\"` // Split from Name\n    LastName  string `json:\"last_name\"`\n}\n\n// Migrate on read\nfunc migrateUser(data []byte) (*UserV2, error) {\n    var v2 UserV2\n    if err := json.Unmarshal(data, &v2); err == nil && v2.FirstName != \"\" {\n        return &v2, nil\n    }\n\n    var v1 UserV1\n    if err := json.Unmarshal(data, &v1); err != nil {\n        return nil, err\n    }\n\n    // Migrate v1 to v2\n    parts := strings.SplitN(v1.Name, \" \", 2)\n    return &UserV2{\n        ID:        v1.ID,\n        FirstName: parts[0],\n        LastName:  parts[1],\n    }, nil\n}",{"id":1102,"title":1103,"titles":1104,"content":29,"level":19},"/v1.0.18/guides/best-practices#anti-patterns","Anti-Patterns",[947],{"id":1106,"title":1107,"titles":1108,"content":1109,"level":35},"/v1.0.18/guides/best-practices#dont-store-large-blobs-in-key-value-stores","Don't Store Large Blobs in Key-Value Stores",[947,1103],"// Bad: large files in Redis\nstore.Set(ctx, \"file:video.mp4\", &LargeFile{Data: videoBytes}, 0)\n\n// Good: use blob storage\nbucket.Put(ctx, &grub.Object[FileMetadata]{\n    Key:  \"video.mp4\",\n    Data: FileMetadata{Name: \"video.mp4\", Size: len(videoBytes)},\n})",{"id":1111,"title":1112,"titles":1113,"content":1114,"level":35},"/v1.0.18/guides/best-practices#dont-create-stores-per-request","Don't Create Stores Per-Request",[947,1103],"// Bad: creates store on every request\nfunc Handler(w http.ResponseWriter, r *http.Request) {\n    store := grub.NewStore[Session](redis.New(client))\n    // ...\n}\n\n// Good: reuse store\nvar sessionStore = grub.NewStore[Session](redis.New(client))\n\nfunc Handler(w http.ResponseWriter, r *http.Request) {\n    session, _ := sessionStore.Get(r.Context(), sessionID)\n    // ...\n}",{"id":1116,"title":1117,"titles":1118,"content":1119,"level":35},"/v1.0.18/guides/best-practices#dont-ignore-context-cancellation","Don't Ignore Context Cancellation",[947,1103],"// Good: check context\nselect {\ncase \u003C-ctx.Done():\n    return ctx.Err()\ndefault:\n    return store.Set(ctx, key, value, 0)\n} html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .skxcq, html code.shiki .skxcq{--shiki-default:var(--shiki-builtin)}",{"id":1121,"title":1122,"titles":1123,"content":1124,"level":9},"/v1.0.18/cookbook/caching","Caching Patterns",[],"TTL strategies, cache invalidation, and read-through patterns",{"id":1126,"title":1122,"titles":1127,"content":1128,"level":9},"/v1.0.18/cookbook/caching#caching-patterns",[],"Real-world caching patterns using grub stores.",{"id":1130,"title":1131,"titles":1132,"content":1133,"level":19},"/v1.0.18/cookbook/caching#cache-aside-pattern","Cache-Aside Pattern",[1122],"The most common caching pattern: check cache first, fall back to source. package cache\n\nimport (\n    \"context\"\n    \"errors\"\n    \"time\"\n\n    \"github.com/zoobz-io/grub\"\n)\n\ntype CacheAside[T any] struct {\n    cache  *grub.Store[T]\n    source func(ctx context.Context, key string) (*T, error)\n    ttl    time.Duration\n}\n\nfunc NewCacheAside[T any](\n    cache *grub.Store[T],\n    source func(ctx context.Context, key string) (*T, error),\n    ttl time.Duration,\n) *CacheAside[T] {\n    return &CacheAside[T]{\n        cache:  cache,\n        source: source,\n        ttl:    ttl,\n    }\n}\n\nfunc (c *CacheAside[T]) Get(ctx context.Context, key string) (*T, error) {\n    // Try cache first\n    val, err := c.cache.Get(ctx, key)\n    if err == nil {\n        return val, nil\n    }\n\n    // Not in cache - get from source\n    if !errors.Is(err, grub.ErrNotFound) {\n        return nil, err // Cache error\n    }\n\n    val, err = c.source(ctx, key)\n    if err != nil {\n        return nil, err\n    }\n\n    // Populate cache (ignore errors - cache is best-effort)\n    _ = c.cache.Set(ctx, key, val, c.ttl)\n\n    return val, nil\n}\n\nfunc (c *CacheAside[T]) Invalidate(ctx context.Context, key string) error {\n    err := c.cache.Delete(ctx, key)\n    if errors.Is(err, grub.ErrNotFound) {\n        return nil\n    }\n    return err\n}",{"id":1135,"title":244,"titles":1136,"content":1137,"level":35},"/v1.0.18/cookbook/caching#usage",[1122,1131],"// Source: database lookup\nsource := func(ctx context.Context, key string) (*User, error) {\n    return db.Get(ctx, key)\n}\n\n// Cache with 5-minute TTL\nuserCache := NewCacheAside(\n    grub.NewStore[User](redis.New(client)),\n    source,\n    5*time.Minute,\n)\n\n// Get user (cache-aside)\nuser, err := userCache.Get(ctx, \"user:123\")\n\n// Invalidate on update\nuserCache.Invalidate(ctx, \"user:123\")",{"id":1139,"title":1140,"titles":1141,"content":1142,"level":19},"/v1.0.18/cookbook/caching#read-through-cache","Read-Through Cache",[1122],"Cache handles loading automatically with configurable staleness. package cache\n\nimport (\n    \"context\"\n    \"errors\"\n    \"sync\"\n    \"time\"\n\n    \"github.com/zoobz-io/grub\"\n)\n\ntype CachedValue[T any] struct {\n    Value     T         `json:\"value\"`\n    CachedAt  time.Time `json:\"cached_at\"`\n    ExpiresAt time.Time `json:\"expires_at\"`\n}\n\ntype ReadThrough[T any] struct {\n    cache   *grub.Store[CachedValue[T]]\n    loader  func(ctx context.Context, key string) (*T, error)\n    ttl     time.Duration\n    loading sync.Map // Prevents thundering herd\n}\n\nfunc NewReadThrough[T any](\n    cache *grub.Store[CachedValue[T]],\n    loader func(ctx context.Context, key string) (*T, error),\n    ttl time.Duration,\n) *ReadThrough[T] {\n    return &ReadThrough[T]{\n        cache:  cache,\n        loader: loader,\n        ttl:    ttl,\n    }\n}\n\nfunc (r *ReadThrough[T]) Get(ctx context.Context, key string) (*T, error) {\n    // Try cache\n    cached, err := r.cache.Get(ctx, key)\n    if err == nil {\n        if time.Now().Before(cached.ExpiresAt) {\n            return &cached.Value, nil\n        }\n        // Expired - fall through to reload\n    } else if !errors.Is(err, grub.ErrNotFound) {\n        return nil, err\n    }\n\n    // Prevent thundering herd\n    ch := make(chan struct{})\n    actual, loaded := r.loading.LoadOrStore(key, ch)\n    if loaded {\n        // Another goroutine is loading - wait\n        \u003C-actual.(chan struct{})\n        // Retry from cache\n        cached, err := r.cache.Get(ctx, key)\n        if err == nil {\n            return &cached.Value, nil\n        }\n        return nil, err\n    }\n\n    defer func() {\n        r.loading.Delete(key)\n        close(ch)\n    }()\n\n    // Load from source\n    val, err := r.loader(ctx, key)\n    if err != nil {\n        return nil, err\n    }\n\n    // Cache the result\n    now := time.Now()\n    _ = r.cache.Set(ctx, key, &CachedValue[T]{\n        Value:     *val,\n        CachedAt:  now,\n        ExpiresAt: now.Add(r.ttl),\n    }, r.ttl)\n\n    return val, nil\n}",{"id":1144,"title":1145,"titles":1146,"content":1147,"level":19},"/v1.0.18/cookbook/caching#write-through-cache","Write-Through Cache",[1122],"Update cache and source together. package cache\n\nimport (\n    \"context\"\n    \"time\"\n\n    \"github.com/zoobz-io/grub\"\n)\n\ntype WriteThrough[T any] struct {\n    cache  *grub.Store[T]\n    source interface {\n        Get(ctx context.Context, key string) (*T, error)\n        Set(ctx context.Context, key string, val *T) error\n        Delete(ctx context.Context, key string) error\n    }\n    ttl time.Duration\n}\n\nfunc (w *WriteThrough[T]) Get(ctx context.Context, key string) (*T, error) {\n    // Try cache first\n    val, err := w.cache.Get(ctx, key)\n    if err == nil {\n        return val, nil\n    }\n\n    // Load from source\n    val, err = w.source.Get(ctx, key)\n    if err != nil {\n        return nil, err\n    }\n\n    // Populate cache\n    _ = w.cache.Set(ctx, key, val, w.ttl)\n    return val, nil\n}\n\nfunc (w *WriteThrough[T]) Set(ctx context.Context, key string, val *T) error {\n    // Write to source first (source of truth)\n    if err := w.source.Set(ctx, key, val); err != nil {\n        return err\n    }\n\n    // Then update cache\n    return w.cache.Set(ctx, key, val, w.ttl)\n}\n\nfunc (w *WriteThrough[T]) Delete(ctx context.Context, key string) error {\n    // Delete from source first\n    if err := w.source.Delete(ctx, key); err != nil {\n        return err\n    }\n\n    // Then invalidate cache (ignore not found)\n    _ = w.cache.Delete(ctx, key)\n    return nil\n}",{"id":1149,"title":1150,"titles":1151,"content":29,"level":19},"/v1.0.18/cookbook/caching#ttl-strategies","TTL Strategies",[1122],{"id":1153,"title":1154,"titles":1155,"content":1156,"level":35},"/v1.0.18/cookbook/caching#fixed-ttl","Fixed TTL",[1122,1150],"Simple, predictable cache duration. const cacheTTL = 5 * time.Minute\n\nfunc (c *Cache[T]) Set(ctx context.Context, key string, val *T) error {\n    return c.store.Set(ctx, key, val, cacheTTL)\n}",{"id":1158,"title":1159,"titles":1160,"content":1161,"level":35},"/v1.0.18/cookbook/caching#sliding-window","Sliding Window",[1122,1150],"Reset TTL on each access. func (c *SlidingCache[T]) Get(ctx context.Context, key string) (*T, error) {\n    val, err := c.store.Get(ctx, key)\n    if err != nil {\n        return nil, err\n    }\n\n    // Reset TTL on access\n    _ = c.store.Set(ctx, key, val, c.ttl)\n    return val, nil\n}",{"id":1163,"title":1164,"titles":1165,"content":1166,"level":35},"/v1.0.18/cookbook/caching#tiered-ttl","Tiered TTL",[1122,1150],"Different TTLs based on data type or access patterns. var ttlByType = map[string]time.Duration{\n    \"session\":  24 * time.Hour,\n    \"config\":   1 * time.Hour,\n    \"user\":     5 * time.Minute,\n    \"product\":  15 * time.Minute,\n}\n\nfunc getTTL(keyType string) time.Duration {\n    if ttl, ok := ttlByType[keyType]; ok {\n        return ttl\n    }\n    return 5 * time.Minute // Default\n}",{"id":1168,"title":1169,"titles":1170,"content":1171,"level":35},"/v1.0.18/cookbook/caching#jittered-ttl","Jittered TTL",[1122,1150],"Prevent cache stampedes by adding randomness. import \"math/rand\"\n\nfunc jitteredTTL(base time.Duration) time.Duration {\n    // Add 0-20% jitter\n    jitter := time.Duration(rand.Float64() * 0.2 * float64(base))\n    return base + jitter\n}\n\nfunc (c *Cache[T]) Set(ctx context.Context, key string, val *T) error {\n    return c.store.Set(ctx, key, val, jitteredTTL(c.baseTTL))\n}",{"id":1173,"title":1174,"titles":1175,"content":29,"level":19},"/v1.0.18/cookbook/caching#cache-invalidation","Cache Invalidation",[1122],{"id":1177,"title":1178,"titles":1179,"content":1180,"level":35},"/v1.0.18/cookbook/caching#event-based-invalidation","Event-Based Invalidation",[1122,1174],"// User service\nfunc (s *UserService) Update(ctx context.Context, user *User) error {\n    if err := s.db.Set(ctx, user.ID, user); err != nil {\n        return err\n    }\n\n    // Invalidate cache\n    key := \"user:\" + user.ID\n    _ = s.cache.Delete(ctx, key)\n\n    // Publish event for other services\n    s.events.Publish(ctx, \"user.updated\", user.ID)\n\n    return nil\n}\n\n// Cache listener\nfunc (c *CacheInvalidator) HandleUserUpdated(ctx context.Context, userID string) {\n    key := \"user:\" + userID\n    _ = c.store.Delete(ctx, key)\n\n    // Also invalidate derived caches\n    _ = c.store.Delete(ctx, \"user-profile:\"+userID)\n    _ = c.store.Delete(ctx, \"user-permissions:\"+userID)\n}",{"id":1182,"title":1183,"titles":1184,"content":1185,"level":35},"/v1.0.18/cookbook/caching#tag-based-invalidation","Tag-Based Invalidation",[1122,1174],"Group cached items by tags for bulk invalidation. type TaggedCache[T any] struct {\n    data *grub.Store[T]\n    tags *grub.Store[[]string] // tag -> keys\n}\n\nfunc (c *TaggedCache[T]) Set(ctx context.Context, key string, val *T, tags []string, ttl time.Duration) error {\n    // Store value\n    if err := c.data.Set(ctx, key, val, ttl); err != nil {\n        return err\n    }\n\n    // Update tag indexes\n    for _, tag := range tags {\n        tagKey := \"tag:\" + tag\n        existing, _ := c.tags.Get(ctx, tagKey)\n        keys := []string{key}\n        if existing != nil {\n            keys = append(*existing, key)\n        }\n        _ = c.tags.Set(ctx, tagKey, &keys, ttl)\n    }\n\n    return nil\n}\n\nfunc (c *TaggedCache[T]) InvalidateTag(ctx context.Context, tag string) error {\n    tagKey := \"tag:\" + tag\n    keys, err := c.tags.Get(ctx, tagKey)\n    if err != nil {\n        return nil // No keys for tag\n    }\n\n    for _, key := range *keys {\n        _ = c.data.Delete(ctx, key)\n    }\n\n    return c.tags.Delete(ctx, tagKey)\n}",{"id":1187,"title":244,"titles":1188,"content":1189,"level":35},"/v1.0.18/cookbook/caching#usage-1",[1122,1174],"cache := NewTaggedCache[Product](dataStore, tagStore)\n\n// Cache product with tags\ncache.Set(ctx, \"product:123\", &product, []string{\"category:electronics\", \"vendor:acme\"}, time.Hour)\n\n// Invalidate all products in category\ncache.InvalidateTag(ctx, \"category:electronics\")",{"id":1191,"title":1192,"titles":1193,"content":1194,"level":19},"/v1.0.18/cookbook/caching#multi-level-cache","Multi-Level Cache",[1122],"Local memory cache with Redis backing. package cache\n\nimport (\n    \"context\"\n    \"sync\"\n    \"time\"\n\n    \"github.com/zoobz-io/grub\"\n)\n\ntype L1L2Cache[T any] struct {\n    l1     sync.Map                   // In-memory (L1)\n    l2     *grub.Store[T]             // Redis (L2)\n    l1TTL  time.Duration\n    l2TTL  time.Duration\n}\n\ntype l1Entry[T any] struct {\n    value     *T\n    expiresAt time.Time\n}\n\nfunc (c *L1L2Cache[T]) Get(ctx context.Context, key string) (*T, error) {\n    // Check L1\n    if entry, ok := c.l1.Load(key); ok {\n        e := entry.(l1Entry[T])\n        if time.Now().Before(e.expiresAt) {\n            return e.value, nil\n        }\n        c.l1.Delete(key) // Expired\n    }\n\n    // Check L2\n    val, err := c.l2.Get(ctx, key)\n    if err != nil {\n        return nil, err\n    }\n\n    // Populate L1\n    c.l1.Store(key, l1Entry[T]{\n        value:     val,\n        expiresAt: time.Now().Add(c.l1TTL),\n    })\n\n    return val, nil\n}\n\nfunc (c *L1L2Cache[T]) Set(ctx context.Context, key string, val *T) error {\n    // Write to L2\n    if err := c.l2.Set(ctx, key, val, c.l2TTL); err != nil {\n        return err\n    }\n\n    // Write to L1\n    c.l1.Store(key, l1Entry[T]{\n        value:     val,\n        expiresAt: time.Now().Add(c.l1TTL),\n    })\n\n    return nil\n}\n\nfunc (c *L1L2Cache[T]) Delete(ctx context.Context, key string) error {\n    c.l1.Delete(key)\n    return c.l2.Delete(ctx, key)\n}",{"id":1196,"title":1197,"titles":1198,"content":1199,"level":19},"/v1.0.18/cookbook/caching#warming-the-cache","Warming the Cache",[1122],"Pre-populate cache on startup. func WarmCache[T any](ctx context.Context, cache *grub.Store[T], keys []string, loader func(string) (*T, error), ttl time.Duration) error {\n    const workers = 10\n    keyCh := make(chan string, len(keys))\n\n    var wg sync.WaitGroup\n    var mu sync.Mutex\n    var errors []error\n\n    // Start workers\n    for i := 0; i \u003C workers; i++ {\n        wg.Add(1)\n        go func() {\n            defer wg.Done()\n            for key := range keyCh {\n                val, err := loader(key)\n                if err != nil {\n                    mu.Lock()\n                    errors = append(errors, err)\n                    mu.Unlock()\n                    continue\n                }\n                _ = cache.Set(ctx, key, val, ttl)\n            }\n        }()\n    }\n\n    // Send keys\n    for _, key := range keys {\n        keyCh \u003C- key\n    }\n    close(keyCh)\n\n    wg.Wait()\n\n    if len(errors) > 0 {\n        return fmt.Errorf(\"cache warming had %d errors\", len(errors))\n    }\n    return nil\n}",{"id":1201,"title":244,"titles":1202,"content":1203,"level":35},"/v1.0.18/cookbook/caching#usage-2",[1122,1197],"// On application startup\nkeys := []string{\"config:app\", \"config:features\", \"config:limits\"}\nloader := func(key string) (*Config, error) {\n    return db.Get(ctx, key)\n}\nWarmCache(ctx, configCache, keys, loader, time.Hour) html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .skxcq, html code.shiki .skxcq{--shiki-default:var(--shiki-builtin)}html pre.shiki code .sfm-E, html code.shiki .sfm-E{--shiki-default:var(--shiki-variable)}html pre.shiki code .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}",{"id":1205,"title":1206,"titles":1207,"content":1208,"level":9},"/v1.0.18/cookbook/migrations","Provider Migrations",[],"Switching storage backends without downtime",{"id":1210,"title":1206,"titles":1211,"content":1212,"level":9},"/v1.0.18/cookbook/migrations#provider-migrations",[],"Strategies for migrating between storage providers.",{"id":1214,"title":1215,"titles":1216,"content":1217,"level":19},"/v1.0.18/cookbook/migrations#migration-patterns","Migration Patterns",[1206],"PatternDowntimeComplexityUse CaseBig BangYesLowSmall datasets, dev/stagingDual WriteNoMediumLarge datasets, productionShadow ReadNoHighValidation before cutover",{"id":1219,"title":1220,"titles":1221,"content":1222,"level":19},"/v1.0.18/cookbook/migrations#big-bang-migration","Big Bang Migration",[1206],"Export from source, import to destination. package migration\n\nimport (\n    \"context\"\n    \"fmt\"\n\n    \"github.com/zoobz-io/grub\"\n)\n\nfunc MigrateStore[T any](\n    ctx context.Context,\n    source *grub.Store[T],\n    dest *grub.Store[T],\n    prefix string,\n) error {\n    const batchSize = 100\n    marker := prefix\n\n    for {\n        keys, err := source.List(ctx, marker, batchSize)\n        if err != nil {\n            return fmt.Errorf(\"listing keys: %w\", err)\n        }\n\n        if len(keys) == 0 {\n            break\n        }\n\n        // Batch read\n        values, err := source.GetBatch(ctx, keys)\n        if err != nil {\n            return fmt.Errorf(\"reading batch: %w\", err)\n        }\n\n        // Batch write (TTL=0, preserve existing TTL separately if needed)\n        if err := dest.SetBatch(ctx, values, 0); err != nil {\n            return fmt.Errorf(\"writing batch: %w\", err)\n        }\n\n        fmt.Printf(\"Migrated %d keys\\n\", len(keys))\n\n        if len(keys) \u003C batchSize {\n            break\n        }\n\n        marker = keys[len(keys)-1] + \"\\x00\"\n    }\n\n    return nil\n}",{"id":1224,"title":244,"titles":1225,"content":1226,"level":35},"/v1.0.18/cookbook/migrations#usage",[1206,1220],"// Migrate from Badger to Redis\nsourceDB, _ := badgerdb.Open(opts)\nsourceStore := grub.NewStore[Session](badger.New(sourceDB))\n\ndestClient := goredis.NewClient(&goredis.Options{Addr: \"redis:6379\"})\ndestStore := grub.NewStore[Session](redis.New(destClient))\n\nerr := MigrateStore(ctx, sourceStore, destStore, \"session:\")",{"id":1228,"title":1229,"titles":1230,"content":1231,"level":19},"/v1.0.18/cookbook/migrations#dual-write-pattern","Dual Write Pattern",[1206],"Write to both stores during migration period. package migration\n\nimport (\n    \"context\"\n    \"errors\"\n    \"log\"\n    \"time\"\n\n    \"github.com/zoobz-io/grub\"\n)\n\ntype DualWriteStore[T any] struct {\n    primary   *grub.Store[T]\n    secondary *grub.Store[T]\n    readFrom  string // \"primary\" or \"secondary\"\n}\n\nfunc NewDualWriteStore[T any](primary, secondary *grub.Store[T]) *DualWriteStore[T] {\n    return &DualWriteStore[T]{\n        primary:   primary,\n        secondary: secondary,\n        readFrom:  \"primary\",\n    }\n}\n\nfunc (d *DualWriteStore[T]) Get(ctx context.Context, key string) (*T, error) {\n    if d.readFrom == \"secondary\" {\n        return d.secondary.Get(ctx, key)\n    }\n    return d.primary.Get(ctx, key)\n}\n\nfunc (d *DualWriteStore[T]) Set(ctx context.Context, key string, val *T, ttl time.Duration) error {\n    // Write to primary first\n    if err := d.primary.Set(ctx, key, val, ttl); err != nil {\n        return err\n    }\n\n    // Write to secondary (async, log errors)\n    go func() {\n        if err := d.secondary.Set(context.Background(), key, val, ttl); err != nil {\n            log.Printf(\"secondary write failed: %v\", err)\n        }\n    }()\n\n    return nil\n}\n\nfunc (d *DualWriteStore[T]) Delete(ctx context.Context, key string) error {\n    err1 := d.primary.Delete(ctx, key)\n    err2 := d.secondary.Delete(ctx, key)\n\n    // Return primary error, log secondary\n    if err2 != nil && !errors.Is(err2, grub.ErrNotFound) {\n        log.Printf(\"secondary delete failed: %v\", err2)\n    }\n\n    return err1\n}\n\nfunc (d *DualWriteStore[T]) SwitchToSecondary() {\n    d.readFrom = \"secondary\"\n}\n\nfunc (d *DualWriteStore[T]) Exists(ctx context.Context, key string) (bool, error) {\n    if d.readFrom == \"secondary\" {\n        return d.secondary.Exists(ctx, key)\n    }\n    return d.primary.Exists(ctx, key)\n}\n\nfunc (d *DualWriteStore[T]) List(ctx context.Context, prefix string, limit int) ([]string, error) {\n    if d.readFrom == \"secondary\" {\n        return d.secondary.List(ctx, prefix, limit)\n    }\n    return d.primary.List(ctx, prefix, limit)\n}",{"id":1233,"title":1234,"titles":1235,"content":1236,"level":35},"/v1.0.18/cookbook/migrations#migration-steps","Migration Steps",[1206,1229],"// 1. Start dual writes\noldStore := grub.NewStore[Session](badger.New(db))\nnewStore := grub.NewStore[Session](redis.New(client))\ndualStore := NewDualWriteStore(oldStore, newStore)\n\n// 2. Migrate existing data\nMigrateStore(ctx, oldStore, newStore, \"session:\")\n\n// 3. Verify data consistency\nValidateStores(ctx, oldStore, newStore, \"session:\")\n\n// 4. Switch reads to new store\ndualStore.SwitchToSecondary()\n\n// 5. After confidence period, switch to single store\nsessionStore := newStore",{"id":1238,"title":1239,"titles":1240,"content":1241,"level":19},"/v1.0.18/cookbook/migrations#shadow-read-pattern","Shadow Read Pattern",[1206],"Read from both and compare to validate before cutover. package migration\n\nimport (\n    \"context\"\n    \"log\"\n    \"reflect\"\n    \"time\"\n\n    \"github.com/zoobz-io/grub\"\n)\n\ntype ShadowReadStore[T any] struct {\n    primary   *grub.Store[T]\n    shadow    *grub.Store[T]\n    compare   func(*T, *T) bool\n    onMismatch func(key string, primary, shadow *T)\n}\n\nfunc (s *ShadowReadStore[T]) Get(ctx context.Context, key string) (*T, error) {\n    // Read from primary (authoritative)\n    primaryVal, err := s.primary.Get(ctx, key)\n\n    // Shadow read (async, for comparison)\n    go func() {\n        shadowVal, shadowErr := s.shadow.Get(context.Background(), key)\n\n        // Compare results\n        if err != shadowErr {\n            log.Printf(\"shadow: error mismatch for %s: primary=%v shadow=%v\", key, err, shadowErr)\n            return\n        }\n\n        if err == nil && !s.compare(primaryVal, shadowVal) {\n            s.onMismatch(key, primaryVal, shadowVal)\n        }\n    }()\n\n    return primaryVal, err\n}",{"id":1243,"title":244,"titles":1244,"content":1245,"level":35},"/v1.0.18/cookbook/migrations#usage-1",[1206,1239],"shadowStore := &ShadowReadStore[User]{\n    primary: oldStore,\n    shadow:  newStore,\n    compare: func(a, b *User) bool {\n        return reflect.DeepEqual(a, b)\n    },\n    onMismatch: func(key string, primary, shadow *User) {\n        log.Printf(\"MISMATCH %s: primary=%+v shadow=%+v\", key, primary, shadow)\n        metrics.Increment(\"migration.mismatch\")\n    },\n}",{"id":1247,"title":1248,"titles":1249,"content":29,"level":19},"/v1.0.18/cookbook/migrations#validation-utilities","Validation Utilities",[1206],{"id":1251,"title":1252,"titles":1253,"content":1254,"level":35},"/v1.0.18/cookbook/migrations#compare-stores","Compare Stores",[1206,1248],"func ValidateStores[T any](\n    ctx context.Context,\n    source *grub.Store[T],\n    dest *grub.Store[T],\n    prefix string,\n) (int, []string, error) {\n    const batchSize = 100\n    var mismatches []string\n    total := 0\n    marker := prefix\n\n    for {\n        keys, err := source.List(ctx, marker, batchSize)\n        if err != nil {\n            return 0, nil, err\n        }\n\n        if len(keys) == 0 {\n            break\n        }\n\n        sourceVals, _ := source.GetBatch(ctx, keys)\n        destVals, _ := dest.GetBatch(ctx, keys)\n\n        for key, sourceVal := range sourceVals {\n            total++\n            destVal, exists := destVals[key]\n            if !exists {\n                mismatches = append(mismatches, key+\" (missing)\")\n                continue\n            }\n            if !reflect.DeepEqual(sourceVal, destVal) {\n                mismatches = append(mismatches, key+\" (different)\")\n            }\n        }\n\n        if len(keys) \u003C batchSize {\n            break\n        }\n        marker = keys[len(keys)-1] + \"\\x00\"\n    }\n\n    return total, mismatches, nil\n}",{"id":1256,"title":1257,"titles":1258,"content":1259,"level":35},"/v1.0.18/cookbook/migrations#sync-missing-keys","Sync Missing Keys",[1206,1248],"func SyncMissing[T any](\n    ctx context.Context,\n    source *grub.Store[T],\n    dest *grub.Store[T],\n    keys []string,\n) error {\n    for _, key := range keys {\n        val, err := source.Get(ctx, key)\n        if err != nil {\n            continue\n        }\n        if err := dest.Set(ctx, key, val, 0); err != nil {\n            return err\n        }\n    }\n    return nil\n}",{"id":1261,"title":1262,"titles":1263,"content":1264,"level":19},"/v1.0.18/cookbook/migrations#bucket-migration","Bucket Migration",[1206],"Migrate blob storage between providers. func MigrateBucket[T any](\n    ctx context.Context,\n    source *grub.Bucket[T],\n    dest *grub.Bucket[T],\n    prefix string,\n) error {\n    const batchSize = 50\n    marker := prefix\n\n    for {\n        infos, err := source.List(ctx, marker, batchSize)\n        if err != nil {\n            return err\n        }\n\n        if len(infos) == 0 {\n            break\n        }\n\n        for _, info := range infos {\n            obj, err := source.Get(ctx, info.Key)\n            if err != nil {\n                return fmt.Errorf(\"reading %s: %w\", info.Key, err)\n            }\n\n            if err := dest.Put(ctx, obj); err != nil {\n                return fmt.Errorf(\"writing %s: %w\", info.Key, err)\n            }\n\n            fmt.Printf(\"Migrated %s (%d bytes)\\n\", info.Key, info.Size)\n        }\n\n        if len(infos) \u003C batchSize {\n            break\n        }\n        marker = infos[len(infos)-1].Key\n    }\n\n    return nil\n}",{"id":1266,"title":1267,"titles":1268,"content":1269,"level":19},"/v1.0.18/cookbook/migrations#database-migration","Database Migration",[1206],"For SQL databases, migrations typically involve schema changes rather than provider changes.",{"id":1271,"title":1272,"titles":1273,"content":1274,"level":35},"/v1.0.18/cookbook/migrations#provider-agnostic-schema","Provider-Agnostic Schema",[1206,1267],"Use grub's Database wrapper with different renderers: // Development: SQLite\ndevDB := grub.NewDatabase[User](sqliteConn, \"users\", sqlite.New())\n\n// Production: PostgreSQL\nprodDB := grub.NewDatabase[User](pgConn, \"users\", postgres.New())",{"id":1276,"title":1277,"titles":1278,"content":1279,"level":35},"/v1.0.18/cookbook/migrations#data-exportimport","Data Export/Import",[1206,1267],"func ExportTable[T any](ctx context.Context, db *grub.Database[T]) ([]*T, error) {\n    return db.ExecQuery(ctx, grub.QueryAll, nil)\n}\n\nfunc ImportTable[T any](ctx context.Context, db *grub.Database[T], records []*T, keyFn func(*T) string) error {\n    for _, record := range records {\n        if err := db.Set(ctx, keyFn(record), record); err != nil {\n            return err\n        }\n    }\n    return nil\n}",{"id":1281,"title":1282,"titles":1283,"content":1284,"level":19},"/v1.0.18/cookbook/migrations#rollback-strategy","Rollback Strategy",[1206],"Always plan for rollback. type MigrationState struct {\n    Phase           string    // \"dual_write\", \"shadow_read\", \"cutover\", \"complete\"\n    StartedAt       time.Time\n    CutoverAt       *time.Time\n    RollbackAt      *time.Time\n    KeysMigrated    int\n    KeysValidated   int\n    MismatchCount   int\n}\n\nfunc (m *MigrationState) CanRollback() bool {\n    // Can rollback if dual write is still active\n    return m.Phase == \"dual_write\" || m.Phase == \"shadow_read\"\n}\n\nfunc (m *MigrationState) Rollback() {\n    m.Phase = \"rolled_back\"\n    now := time.Now()\n    m.RollbackAt = &now\n}",{"id":1286,"title":1287,"titles":1288,"content":29,"level":19},"/v1.0.18/cookbook/migrations#checklist","Checklist",[1206],{"id":1290,"title":1291,"titles":1292,"content":1293,"level":35},"/v1.0.18/cookbook/migrations#before-migration","Before Migration",[1206,1287],"Estimate data volume and migration time Plan maintenance window (if big bang) Set up monitoring for dual write errors Test migration on staging Document rollback procedure",{"id":1295,"title":1296,"titles":1297,"content":1298,"level":35},"/v1.0.18/cookbook/migrations#during-migration","During Migration",[1206,1287],"Monitor error rates Check replication lag (if applicable) Validate sample records Watch resource utilization",{"id":1300,"title":1301,"titles":1302,"content":1303,"level":35},"/v1.0.18/cookbook/migrations#after-migration","After Migration",[1206,1287],"Run full validation Monitor for mismatches Keep old store available for rollback period Clean up old infrastructure after confidence period html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sfm-E, html code.shiki .sfm-E{--shiki-default:var(--shiki-variable)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html pre.shiki code .skxcq, html code.shiki .skxcq{--shiki-default:var(--shiki-builtin)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .suWN2, html code.shiki .suWN2{--shiki-default:var(--shiki-tag)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}",{"id":1305,"title":1306,"titles":1307,"content":1308,"level":9},"/v1.0.18/cookbook/multi-tenant","Multi-Tenant Patterns",[],"Isolating tenant data across storage backends",{"id":1310,"title":1306,"titles":1311,"content":1312,"level":9},"/v1.0.18/cookbook/multi-tenant#multi-tenant-patterns",[],"Strategies for isolating tenant data using grub.",{"id":1314,"title":1315,"titles":1316,"content":1317,"level":19},"/v1.0.18/cookbook/multi-tenant#isolation-strategies","Isolation Strategies",[1306],"StrategyIsolationComplexityCostKey PrefixLogicalLowLowSeparate StoresStrongMediumMediumSeparate ProvidersCompleteHighHigh",{"id":1319,"title":1320,"titles":1321,"content":1322,"level":19},"/v1.0.18/cookbook/multi-tenant#key-prefix-isolation","Key Prefix Isolation",[1306],"Simplest approach: prefix all keys with tenant ID. package tenant\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"time\"\n\n    \"github.com/zoobz-io/grub\"\n)\n\ntype TenantStore[T any] struct {\n    store *grub.Store[T]\n}\n\nfunc NewTenantStore[T any](store *grub.Store[T]) *TenantStore[T] {\n    return &TenantStore[T]{store: store}\n}\n\nfunc (t *TenantStore[T]) key(tenantID, key string) string {\n    return fmt.Sprintf(\"tenant:%s:%s\", tenantID, key)\n}\n\nfunc (t *TenantStore[T]) Get(ctx context.Context, tenantID, key string) (*T, error) {\n    return t.store.Get(ctx, t.key(tenantID, key))\n}\n\nfunc (t *TenantStore[T]) Set(ctx context.Context, tenantID, key string, val *T, ttl time.Duration) error {\n    return t.store.Set(ctx, t.key(tenantID, key), val, ttl)\n}\n\nfunc (t *TenantStore[T]) Delete(ctx context.Context, tenantID, key string) error {\n    return t.store.Delete(ctx, t.key(tenantID, key))\n}\n\nfunc (t *TenantStore[T]) Exists(ctx context.Context, tenantID, key string) (bool, error) {\n    return t.store.Exists(ctx, t.key(tenantID, key))\n}\n\nfunc (t *TenantStore[T]) List(ctx context.Context, tenantID, prefix string, limit int) ([]string, error) {\n    fullPrefix := t.key(tenantID, prefix)\n    keys, err := t.store.List(ctx, fullPrefix, limit)\n    if err != nil {\n        return nil, err\n    }\n\n    // Strip tenant prefix from results\n    baseLen := len(t.key(tenantID, \"\"))\n    result := make([]string, len(keys))\n    for i, key := range keys {\n        result[i] = key[baseLen:]\n    }\n    return result, nil\n}",{"id":1324,"title":244,"titles":1325,"content":1326,"level":35},"/v1.0.18/cookbook/multi-tenant#usage",[1306,1320],"store := grub.NewStore[Session](redis.New(client))\ntenantStore := NewTenantStore(store)\n\n// Each tenant's data is isolated by prefix\ntenantStore.Set(ctx, \"acme\", \"session:123\", &session, time.Hour)\ntenantStore.Set(ctx, \"beta\", \"session:123\", &session, time.Hour)\n\n// Listing shows only tenant's keys\nkeys, _ := tenantStore.List(ctx, \"acme\", \"session:\", 100)\n// Returns [\"session:123\"], not \"tenant:acme:session:123\"",{"id":1328,"title":1329,"titles":1330,"content":1331,"level":19},"/v1.0.18/cookbook/multi-tenant#context-based-tenant-resolution","Context-Based Tenant Resolution",[1306],"Extract tenant from context for cleaner API. package tenant\n\nimport (\n    \"context\"\n    \"errors\"\n)\n\ntype contextKey string\n\nconst tenantKey contextKey = \"tenant_id\"\n\nvar ErrNoTenant = errors.New(\"no tenant in context\")\n\nfunc WithTenant(ctx context.Context, tenantID string) context.Context {\n    return context.WithValue(ctx, tenantKey, tenantID)\n}\n\nfunc FromContext(ctx context.Context) (string, error) {\n    id, ok := ctx.Value(tenantKey).(string)\n    if !ok || id == \"\" {\n        return \"\", ErrNoTenant\n    }\n    return id, nil\n}",{"id":1333,"title":1334,"titles":1335,"content":1336,"level":35},"/v1.0.18/cookbook/multi-tenant#store-with-context-tenant","Store with Context Tenant",[1306,1329],"type ContextTenantStore[T any] struct {\n    inner *TenantStore[T]\n}\n\nfunc (c *ContextTenantStore[T]) Get(ctx context.Context, key string) (*T, error) {\n    tenantID, err := FromContext(ctx)\n    if err != nil {\n        return nil, err\n    }\n    return c.inner.Get(ctx, tenantID, key)\n}\n\nfunc (c *ContextTenantStore[T]) Set(ctx context.Context, key string, val *T, ttl time.Duration) error {\n    tenantID, err := FromContext(ctx)\n    if err != nil {\n        return err\n    }\n    return c.inner.Set(ctx, tenantID, key, val, ttl)\n}\n\n// ... other methods",{"id":1338,"title":244,"titles":1339,"content":1340,"level":35},"/v1.0.18/cookbook/multi-tenant#usage-1",[1306,1329],"// Middleware sets tenant\nfunc TenantMiddleware(next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        tenantID := r.Header.Get(\"X-Tenant-ID\")\n        ctx := WithTenant(r.Context(), tenantID)\n        next.ServeHTTP(w, r.WithContext(ctx))\n    })\n}\n\n// Handler uses store without explicit tenant\nfunc (h *Handler) GetSession(w http.ResponseWriter, r *http.Request) {\n    session, err := h.sessions.Get(r.Context(), sessionID)\n    // Tenant is extracted from context automatically\n}",{"id":1342,"title":1343,"titles":1344,"content":1345,"level":19},"/v1.0.18/cookbook/multi-tenant#separate-stores-per-tenant","Separate Stores Per Tenant",[1306],"Stronger isolation with dedicated stores. package tenant\n\nimport (\n    \"context\"\n    \"sync\"\n    \"time\"\n\n    \"github.com/zoobz-io/grub\"\n)\n\ntype StoreFactory[T any] func(tenantID string) (*grub.Store[T], error)\n\ntype MultiTenantStore[T any] struct {\n    factory StoreFactory[T]\n    stores  sync.Map // tenantID -> *grub.Store[T]\n}\n\nfunc NewMultiTenantStore[T any](factory StoreFactory[T]) *MultiTenantStore[T] {\n    return &MultiTenantStore[T]{factory: factory}\n}\n\nfunc (m *MultiTenantStore[T]) getStore(tenantID string) (*grub.Store[T], error) {\n    if store, ok := m.stores.Load(tenantID); ok {\n        return store.(*grub.Store[T]), nil\n    }\n\n    store, err := m.factory(tenantID)\n    if err != nil {\n        return nil, err\n    }\n\n    actual, _ := m.stores.LoadOrStore(tenantID, store)\n    return actual.(*grub.Store[T]), nil\n}\n\nfunc (m *MultiTenantStore[T]) Get(ctx context.Context, tenantID, key string) (*T, error) {\n    store, err := m.getStore(tenantID)\n    if err != nil {\n        return nil, err\n    }\n    return store.Get(ctx, key)\n}\n\nfunc (m *MultiTenantStore[T]) Set(ctx context.Context, tenantID, key string, val *T, ttl time.Duration) error {\n    store, err := m.getStore(tenantID)\n    if err != nil {\n        return err\n    }\n    return store.Set(ctx, key, val, ttl)\n}",{"id":1347,"title":1348,"titles":1349,"content":1350,"level":35},"/v1.0.18/cookbook/multi-tenant#with-boltdb-separate-buckets","With BoltDB (Separate Buckets)",[1306,1343],"func BoltStoreFactory[T any](db *bbolt.DB) StoreFactory[T] {\n    return func(tenantID string) (*grub.Store[T], error) {\n        return grub.NewStore[T](bolt.New(db, tenantID)), nil\n    }\n}\n\n// Usage\ndb, _ := bbolt.Open(\"tenants.db\", 0600, nil)\nmultiStore := NewMultiTenantStore(BoltStoreFactory[Session](db))",{"id":1352,"title":1353,"titles":1354,"content":1355,"level":35},"/v1.0.18/cookbook/multi-tenant#with-redis-separate-databases","With Redis (Separate Databases)",[1306,1343],"func RedisStoreFactory[T any](baseOpts *goredis.Options) StoreFactory[T] {\n    var dbIndex int\n    var mu sync.Mutex\n    tenantDBs := make(map[string]int)\n\n    return func(tenantID string) (*grub.Store[T], error) {\n        mu.Lock()\n        defer mu.Unlock()\n\n        if db, ok := tenantDBs[tenantID]; ok {\n            opts := *baseOpts\n            opts.DB = db\n            return grub.NewStore[T](redis.New(goredis.NewClient(&opts))), nil\n        }\n\n        dbIndex++\n        tenantDBs[tenantID] = dbIndex\n        opts := *baseOpts\n        opts.DB = dbIndex\n        return grub.NewStore[T](redis.New(goredis.NewClient(&opts))), nil\n    }\n}",{"id":1357,"title":1358,"titles":1359,"content":1360,"level":19},"/v1.0.18/cookbook/multi-tenant#separate-providers-per-tenant","Separate Providers Per Tenant",[1306],"Complete isolation with dedicated infrastructure. package tenant\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"sync\"\n\n    \"github.com/zoobz-io/grub\"\n    \"github.com/zoobz-io/grub/badger\"\n    badgerdb \"github.com/dgraph-io/badger/v4\"\n)\n\ntype TenantConfig struct {\n    ID      string\n    DataDir string\n}\n\ntype IsolatedTenantStore[T any] struct {\n    configs map[string]TenantConfig\n    stores  sync.Map\n}\n\nfunc (i *IsolatedTenantStore[T]) getStore(tenantID string) (*grub.Store[T], error) {\n    if store, ok := i.stores.Load(tenantID); ok {\n        return store.(*grub.Store[T]), nil\n    }\n\n    config, ok := i.configs[tenantID]\n    if !ok {\n        return nil, fmt.Errorf(\"unknown tenant: %s\", tenantID)\n    }\n\n    opts := badgerdb.DefaultOptions(config.DataDir)\n    opts.Logger = nil\n    db, err := badgerdb.Open(opts)\n    if err != nil {\n        return nil, err\n    }\n\n    store := grub.NewStore[T](badger.New(db))\n    i.stores.Store(tenantID, store)\n    return store, nil\n}",{"id":1362,"title":1363,"titles":1364,"content":1365,"level":19},"/v1.0.18/cookbook/multi-tenant#multi-tenant-bucket","Multi-Tenant Bucket",[1306],"Same patterns apply to blob storage. type TenantBucket[T any] struct {\n    bucket *grub.Bucket[T]\n}\n\nfunc (t *TenantBucket[T]) key(tenantID, key string) string {\n    return fmt.Sprintf(\"%s/%s\", tenantID, key)\n}\n\nfunc (t *TenantBucket[T]) Get(ctx context.Context, tenantID, key string) (*grub.Object[T], error) {\n    obj, err := t.bucket.Get(ctx, t.key(tenantID, key))\n    if err != nil {\n        return nil, err\n    }\n    // Strip tenant prefix from key\n    obj.Key = key\n    return obj, nil\n}\n\nfunc (t *TenantBucket[T]) Put(ctx context.Context, tenantID string, obj *grub.Object[T]) error {\n    obj.Key = t.key(tenantID, obj.Key)\n    return t.bucket.Put(ctx, obj)\n}\n\nfunc (t *TenantBucket[T]) List(ctx context.Context, tenantID, prefix string, limit int) ([]grub.ObjectInfo, error) {\n    fullPrefix := t.key(tenantID, prefix)\n    infos, err := t.bucket.List(ctx, fullPrefix, limit)\n    if err != nil {\n        return nil, err\n    }\n\n    // Strip tenant prefix\n    baseLen := len(t.key(tenantID, \"\"))\n    for i := range infos {\n        infos[i].Key = infos[i].Key[baseLen:]\n    }\n    return infos, nil\n}",{"id":1367,"title":1368,"titles":1369,"content":1370,"level":19},"/v1.0.18/cookbook/multi-tenant#multi-tenant-database","Multi-Tenant Database",[1306],"For SQL, use row-level or schema-level isolation.",{"id":1372,"title":1373,"titles":1374,"content":1375,"level":35},"/v1.0.18/cookbook/multi-tenant#row-level-isolation","Row-Level Isolation",[1306,1368],"type TenantUser struct {\n    ID       string `json:\"id\" db:\"id\"`\n    TenantID string `json:\"tenant_id\" db:\"tenant_id\"`\n    Email    string `json:\"email\" db:\"email\"`\n}\n\n// Query with tenant filter using builder\nusers, _ := db.Query().\n    Where(\"tenant_id\", \"=\", \"tenant_id\").\n    Exec(ctx, map[string]any{\"tenant_id\": tenantID})",{"id":1377,"title":1378,"titles":1379,"content":1380,"level":35},"/v1.0.18/cookbook/multi-tenant#schema-level-isolation-postgresql","Schema-Level Isolation (PostgreSQL)",[1306,1368],"func SetSearchPath(db *sqlx.DB, tenantID string) error {\n    schema := fmt.Sprintf(\"tenant_%s\", tenantID)\n    _, err := db.Exec(fmt.Sprintf(\"SET search_path TO %s\", schema))\n    return err\n}\n\n// Usage per request\nfunc TenantDBMiddleware(db *sqlx.DB) func(http.Handler) http.Handler {\n    return func(next http.Handler) http.Handler {\n        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n            tenantID := r.Header.Get(\"X-Tenant-ID\")\n            SetSearchPath(db, tenantID)\n            next.ServeHTTP(w, r)\n        })\n    }\n}",{"id":1382,"title":1383,"titles":1384,"content":29,"level":19},"/v1.0.18/cookbook/multi-tenant#tenant-data-operations","Tenant Data Operations",[1306],{"id":1386,"title":1387,"titles":1388,"content":1389,"level":35},"/v1.0.18/cookbook/multi-tenant#export-tenant-data","Export Tenant Data",[1306,1383],"func ExportTenantData[T any](\n    ctx context.Context,\n    store *TenantStore[T],\n    tenantID string,\n) (map[string]*T, error) {\n    keys, err := store.List(ctx, tenantID, \"\", 0)\n    if err != nil {\n        return nil, err\n    }\n\n    data := make(map[string]*T)\n    for _, key := range keys {\n        val, err := store.Get(ctx, tenantID, key)\n        if err != nil {\n            continue\n        }\n        data[key] = val\n    }\n\n    return data, nil\n}",{"id":1391,"title":1392,"titles":1393,"content":1394,"level":35},"/v1.0.18/cookbook/multi-tenant#delete-tenant-data","Delete Tenant Data",[1306,1383],"func DeleteTenantData[T any](\n    ctx context.Context,\n    store *TenantStore[T],\n    tenantID string,\n) error {\n    for {\n        keys, err := store.List(ctx, tenantID, \"\", 100)\n        if err != nil {\n            return err\n        }\n        if len(keys) == 0 {\n            break\n        }\n\n        for _, key := range keys {\n            if err := store.Delete(ctx, tenantID, key); err != nil {\n                return err\n            }\n        }\n    }\n    return nil\n}",{"id":1396,"title":1397,"titles":1398,"content":1399,"level":19},"/v1.0.18/cookbook/multi-tenant#tenant-quotas","Tenant Quotas",[1306],"Enforce storage limits per tenant. type QuotaEnforcedStore[T any] struct {\n    store    *TenantStore[T]\n    quotas   map[string]int64 // tenantID -> max bytes\n    usage    sync.Map         // tenantID -> current bytes\n}\n\nfunc (q *QuotaEnforcedStore[T]) Set(ctx context.Context, tenantID, key string, val *T, ttl time.Duration) error {\n    // Estimate size (simplified)\n    data, _ := json.Marshal(val)\n    size := int64(len(data))\n\n    current, _ := q.usage.LoadOrStore(tenantID, int64(0))\n    currentUsage := current.(int64)\n\n    quota, ok := q.quotas[tenantID]\n    if ok && currentUsage+size > quota {\n        return fmt.Errorf(\"tenant %s quota exceeded\", tenantID)\n    }\n\n    if err := q.store.Set(ctx, tenantID, key, val, ttl); err != nil {\n        return err\n    }\n\n    q.usage.Store(tenantID, currentUsage+size)\n    return nil\n}",{"id":1401,"title":947,"titles":1402,"content":29,"level":19},"/v1.0.18/cookbook/multi-tenant#best-practices",[1306],{"id":1404,"title":1405,"titles":1406,"content":1407,"level":35},"/v1.0.18/cookbook/multi-tenant#_1-choose-isolation-level-based-on-requirements","1. Choose Isolation Level Based on Requirements",[1306,947],"RequirementStrategyCost-sensitiveKey prefixCompliance (data residency)Separate providersPerformance isolationSeparate storesSimple operationsKey prefix",{"id":1409,"title":1410,"titles":1411,"content":1412,"level":35},"/v1.0.18/cookbook/multi-tenant#_2-always-include-tenant-in-logging","2. Always Include Tenant in Logging",[1306,947],"log.Info(\"operation completed\",\n    \"tenant\", tenantID,\n    \"key\", key,\n    \"duration\", elapsed,\n)",{"id":1414,"title":1415,"titles":1416,"content":1417,"level":35},"/v1.0.18/cookbook/multi-tenant#_3-validate-tenant-access","3. Validate Tenant Access",[1306,947],"func (s *Service) Get(ctx context.Context, key string) (*Data, error) {\n    tenantID, err := tenant.FromContext(ctx)\n    if err != nil {\n        return nil, err\n    }\n\n    // Never trust key from user input\n    if !strings.HasPrefix(key, \"allowed:\") {\n        return nil, errors.New(\"invalid key\")\n    }\n\n    return s.store.Get(ctx, tenantID, key)\n}",{"id":1419,"title":1420,"titles":1421,"content":1422,"level":35},"/v1.0.18/cookbook/multi-tenant#_4-plan-for-tenant-migration","4. Plan for Tenant Migration",[1306,947],"Design so tenant data can be exported/imported: Use consistent key structuresDocument data formatsTest export/import regularly html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .skxcq, html code.shiki .skxcq{--shiki-default:var(--shiki-builtin)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .sfm-E, html code.shiki .sfm-E{--shiki-default:var(--shiki-variable)}",{"id":1424,"title":1425,"titles":1426,"content":1427,"level":9},"/v1.0.18/cookbook/vector-search","Vector Search with PostgreSQL",[],"Using Database[T] with pgvector for similarity search",{"id":1429,"title":1425,"titles":1430,"content":1431,"level":9},"/v1.0.18/cookbook/vector-search#vector-search-with-postgresql",[],"PostgreSQL with the pgvector extension provides vector similarity search directly in your database. Rather than treating vectors as a separate storage concern, grub handles them as typed columns via Database[T].",{"id":1433,"title":1434,"titles":1435,"content":1436,"level":19},"/v1.0.18/cookbook/vector-search#setup","Setup",[1425],"Install the pgvector extension: CREATE EXTENSION IF NOT EXISTS vector; Create a table with a vector column: CREATE TABLE documents (\n    id BIGSERIAL PRIMARY KEY,\n    title TEXT NOT NULL,\n    content TEXT NOT NULL,\n    category TEXT NOT NULL,\n    embedding vector(1536)\n);\n\nCREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);",{"id":1438,"title":1439,"titles":1440,"content":1441,"level":19},"/v1.0.18/cookbook/vector-search#define-your-type","Define Your Type",[1425],"Map the table to a Go struct. The vector column uses a custom type with sql.Scanner and driver.Valuer: type Document struct {\n    ID        int64   `db:\"id\"`\n    Title     string  `db:\"title\"`\n    Content   string  `db:\"content\"`\n    Category  string  `db:\"category\"`\n    Embedding Vector  `db:\"embedding\"`\n}\n\n// Vector wraps []float32 for pgvector compatibility\ntype Vector []float32\n\nfunc (v *Vector) Scan(src any) error {\n    if src == nil {\n        *v = nil\n        return nil\n    }\n    s, ok := src.(string)\n    if !ok {\n        return fmt.Errorf(\"expected string, got %T\", src)\n    }\n    // pgvector format: [1.0,2.0,3.0]\n    var floats []float32\n    if err := json.Unmarshal([]byte(s), &floats); err != nil {\n        return err\n    }\n    *v = floats\n    return nil\n}\n\nfunc (v Vector) Value() (driver.Value, error) {\n    if v == nil {\n        return nil, nil\n    }\n    // Convert to pgvector string format\n    b, err := json.Marshal([]float32(v))\n    if err != nil {\n        return nil, err\n    }\n    return string(b), nil\n}",{"id":1443,"title":1444,"titles":1445,"content":1446,"level":19},"/v1.0.18/cookbook/vector-search#create-the-database","Create the Database",[1425],"import (\n    \"github.com/jmoiron/sqlx\"\n    \"github.com/zoobz-io/astql/postgres\"\n    \"github.com/zoobz-io/grub\"\n    _ \"github.com/lib/pq\"\n)\n\ndb, _ := sqlx.Connect(\"postgres\", \"postgres://user:pass@localhost/mydb?sslmode=disable\")\ndocs, _ := grub.NewDatabase[Document](db, \"documents\", postgres.New())",{"id":1448,"title":1449,"titles":1450,"content":1451,"level":19},"/v1.0.18/cookbook/vector-search#crud-operations","CRUD Operations",[1425],"Standard operations work as expected: // Insert\ndoc := &Document{\n    Title:     \"Introduction to Vectors\",\n    Content:   \"Vectors represent points in high-dimensional space...\",\n    Category:  \"tutorial\",\n    Embedding: embedding, // []float32 from your embedding model\n}\ndocs.Set(ctx, \"1\", doc)\n\n// Retrieve\ndoc, _ := docs.Get(ctx, \"1\")\n\n// Delete\ndocs.Delete(ctx, \"1\")",{"id":1453,"title":1454,"titles":1455,"content":1456,"level":19},"/v1.0.18/cookbook/vector-search#similarity-search","Similarity Search",[1425],"Use the executor's soy interface for vector queries: // Find 10 most similar documents\nresults, err := docs.Executor().Soy().Query().\n    SelectExpr(\"embedding\", \"\u003C=>\", \"query_vec\", \"distance\").\n    OrderByExpr(\"embedding\", \"\u003C=>\", \"query_vec\", \"asc\").\n    Limit(10).\n    Exec(ctx, map[string]any{\n        \"query_vec\": queryEmbedding,\n    })",{"id":1458,"title":1459,"titles":1460,"content":1461,"level":35},"/v1.0.18/cookbook/vector-search#distance-operators","Distance Operators",[1425,1454],"OperatorMetricUse Case\u003C->L2 (Euclidean)General similarity\u003C=>CosineNormalized embeddings\u003C#>Inner ProductWhen vectors are normalized\u003C+>L1 (Manhattan)Sparse vectors",{"id":1463,"title":1464,"titles":1465,"content":1466,"level":35},"/v1.0.18/cookbook/vector-search#filtered-search","Filtered Search",[1425,1454],"Combine vector search with WHERE clauses: results, err := docs.Executor().Soy().Query().\n    SelectExpr(\"embedding\", \"\u003C=>\", \"query_vec\", \"distance\").\n    Where(\"category\", \"=\", \"cat\").\n    OrderByExpr(\"embedding\", \"\u003C=>\", \"query_vec\", \"asc\").\n    Limit(10).\n    Exec(ctx, map[string]any{\n        \"query_vec\": queryEmbedding,\n        \"cat\":       \"tutorial\",\n    })",{"id":1468,"title":1469,"titles":1470,"content":1471,"level":19},"/v1.0.18/cookbook/vector-search#complete-example","Complete Example",[1425],"package main\n\nimport (\n    \"context\"\n    \"database/sql/driver\"\n    \"encoding/json\"\n    \"fmt\"\n    \"log\"\n\n    \"github.com/jmoiron/sqlx\"\n    \"github.com/zoobz-io/astql/postgres\"\n    \"github.com/zoobz-io/grub\"\n    _ \"github.com/lib/pq\"\n)\n\ntype Vector []float32\n\nfunc (v *Vector) Scan(src any) error {\n    if src == nil {\n        *v = nil\n        return nil\n    }\n    s, ok := src.(string)\n    if !ok {\n        return fmt.Errorf(\"expected string, got %T\", src)\n    }\n    var floats []float32\n    if err := json.Unmarshal([]byte(s), &floats); err != nil {\n        return err\n    }\n    *v = floats\n    return nil\n}\n\nfunc (v Vector) Value() (driver.Value, error) {\n    if v == nil {\n        return nil, nil\n    }\n    b, _ := json.Marshal([]float32(v))\n    return string(b), nil\n}\n\ntype Document struct {\n    ID        int64   `db:\"id\"`\n    Title     string  `db:\"title\"`\n    Category  string  `db:\"category\"`\n    Embedding Vector  `db:\"embedding\"`\n}\n\nfunc main() {\n    ctx := context.Background()\n\n    conn, err := sqlx.Connect(\"postgres\", \"postgres://user:pass@localhost/mydb?sslmode=disable\")\n    if err != nil {\n        log.Fatal(err)\n    }\n    defer conn.Close()\n\n    docs := grub.NewDatabase[Document](conn, \"documents\", postgres.New())\n\n    // Insert a document with embedding\n    doc := &Document{\n        Title:     \"Vector Search Guide\",\n        Category:  \"tutorial\",\n        Embedding: Vector{0.1, 0.2, 0.3}, // Your actual embedding\n    }\n    if err := docs.Set(ctx, \"1\", doc); err != nil {\n        log.Fatal(err)\n    }\n\n    // Search for similar documents\n    queryVec := Vector{0.1, 0.2, 0.3}\n    results, err := docs.Executor().Soy().Query().\n        SelectExpr(\"embedding\", \"\u003C=>\", \"query_vec\", \"distance\").\n        Where(\"category\", \"=\", \"cat\").\n        OrderByExpr(\"embedding\", \"\u003C=>\", \"query_vec\", \"asc\").\n        Limit(5).\n        Exec(ctx, map[string]any{\n            \"query_vec\": queryVec,\n            \"cat\":       \"tutorial\",\n        })\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    for _, r := range results {\n        fmt.Printf(\"ID: %d, Title: %s\\n\", r.ID, r.Title)\n    }\n}",{"id":1473,"title":1474,"titles":1475,"content":1476,"level":19},"/v1.0.18/cookbook/vector-search#why-databaset-instead-of-vectorprovider","Why DatabaseT Instead of VectorProvider?",[1425],"PostgreSQL with pgvector is fundamentally a relational database with vector support, not a dedicated vector store. Using Database[T]: Type-safe columns — Vector is one column among many typed fieldsFull SQL capabilities — Joins, CTEs, transactions, constraintsNo serialization overhead — No marshal/unmarshal at provider boundaryExisting infrastructure — Use your PostgreSQL instance directly For dedicated vector databases (Pinecone, Qdrant, Milvus, Weaviate), use Index[T] with their respective providers. html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .suWN2, html code.shiki .suWN2{--shiki-default:var(--shiki-tag)}",{"id":1478,"title":1479,"titles":1480,"content":1481,"level":9},"/v1.0.18/reference/api","API Reference",[],"Complete API documentation for grub",{"id":1483,"title":1479,"titles":1484,"content":1485,"level":9},"/v1.0.18/reference/api#api-reference",[],"Complete documentation for grub's public API.",{"id":1487,"title":1488,"titles":1489,"content":1490,"level":19},"/v1.0.18/reference/api#package-grub","Package grub",[1479],"import \"github.com/zoobz-io/grub\"",{"id":1492,"title":1493,"titles":1494,"content":1495,"level":19},"/v1.0.18/reference/api#errors","Errors",[1479],"Semantic errors returned by all providers. ErrorDescriptionErrNotFoundRecord does not existErrDuplicateRecord with same key already existsErrConflictConcurrent modification conflictErrConstraintConstraint violation (FK, check, etc.)ErrInvalidKeyKey is malformed or emptyErrReadOnlyWrite attempted on read-only connectionErrTableExistsTable name already registeredErrTableNotFoundTable not registeredErrTTLNotSupportedProvider doesn't support TTLErrDimensionMismatchVector dimension doesn't match indexErrInvalidVectorVector is malformed (nil, empty, NaN)ErrIndexNotReadyIndex not loaded or initializedErrInvalidQueryFilter contains validation errorsErrOperatorNotSupportedProvider doesn't support filter operatorErrNoPrimaryKeyNo field has constraints:\"primarykey\" tagErrMultiplePrimaryKeysMultiple fields have primarykey constraint if errors.Is(err, grub.ErrNotFound) {\n    // Handle missing record\n}",{"id":1497,"title":306,"titles":1498,"content":1499,"level":19},"/v1.0.18/reference/api#storet",[1479],"Type-safe key-value store wrapper.",{"id":1501,"title":1502,"titles":1503,"content":1504,"level":35},"/v1.0.18/reference/api#newstore","NewStore",[1479,306],"func NewStore[T any](provider StoreProvider) *Store[T] Creates a new Store with JSON codec. store := grub.NewStore[Session](redis.New(client))",{"id":1506,"title":1507,"titles":1508,"content":1509,"level":35},"/v1.0.18/reference/api#newstorewithcodec","NewStoreWithCodec",[1479,306],"func NewStoreWithCodec[T any](provider StoreProvider, codec Codec) *Store[T] Creates a new Store with custom codec. store := grub.NewStoreWithCodec[Config](provider, grub.GobCodec{})",{"id":1511,"title":1512,"titles":1513,"content":29,"level":35},"/v1.0.18/reference/api#methods","Methods",[1479,306],{"id":1515,"title":571,"titles":1516,"content":1517,"level":1518},"/v1.0.18/reference/api#get",[1479,306,1512],"func (s *Store[T]) Get(ctx context.Context, key string) (*T, error) Retrieves value by key. Returns ErrNotFound if key doesn't exist. session, err := store.Get(ctx, \"session:abc123\")",4,{"id":1520,"title":576,"titles":1521,"content":1522,"level":1518},"/v1.0.18/reference/api#set",[1479,306,1512],"func (s *Store[T]) Set(ctx context.Context, key string, value *T, ttl time.Duration) error Stores value with optional TTL. TTL=0 means no expiration. store.Set(ctx, \"session:abc123\", &session, 24*time.Hour)\nstore.Set(ctx, \"config:app\", &config, 0) // No expiration",{"id":1524,"title":581,"titles":1525,"content":1526,"level":1518},"/v1.0.18/reference/api#delete",[1479,306,1512],"func (s *Store[T]) Delete(ctx context.Context, key string) error Removes key. Returns ErrNotFound if key doesn't exist. err := store.Delete(ctx, \"session:abc123\")",{"id":1528,"title":586,"titles":1529,"content":1530,"level":1518},"/v1.0.18/reference/api#exists",[1479,306,1512],"func (s *Store[T]) Exists(ctx context.Context, key string) (bool, error) Checks if key exists without loading value. exists, err := store.Exists(ctx, \"session:abc123\")",{"id":1532,"title":591,"titles":1533,"content":1534,"level":1518},"/v1.0.18/reference/api#list",[1479,306,1512],"func (s *Store[T]) List(ctx context.Context, prefix string, limit int) ([]string, error) Lists keys matching prefix. Limit=0 means no limit. keys, err := store.List(ctx, \"session:\", 100)\nkeys, err := store.List(ctx, \"\", 0) // All keys",{"id":1536,"title":596,"titles":1537,"content":1538,"level":1518},"/v1.0.18/reference/api#getbatch",[1479,306,1512],"func (s *Store[T]) GetBatch(ctx context.Context, keys []string) (map[string]*T, error) Retrieves multiple keys. Missing keys are omitted from result (no error). users, err := store.GetBatch(ctx, []string{\"user:1\", \"user:2\"})",{"id":1540,"title":601,"titles":1541,"content":1542,"level":1518},"/v1.0.18/reference/api#setbatch",[1479,306,1512],"func (s *Store[T]) SetBatch(ctx context.Context, items map[string]*T, ttl time.Duration) error Stores multiple values with same TTL. items := map[string]*User{\"user:1\": &alice, \"user:2\": &bob}\nerr := store.SetBatch(ctx, items, time.Hour)",{"id":1544,"title":1545,"titles":1546,"content":1547,"level":1518},"/v1.0.18/reference/api#atomic","Atomic",[1479,306,1512],"func (s *Store[T]) Atomic() *atomic.Store[T] Returns atomic view for field-level access. Lazily initialized, cached. Panics if T is not atomizable. atomicStore := store.Atomic()\natom, err := atomicStore.Get(ctx, \"key\")",{"id":1549,"title":1550,"titles":1551,"content":1552,"level":19},"/v1.0.18/reference/api#buckett","BucketT",[1479],"Type-safe blob storage wrapper.",{"id":1554,"title":1555,"titles":1556,"content":1557,"level":35},"/v1.0.18/reference/api#newbucket","NewBucket",[1479,1550],"func NewBucket[T any](provider BucketProvider) *Bucket[T] Creates a new Bucket with JSON codec. bucket := grub.NewBucket[Document](s3.New(client, \"my-bucket\"))",{"id":1559,"title":1560,"titles":1561,"content":1562,"level":35},"/v1.0.18/reference/api#newbucketwithcodec","NewBucketWithCodec",[1479,1550],"func NewBucketWithCodec[T any](provider BucketProvider, codec Codec) *Bucket[T] Creates a new Bucket with custom codec.",{"id":1564,"title":1512,"titles":1565,"content":29,"level":35},"/v1.0.18/reference/api#methods-1",[1479,1550],{"id":1567,"title":571,"titles":1568,"content":1569,"level":1518},"/v1.0.18/reference/api#get-1",[1479,1550,1512],"func (b *Bucket[T]) Get(ctx context.Context, key string) (*Object[T], error) Retrieves object with metadata and payload. Returns ErrNotFound if missing. obj, err := bucket.Get(ctx, \"docs/report.json\")\nfmt.Println(obj.Data.Title)\nfmt.Println(obj.ContentType)",{"id":1571,"title":614,"titles":1572,"content":1573,"level":1518},"/v1.0.18/reference/api#put",[1479,1550,1512],"func (b *Bucket[T]) Put(ctx context.Context, obj *Object[T]) error Stores object with metadata. err := bucket.Put(ctx, &grub.Object[Document]{\n    Key:         \"docs/report.json\",\n    ContentType: \"application/json\",\n    Metadata:    map[string]string{\"author\": \"alice\"},\n    Data:        Document{Title: \"Report\"},\n})",{"id":1575,"title":581,"titles":1576,"content":1577,"level":1518},"/v1.0.18/reference/api#delete-1",[1479,1550,1512],"func (b *Bucket[T]) Delete(ctx context.Context, key string) error Removes object. Returns ErrNotFound if missing.",{"id":1579,"title":586,"titles":1580,"content":1581,"level":1518},"/v1.0.18/reference/api#exists-1",[1479,1550,1512],"func (b *Bucket[T]) Exists(ctx context.Context, key string) (bool, error) Checks if object exists.",{"id":1583,"title":591,"titles":1584,"content":1585,"level":1518},"/v1.0.18/reference/api#list-1",[1479,1550,1512],"func (b *Bucket[T]) List(ctx context.Context, prefix string, limit int) ([]ObjectInfo, error) Lists objects matching prefix. Returns metadata only (no payload). infos, err := bucket.List(ctx, \"docs/\", 100)\nfor _, info := range infos {\n    fmt.Printf(\"%s (%d bytes)\\n\", info.Key, info.Size)\n}",{"id":1587,"title":1545,"titles":1588,"content":1589,"level":1518},"/v1.0.18/reference/api#atomic-1",[1479,1550,1512],"func (b *Bucket[T]) Atomic() *atomic.Bucket[T] Returns atomic view. Panics if T is not atomizable.",{"id":1591,"title":1592,"titles":1593,"content":1594,"level":19},"/v1.0.18/reference/api#databaset","DatabaseT",[1479],"Type-safe SQL database wrapper.",{"id":1596,"title":1597,"titles":1598,"content":1599,"level":35},"/v1.0.18/reference/api#newdatabase","NewDatabase",[1479,1592],"func NewDatabase[T any](\n    db *sqlx.DB,\n    table string,\n    renderer astql.Renderer,\n) (*Database[T], error) Creates a new Database wrapper. db: Database connectiontable: Table namerenderer: SQL dialect renderer (postgres.New(), mariadb.New(), etc.) The primary key column is automatically derived from struct tags. Mark the primary key field with constraints:\"primarykey\": type User struct {\n    ID    int    `db:\"id\" constraints:\"primarykey\"`\n    Email string `db:\"email\" constraints:\"notnull,unique\"`\n    Name  string `db:\"name\"`\n}\n\ndb := grub.NewDatabase[User](sqlxDB, \"users\", sqlite.New()) Panics if no field has the primarykey constraint (ErrNoPrimaryKey) or if multiple fields have the constraint (ErrMultiplePrimaryKeys). These are programmer errors — same category as the existing panic in Atomic(). Use the *Tx method variants (GetTx, SetTx, etc.) for transaction support.",{"id":1601,"title":1512,"titles":1602,"content":29,"level":35},"/v1.0.18/reference/api#methods-2",[1479,1592],{"id":1604,"title":571,"titles":1605,"content":1606,"level":1518},"/v1.0.18/reference/api#get-2",[1479,1592,1512],"func (d *Database[T]) Get(ctx context.Context, key string) (*T, error) Retrieves record by primary key. Returns ErrNotFound if missing. user, err := db.Get(ctx, \"123\")",{"id":1608,"title":576,"titles":1609,"content":1610,"level":1518},"/v1.0.18/reference/api#set-1",[1479,1592,1512],"func (d *Database[T]) Set(ctx context.Context, key string, value *T) error Upserts record (insert or update on conflict). err := db.Set(ctx, \"123\", &User{ID: \"123\", Name: \"Alice\"})",{"id":1612,"title":581,"titles":1613,"content":1614,"level":1518},"/v1.0.18/reference/api#delete-2",[1479,1592,1512],"func (d *Database[T]) Delete(ctx context.Context, key string) error Removes record. Returns ErrNotFound if missing.",{"id":1616,"title":586,"titles":1617,"content":1618,"level":1518},"/v1.0.18/reference/api#exists-2",[1479,1592,1512],"func (d *Database[T]) Exists(ctx context.Context, key string) (bool, error) Checks if record exists.",{"id":1620,"title":1621,"titles":1622,"content":1623,"level":35},"/v1.0.18/reference/api#query-builders","Query Builders",[1479,1592],"Direct access to soy query builders for ad-hoc queries.",{"id":1625,"title":1626,"titles":1627,"content":1628,"level":1518},"/v1.0.18/reference/api#query","Query",[1479,1592,1621],"func (d *Database[T]) Query() *soy.Query[T] Returns a query builder for fetching multiple records. users, err := db.Query().\n    Where(\"age\", \">=\", \"min_age\").\n    OrderBy(\"name\", \"ASC\").\n    Limit(10).\n    Exec(ctx, map[string]any{\"min_age\": 18})",{"id":1630,"title":1631,"titles":1632,"content":1633,"level":1518},"/v1.0.18/reference/api#select","Select",[1479,1592,1621],"func (d *Database[T]) Select() *soy.Select[T] Returns a select builder for fetching a single record. user, err := db.Select().\n    Where(\"email\", \"=\", \"email\").\n    Exec(ctx, map[string]any{\"email\": \"alice@example.com\"})",{"id":1635,"title":1636,"titles":1637,"content":1638,"level":1518},"/v1.0.18/reference/api#insert","Insert",[1479,1592,1621],"func (d *Database[T]) Insert() *soy.Create[T] Returns an insert builder (auto-generates PK). user, err := db.Insert().Exec(ctx, &User{Name: \"Alice\", Email: \"alice@example.com\"})",{"id":1640,"title":1641,"titles":1642,"content":1643,"level":1518},"/v1.0.18/reference/api#insertfull","InsertFull",[1479,1592,1621],"func (d *Database[T]) InsertFull() *soy.Create[T] Returns an insert builder that includes the PK field. user, err := db.InsertFull().Exec(ctx, &User{ID: 123, Name: \"Alice\"})",{"id":1645,"title":1646,"titles":1647,"content":1648,"level":1518},"/v1.0.18/reference/api#modify","Modify",[1479,1592,1621],"func (d *Database[T]) Modify() *soy.Update[T] Returns an update builder. updated, err := db.Modify().\n    Set(\"name\", \"new_name\").\n    Where(\"id\", \"=\", \"user_id\").\n    Exec(ctx, map[string]any{\"new_name\": \"Bob\", \"user_id\": 123})",{"id":1650,"title":1651,"titles":1652,"content":1653,"level":1518},"/v1.0.18/reference/api#remove","Remove",[1479,1592,1621],"func (d *Database[T]) Remove() *soy.Delete[T] Returns a delete builder. affected, err := db.Remove().\n    Where(\"status\", \"=\", \"inactive\").\n    Exec(ctx, map[string]any{\"inactive\": \"deleted\"})",{"id":1655,"title":1656,"titles":1657,"content":1658,"level":1518},"/v1.0.18/reference/api#count","Count",[1479,1592,1621],"func (d *Database[T]) Count() *soy.Aggregate[T] Returns an aggregate builder for counting records. // Count all records\ncount, err := db.Count().Exec(ctx, nil)\n\n// Count with condition\ncount, err := db.Count().\n    Where(\"status\", \"=\", \"active\").\n    Exec(ctx, map[string]any{\"active\": \"enabled\"})\n\n// Count in transaction\ncount, err := db.Count().ExecTx(ctx, tx, nil)",{"id":1660,"title":666,"titles":1661,"content":1662,"level":35},"/v1.0.18/reference/api#statement-execution",[1479,1592],"Execute pre-defined edamame statements.",{"id":1664,"title":1665,"titles":1666,"content":1667,"level":1518},"/v1.0.18/reference/api#execquery","ExecQuery",[1479,1592,666],"func (d *Database[T]) ExecQuery(ctx context.Context, stmt edamame.QueryStatement, params map[string]any) ([]*T, error) Executes a query statement returning multiple records. users, err := db.ExecQuery(ctx, grub.QueryAll, nil)\nusers, err := db.ExecQuery(ctx, byRoleStmt, map[string]any{\"role\": \"admin\"})",{"id":1669,"title":1670,"titles":1671,"content":1672,"level":1518},"/v1.0.18/reference/api#execselect","ExecSelect",[1479,1592,666],"func (d *Database[T]) ExecSelect(ctx context.Context, stmt edamame.SelectStatement, params map[string]any) (*T, error) Executes a select statement returning a single record. user, err := db.ExecSelect(ctx, byEmailStmt, map[string]any{\"email\": \"alice@example.com\"})",{"id":1674,"title":1675,"titles":1676,"content":1677,"level":1518},"/v1.0.18/reference/api#execupdate","ExecUpdate",[1479,1592,666],"func (d *Database[T]) ExecUpdate(ctx context.Context, stmt edamame.UpdateStatement, params map[string]any) (*T, error) Executes an update statement returning the modified record.",{"id":1679,"title":1680,"titles":1681,"content":1682,"level":1518},"/v1.0.18/reference/api#execaggregate","ExecAggregate",[1479,1592,666],"func (d *Database[T]) ExecAggregate(ctx context.Context, stmt edamame.AggregateStatement, params map[string]any) (float64, error) Executes an aggregate statement. count, err := db.ExecAggregate(ctx, grub.CountAll, nil)",{"id":1684,"title":1685,"titles":1686,"content":1687,"level":35},"/v1.0.18/reference/api#transaction-methods","Transaction Methods",[1479,1592],"All operations have *Tx variants that accept a transaction as the second parameter.",{"id":1689,"title":1690,"titles":1691,"content":1692,"level":1518},"/v1.0.18/reference/api#gettx","GetTx",[1479,1592,1685],"func (d *Database[T]) GetTx(ctx context.Context, tx *sqlx.Tx, key string) (*T, error)",{"id":1694,"title":1695,"titles":1696,"content":1697,"level":1518},"/v1.0.18/reference/api#settx","SetTx",[1479,1592,1685],"func (d *Database[T]) SetTx(ctx context.Context, tx *sqlx.Tx, key string, value *T) error",{"id":1699,"title":1700,"titles":1701,"content":1702,"level":1518},"/v1.0.18/reference/api#deletetx","DeleteTx",[1479,1592,1685],"func (d *Database[T]) DeleteTx(ctx context.Context, tx *sqlx.Tx, key string) error",{"id":1704,"title":1705,"titles":1706,"content":1707,"level":1518},"/v1.0.18/reference/api#existstx","ExistsTx",[1479,1592,1685],"func (d *Database[T]) ExistsTx(ctx context.Context, tx *sqlx.Tx, key string) (bool, error)",{"id":1709,"title":1710,"titles":1711,"content":1712,"level":1518},"/v1.0.18/reference/api#execquerytx","ExecQueryTx",[1479,1592,1685],"func (d *Database[T]) ExecQueryTx(ctx context.Context, tx *sqlx.Tx, stmt edamame.QueryStatement, params map[string]any) ([]*T, error)",{"id":1714,"title":1715,"titles":1716,"content":1717,"level":1518},"/v1.0.18/reference/api#execselecttx","ExecSelectTx",[1479,1592,1685],"func (d *Database[T]) ExecSelectTx(ctx context.Context, tx *sqlx.Tx, stmt edamame.SelectStatement, params map[string]any) (*T, error)",{"id":1719,"title":1720,"titles":1721,"content":1722,"level":1518},"/v1.0.18/reference/api#execupdatetx","ExecUpdateTx",[1479,1592,1685],"func (d *Database[T]) ExecUpdateTx(ctx context.Context, tx *sqlx.Tx, stmt edamame.UpdateStatement, params map[string]any) (*T, error)",{"id":1724,"title":1725,"titles":1726,"content":1727,"level":1518},"/v1.0.18/reference/api#execaggregatetx","ExecAggregateTx",[1479,1592,1685],"func (d *Database[T]) ExecAggregateTx(ctx context.Context, tx *sqlx.Tx, stmt edamame.AggregateStatement, params map[string]any) (float64, error)",{"id":1729,"title":1730,"titles":1731,"content":1732,"level":1518},"/v1.0.18/reference/api#usage-example","Usage Example",[1479,1592,1685],"tx, err := sqlxDB.BeginTxx(ctx, nil)\nif err != nil {\n    return err\n}\ndefer tx.Rollback()\n\nuser, err := db.GetTx(ctx, tx, \"123\")\nif err != nil {\n    return err\n}\n\nuser.Name = \"Updated\"\nerr = db.SetTx(ctx, tx, \"123\", user)\nif err != nil {\n    return err\n}\n\nreturn tx.Commit()",{"id":1734,"title":1735,"titles":1736,"content":1737,"level":1518},"/v1.0.18/reference/api#executor","Executor",[1479,1592,1685],"func (d *Database[T]) Executor() *edamame.Executor[T] Returns the underlying edamame Executor for advanced query operations.",{"id":1739,"title":1545,"titles":1740,"content":1741,"level":1518},"/v1.0.18/reference/api#atomic-2",[1479,1592,1685],"func (d *Database[T]) Atomic() AtomicDatabase Returns atomic view. Panics if T is not atomizable.",{"id":1743,"title":1744,"titles":1745,"content":1746,"level":19},"/v1.0.18/reference/api#indext","IndexT",[1479],"Type-safe vector storage wrapper.",{"id":1748,"title":1749,"titles":1750,"content":1751,"level":35},"/v1.0.18/reference/api#newindex","NewIndex",[1479,1744],"func NewIndex[T any](provider VectorProvider) *Index[T] Creates a new Index with JSON codec. index := grub.NewIndex[Embedding](qdrant.New(client, qdrant.Config{\n    Collection: \"documents\",\n}))",{"id":1753,"title":1754,"titles":1755,"content":1756,"level":35},"/v1.0.18/reference/api#newindexwithcodec","NewIndexWithCodec",[1479,1744],"func NewIndexWithCodec[T any](provider VectorProvider, codec Codec) *Index[T] Creates a new Index with custom codec.",{"id":1758,"title":1512,"titles":1759,"content":29,"level":35},"/v1.0.18/reference/api#methods-3",[1479,1744],{"id":1761,"title":1762,"titles":1763,"content":1764,"level":1518},"/v1.0.18/reference/api#upsert","Upsert",[1479,1744,1512],"func (i *Index[T]) Upsert(ctx context.Context, id string, vector []float32, metadata *T) error Stores or updates a vector with associated metadata. If the ID exists, the vector and metadata are replaced. index.Upsert(ctx, \"doc:1\", embedding, &Embedding{Category: \"tech\"})",{"id":1766,"title":1767,"titles":1768,"content":1769,"level":1518},"/v1.0.18/reference/api#upsertbatch","UpsertBatch",[1479,1744,1512],"func (i *Index[T]) UpsertBatch(ctx context.Context, vectors []Vector[T]) error Stores or updates multiple vectors. vectors := []grub.Vector[Embedding]{\n    {ID: \"doc:1\", Vector: vec1, Metadata: Embedding{Category: \"tech\"}},\n    {ID: \"doc:2\", Vector: vec2, Metadata: Embedding{Category: \"science\"}},\n}\nerr := index.UpsertBatch(ctx, vectors)",{"id":1771,"title":571,"titles":1772,"content":1773,"level":1518},"/v1.0.18/reference/api#get-3",[1479,1744,1512],"func (i *Index[T]) Get(ctx context.Context, id string) (*Vector[T], error) Retrieves a vector by ID. Returns ErrNotFound if the ID does not exist. result, err := index.Get(ctx, \"doc:1\")",{"id":1775,"title":581,"titles":1776,"content":1777,"level":1518},"/v1.0.18/reference/api#delete-3",[1479,1744,1512],"func (i *Index[T]) Delete(ctx context.Context, id string) error Removes a vector by ID. Returns ErrNotFound if the ID does not exist.",{"id":1779,"title":1780,"titles":1781,"content":1782,"level":1518},"/v1.0.18/reference/api#deletebatch","DeleteBatch",[1479,1744,1512],"func (i *Index[T]) DeleteBatch(ctx context.Context, ids []string) error Removes multiple vectors by ID. Non-existent IDs are silently ignored.",{"id":1784,"title":1785,"titles":1786,"content":1787,"level":1518},"/v1.0.18/reference/api#search","Search",[1479,1744,1512],"func (i *Index[T]) Search(ctx context.Context, vector []float32, k int, filter *T) ([]*Vector[T], error) Performs similarity search and returns the k nearest neighbors. Filter is optional metadata filtering (nil means no filter). results, err := index.Search(ctx, queryVector, 10, nil)\nresults, err := index.Search(ctx, queryVector, 10, &Embedding{Category: \"tech\"})",{"id":1789,"title":1626,"titles":1790,"content":1791,"level":1518},"/v1.0.18/reference/api#query-1",[1479,1744,1512],"func (i *Index[T]) Query(ctx context.Context, vector []float32, k int, filter *vecna.Filter) ([]*Vector[T], error) Performs similarity search with vecna filter support. Returns ErrInvalidQuery if the filter contains validation errors. Returns ErrOperatorNotSupported if the provider doesn't support an operator. filter := vecna.And(\n    vecna.Eq(\"category\", \"tech\"),\n    vecna.Gte(\"score\", 0.8),\n)\nresults, err := index.Query(ctx, queryVector, 10, filter)",{"id":1793,"title":1794,"titles":1795,"content":1796,"level":1518},"/v1.0.18/reference/api#filter","Filter",[1479,1744,1512],"func (i *Index[T]) Filter(ctx context.Context, filter *vecna.Filter, limit int) ([]*Vector[T], error) Returns vectors matching the metadata filter without similarity search. Result ordering is provider-dependent and not guaranteed by the interface. Limit of 0 returns all matching vectors. Returns ErrFilterNotSupported if the provider cannot perform metadata-only filtering (e.g., Pinecone). filter := vecna.Eq(\"category\", \"tech\")\nresults, err := index.Filter(ctx, filter, 100)\n\n// Nil filter returns all vectors\nall, err := index.Filter(ctx, nil, 0)",{"id":1798,"title":591,"titles":1799,"content":1800,"level":1518},"/v1.0.18/reference/api#list-2",[1479,1744,1512],"func (i *Index[T]) List(ctx context.Context, prefix string, limit int) ([]string, error) Returns vector IDs matching the optional prefix. Limit of 0 means no limit.",{"id":1802,"title":586,"titles":1803,"content":1804,"level":1518},"/v1.0.18/reference/api#exists-3",[1479,1744,1512],"func (i *Index[T]) Exists(ctx context.Context, id string) (bool, error) Checks whether a vector ID exists.",{"id":1806,"title":1545,"titles":1807,"content":1808,"level":1518},"/v1.0.18/reference/api#atomic-3",[1479,1744,1512],"func (i *Index[T]) Atomic() *atomic.Index[T] Returns atomic view for field-level access. Lazily initialized, cached. Panics if T is not atomizable.",{"id":1810,"title":1811,"titles":1812,"content":29,"level":19},"/v1.0.18/reference/api#types","Types",[1479],{"id":1814,"title":1815,"titles":1816,"content":1817,"level":35},"/v1.0.18/reference/api#objectt","ObjectT",[1479,1811],"Blob object with metadata and typed payload. type Object[T any] struct {\n    Key         string            `json:\"key\"`\n    ContentType string            `json:\"content_type\"`\n    Size        int64             `json:\"size\"`\n    ETag        string            `json:\"etag,omitempty\"`\n    Metadata    map[string]string `json:\"metadata,omitempty\"`\n    Data        T                 `json:\"data\"`\n}",{"id":1819,"title":1820,"titles":1821,"content":1822,"level":35},"/v1.0.18/reference/api#objectinfo","ObjectInfo",[1479,1811],"Blob metadata without payload (returned by List). type ObjectInfo struct {\n    Key         string\n    ContentType string\n    Size        int64\n    ETag        string\n    Metadata    map[string]string\n}",{"id":1824,"title":1825,"titles":1826,"content":1827,"level":35},"/v1.0.18/reference/api#vectort","VectorT",[1479,1811],"Vector with typed metadata payload. type Vector[T any] struct {\n    ID       string    `json:\"id\"`\n    Vector   []float32 `json:\"vector\"`\n    Score    float32   `json:\"score,omitempty\"`\n    Metadata T         `json:\"metadata\"`\n}",{"id":1829,"title":1830,"titles":1831,"content":1832,"level":35},"/v1.0.18/reference/api#vectorinfo","VectorInfo",[1479,1811],"Vector metadata returned by providers. type VectorInfo struct {\n    ID        string\n    Dimension int\n    Score     float32\n    Metadata  map[string]any\n}",{"id":1834,"title":1835,"titles":1836,"content":1837,"level":35},"/v1.0.18/reference/api#vectorrecord","VectorRecord",[1479,1811],"Batch operation format for vectors. type VectorRecord struct {\n    ID       string\n    Vector   []float32\n    Metadata map[string]any\n}",{"id":1839,"title":1840,"titles":1841,"content":1842,"level":35},"/v1.0.18/reference/api#vectorresult","VectorResult",[1479,1811],"Search result returned by providers. type VectorResult struct {\n    ID       string\n    Vector   []float32\n    Metadata map[string]any\n    Score    float32\n}",{"id":1844,"title":1845,"titles":1846,"content":1847,"level":35},"/v1.0.18/reference/api#atomicvector","AtomicVector",[1479,1811],"Vector with atomized metadata payload. type AtomicVector struct {\n    ID       string\n    Vector   []float32\n    Score    float32\n    Metadata *atom.Atom\n}",{"id":1849,"title":1850,"titles":1851,"content":29,"level":19},"/v1.0.18/reference/api#interfaces","Interfaces",[1479],{"id":1853,"title":1854,"titles":1855,"content":1856,"level":35},"/v1.0.18/reference/api#storeprovider","StoreProvider",[1479,1850],"Raw key-value storage interface. type StoreProvider interface {\n    Get(ctx context.Context, key string) ([]byte, error)\n    Set(ctx context.Context, key string, value []byte, ttl time.Duration) error\n    Delete(ctx context.Context, key string) error\n    Exists(ctx context.Context, key string) (bool, error)\n    List(ctx context.Context, prefix string, limit int) ([]string, error)\n    GetBatch(ctx context.Context, keys []string) (map[string][]byte, error)\n    SetBatch(ctx context.Context, items map[string][]byte, ttl time.Duration) error\n}",{"id":1858,"title":1859,"titles":1860,"content":1861,"level":35},"/v1.0.18/reference/api#bucketprovider","BucketProvider",[1479,1850],"Raw blob storage interface. type BucketProvider interface {\n    Get(ctx context.Context, key string) ([]byte, *ObjectInfo, error)\n    Put(ctx context.Context, key string, data []byte, info *ObjectInfo) error\n    Delete(ctx context.Context, key string) error\n    Exists(ctx context.Context, key string) (bool, error)\n    List(ctx context.Context, prefix string, limit int) ([]ObjectInfo, error)\n}",{"id":1863,"title":1864,"titles":1865,"content":1866,"level":35},"/v1.0.18/reference/api#vectorprovider","VectorProvider",[1479,1850],"Raw vector storage interface. type VectorProvider interface {\n    Upsert(ctx context.Context, id string, vector []float32, metadata map[string]any) error\n    UpsertBatch(ctx context.Context, vectors []VectorRecord) error\n    Get(ctx context.Context, id string) ([]float32, *VectorInfo, error)\n    Delete(ctx context.Context, id string) error\n    DeleteBatch(ctx context.Context, ids []string) error\n    Search(ctx context.Context, vector []float32, k int, filter map[string]any) ([]VectorResult, error)\n    Query(ctx context.Context, vector []float32, k int, filter *vecna.Filter) ([]VectorResult, error)\n    Filter(ctx context.Context, filter *vecna.Filter, limit int) ([]VectorResult, error)\n    List(ctx context.Context, prefix string, limit int) ([]string, error)\n    Exists(ctx context.Context, id string) (bool, error)\n}",{"id":1868,"title":1869,"titles":1870,"content":1871,"level":35},"/v1.0.18/reference/api#beforesave","BeforeSave",[1479,1850],"Called before persisting T. Return an error to abort the write. type BeforeSave interface {\n    BeforeSave(ctx context.Context) error\n}",{"id":1873,"title":1874,"titles":1875,"content":1876,"level":35},"/v1.0.18/reference/api#aftersave","AfterSave",[1479,1850],"Called after T has been successfully persisted. type AfterSave interface {\n    AfterSave(ctx context.Context) error\n}",{"id":1878,"title":1879,"titles":1880,"content":1881,"level":35},"/v1.0.18/reference/api#afterload","AfterLoad",[1479,1850],"Called after T has been loaded and decoded. type AfterLoad interface {\n    AfterLoad(ctx context.Context) error\n}",{"id":1883,"title":1884,"titles":1885,"content":1886,"level":35},"/v1.0.18/reference/api#beforedelete","BeforeDelete",[1479,1850],"Called before deleting a record. Invoked on a zero-value T (no loaded state). type BeforeDelete interface {\n    BeforeDelete(ctx context.Context) error\n}",{"id":1888,"title":1889,"titles":1890,"content":1891,"level":35},"/v1.0.18/reference/api#afterdelete","AfterDelete",[1479,1850],"Called after a record has been successfully deleted. Invoked on a zero-value T (no loaded state). type AfterDelete interface {\n    AfterDelete(ctx context.Context) error\n}",{"id":1893,"title":1894,"titles":1895,"content":1896,"level":35},"/v1.0.18/reference/api#codec","Codec",[1479,1850],"Serialization interface. type Codec interface {\n    Encode(v any) ([]byte, error)\n    Decode(data []byte, v any) error\n}",{"id":1898,"title":1899,"titles":1900,"content":1901,"level":35},"/v1.0.18/reference/api#atomicstore","AtomicStore",[1479,1850],"Type-agnostic key-value access. type AtomicStore interface {\n    Spec() atom.Spec\n    Get(ctx context.Context, key string) (*atom.Atom, error)\n    Set(ctx context.Context, key string, a *atom.Atom, ttl time.Duration) error\n    Delete(ctx context.Context, key string) error\n    Exists(ctx context.Context, key string) (bool, error)\n}",{"id":1903,"title":1904,"titles":1905,"content":1906,"level":35},"/v1.0.18/reference/api#atomicbucket","AtomicBucket",[1479,1850],"Type-agnostic blob access. type AtomicBucket interface {\n    Spec() atom.Spec\n    Get(ctx context.Context, key string) (*AtomicObject, error)\n    Put(ctx context.Context, key string, obj *AtomicObject) error\n    Delete(ctx context.Context, key string) error\n    Exists(ctx context.Context, key string) (bool, error)\n}",{"id":1908,"title":1909,"titles":1910,"content":1911,"level":35},"/v1.0.18/reference/api#atomicdatabase","AtomicDatabase",[1479,1850],"Type-agnostic SQL access. type AtomicDatabase interface {\n    Table() string\n    Spec() atom.Spec\n    Get(ctx context.Context, key string) (*atom.Atom, error)\n    Set(ctx context.Context, key string, a *atom.Atom) error\n    Delete(ctx context.Context, key string) error\n    Exists(ctx context.Context, key string) (bool, error)\n    ExecQuery(ctx context.Context, stmt edamame.QueryStatement, params map[string]any) ([]*atom.Atom, error)\n    ExecSelect(ctx context.Context, stmt edamame.SelectStatement, params map[string]any) (*atom.Atom, error)\n}",{"id":1913,"title":1914,"titles":1915,"content":1916,"level":35},"/v1.0.18/reference/api#atomicindex","AtomicIndex",[1479,1850],"Type-agnostic vector access. type AtomicIndex interface {\n    Spec() atom.Spec\n    Get(ctx context.Context, id string) (*AtomicVector, error)\n    Upsert(ctx context.Context, id string, vector []float32, metadata *atom.Atom) error\n    Delete(ctx context.Context, id string) error\n    Exists(ctx context.Context, id string) (bool, error)\n    Search(ctx context.Context, vector []float32, k int, filter *atom.Atom) ([]AtomicVector, error)\n    Query(ctx context.Context, vector []float32, k int, filter *vecna.Filter) ([]AtomicVector, error)\n    Filter(ctx context.Context, filter *vecna.Filter, limit int) ([]AtomicVector, error)\n}",{"id":1918,"title":259,"titles":1919,"content":29,"level":19},"/v1.0.18/reference/api#codecs",[1479],{"id":1921,"title":1922,"titles":1923,"content":1924,"level":35},"/v1.0.18/reference/api#jsoncodec","JSONCodec",[1479,259],"Default codec using encoding/json. type JSONCodec struct{}",{"id":1926,"title":1927,"titles":1928,"content":1929,"level":35},"/v1.0.18/reference/api#gobcodec","GobCodec",[1479,259],"Binary codec using encoding/gob. type GobCodec struct{}",{"id":1931,"title":244,"titles":1932,"content":1933,"level":35},"/v1.0.18/reference/api#usage",[1479,259],"// Default (JSON)\nstore := grub.NewStore[Config](provider)\n\n// Custom codec\nstore := grub.NewStoreWithCodec[Config](provider, grub.GobCodec{}) html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html pre.shiki code .suWN2, html code.shiki .suWN2{--shiki-default:var(--shiki-tag)}",{"id":1935,"title":1936,"titles":1937,"content":1938,"level":9},"/v1.0.18/reference/providers","Provider Reference",[],"Provider-specific behaviors and configuration",{"id":1940,"title":1936,"titles":1941,"content":1942,"level":9},"/v1.0.18/reference/providers#provider-reference",[],"Provider-specific behaviors, limitations, and configuration options.",{"id":1944,"title":1945,"titles":1946,"content":29,"level":19},"/v1.0.18/reference/providers#key-value-providers","Key-Value Providers",[1936],{"id":1948,"title":437,"titles":1949,"content":1950,"level":35},"/v1.0.18/reference/providers#redis",[1936,1945],"import \"github.com/zoobz-io/grub/redis\"",{"id":1952,"title":1953,"titles":1954,"content":1955,"level":1518},"/v1.0.18/reference/providers#constructor","Constructor",[1936,1945,437],"func New(client *redis.Client) *Provider Accepts *redis.Client, *redis.ClusterClient, or any client implementing the required interface. client := goredis.NewClient(&goredis.Options{\n    Addr:     \"localhost:6379\",\n    Password: \"\",\n    DB:       0,\n})\nprovider := redis.New(client)",{"id":1957,"title":1958,"titles":1959,"content":1960,"level":1518},"/v1.0.18/reference/providers#behaviors","Behaviors",[1936,1945,437],"OperationImplementationGetGET commandSetSET with EX optionDeleteDEL commandExistsEXISTS commandListSCAN with MATCHGetBatchMGET commandSetBatchPipelined SET",{"id":1962,"title":1963,"titles":1964,"content":1965,"level":1518},"/v1.0.18/reference/providers#features","Features",[1936,1945,437],"FeatureSupportTTLFull (native)Batch atomicityNo (pipelined)DistributedYes",{"id":1967,"title":1968,"titles":1969,"content":1970,"level":1518},"/v1.0.18/reference/providers#error-mapping","Error Mapping",[1936,1945,437],"Redis ErrorGrub Errorredis.NilErrNotFound",{"id":1972,"title":1973,"titles":1974,"content":1975,"level":1518},"/v1.0.18/reference/providers#notes","Notes",[1936,1945,437],"List uses SCAN, which is safe for production (non-blocking)SetBatch uses pipelining (each SET is independent)Cluster mode supported via *redis.ClusterClient",{"id":1977,"title":442,"titles":1978,"content":1979,"level":35},"/v1.0.18/reference/providers#badgerdb",[1936,1945],"import \"github.com/zoobz-io/grub/badger\"",{"id":1981,"title":1953,"titles":1982,"content":1983,"level":1518},"/v1.0.18/reference/providers#constructor-1",[1936,1945,442],"func New(db *badger.DB) *Provider opts := badgerdb.DefaultOptions(\"./data\")\nopts.Logger = nil\ndb, _ := badgerdb.Open(opts)\nprovider := badger.New(db)",{"id":1985,"title":1958,"titles":1986,"content":1987,"level":1518},"/v1.0.18/reference/providers#behaviors-1",[1936,1945,442],"OperationImplementationGetView transactionSetUpdate transaction with EntryDeleteUpdate transactionExistsView transactionListIterator with prefixGetBatchView transactionSetBatchWriteBatch",{"id":1989,"title":1963,"titles":1990,"content":1991,"level":1518},"/v1.0.18/reference/providers#features-1",[1936,1945,442],"FeatureSupportTTLFull (Entry.WithTTL)Batch atomicityYes (WriteBatch)EmbeddedYesEncryptionYes (Badger native)",{"id":1993,"title":1968,"titles":1994,"content":1995,"level":1518},"/v1.0.18/reference/providers#error-mapping-1",[1936,1945,442],"Badger ErrorGrub Errorbadger.ErrKeyNotFoundErrNotFound",{"id":1997,"title":1998,"titles":1999,"content":2000,"level":1518},"/v1.0.18/reference/providers#configuration-options","Configuration Options",[1936,1945,442],"opts := badgerdb.DefaultOptions(path)\nopts.Logger = nil                    // Silence logs\nopts.ValueLogFileSize = 64 \u003C\u003C 20     // 64MB value log\nopts.NumMemtables = 2                // Memory tables\nopts.NumLevelZeroTables = 2          // L0 tables",{"id":2002,"title":2003,"titles":2004,"content":2005,"level":1518},"/v1.0.18/reference/providers#in-memory-mode","In-Memory Mode",[1936,1945,442],"opts := badgerdb.DefaultOptions(\"\").WithInMemory(true)",{"id":2007,"title":447,"titles":2008,"content":2009,"level":35},"/v1.0.18/reference/providers#boltdb",[1936,1945],"import \"github.com/zoobz-io/grub/bolt\"",{"id":2011,"title":1953,"titles":2012,"content":2013,"level":1518},"/v1.0.18/reference/providers#constructor-2",[1936,1945,447],"func New(db *bbolt.DB, bucket string) *Provider bucket is the BoltDB bucket name for data storage. db, _ := bbolt.Open(\"my.db\", 0600, nil)\nprovider := bolt.New(db, \"sessions\")",{"id":2015,"title":1958,"titles":2016,"content":2017,"level":1518},"/v1.0.18/reference/providers#behaviors-2",[1936,1945,447],"OperationImplementationGetView transactionSetUpdate transactionDeleteUpdate transactionExistsView transactionListCursor iterationGetBatchView transactionSetBatchSingle Update transaction",{"id":2019,"title":1963,"titles":2020,"content":2021,"level":1518},"/v1.0.18/reference/providers#features-2",[1936,1945,447],"FeatureSupportTTLNot supportedBatch atomicityYesEmbeddedYesMultiple storesYes (via bucket name)",{"id":2023,"title":1968,"titles":2024,"content":2025,"level":1518},"/v1.0.18/reference/providers#error-mapping-2",[1936,1945,447],"ConditionGrub ErrorKey not in bucketErrNotFoundTTL > 0ErrTTLNotSupported",{"id":2027,"title":1973,"titles":2028,"content":2029,"level":1518},"/v1.0.18/reference/providers#notes-1",[1936,1945,447],"TTL is not supported. Set with ttl > 0 returns ErrTTLNotSupportedMultiple stores can share one DB file using different bucket namesAuto-creates bucket on first write",{"id":2031,"title":2032,"titles":2033,"content":2034,"level":1518},"/v1.0.18/reference/providers#multiple-buckets","Multiple Buckets",[1936,1945,447],"db, _ := bbolt.Open(\"app.db\", 0600, nil)\nsessions := grub.NewStore[Session](bolt.New(db, \"sessions\"))\nconfigs := grub.NewStore[Config](bolt.New(db, \"configs\"))",{"id":2036,"title":2037,"titles":2038,"content":29,"level":19},"/v1.0.18/reference/providers#blob-storage-providers","Blob Storage Providers",[1936],{"id":2040,"title":456,"titles":2041,"content":2042,"level":35},"/v1.0.18/reference/providers#aws-s3",[1936,2037],"import \"github.com/zoobz-io/grub/s3\"",{"id":2044,"title":1953,"titles":2045,"content":2046,"level":1518},"/v1.0.18/reference/providers#constructor-3",[1936,2037,456],"func New(client *s3.Client, bucket string) *Provider cfg, _ := config.LoadDefaultConfig(ctx)\nclient := awss3.NewFromConfig(cfg)\nprovider := s3.New(client, \"my-bucket\")",{"id":2048,"title":1958,"titles":2049,"content":2050,"level":1518},"/v1.0.18/reference/providers#behaviors-3",[1936,2037,456],"OperationImplementationGetGetObjectPutPutObjectDeleteHeadObject + DeleteObjectExistsHeadObjectListListObjectsV2",{"id":2052,"title":1963,"titles":2053,"content":2054,"level":1518},"/v1.0.18/reference/providers#features-3",[1936,2037,456],"FeatureSupportMetadataYesContentTypeYesETagYes (from response)",{"id":2056,"title":1968,"titles":2057,"content":2058,"level":1518},"/v1.0.18/reference/providers#error-mapping-3",[1936,2037,456],"S3 ErrorGrub ErrorNoSuchKeyErrNotFound",{"id":2060,"title":1973,"titles":2061,"content":2062,"level":1518},"/v1.0.18/reference/providers#notes-2",[1936,2037,456],"Delete checks Exists first (S3 DeleteObject succeeds on missing keys)List uses continuation tokens for paginationMetadata preserved in ObjectInfo",{"id":2064,"title":2065,"titles":2066,"content":2067,"level":1518},"/v1.0.18/reference/providers#custom-endpoint-localstack","Custom Endpoint (LocalStack)",[1936,2037,456],"client := awss3.NewFromConfig(cfg, func(o *awss3.Options) {\n    o.BaseEndpoint = aws.String(\"http://localhost:4566\")\n    o.UsePathStyle = true\n})",{"id":2069,"title":461,"titles":2070,"content":2071,"level":35},"/v1.0.18/reference/providers#minio",[1936,2037],"import \"github.com/zoobz-io/grub/minio\"",{"id":2073,"title":1953,"titles":2074,"content":2075,"level":1518},"/v1.0.18/reference/providers#constructor-4",[1936,2037,461],"func New(client *minio.Client, bucket string) *Provider client, _ := minio.New(\"localhost:9000\", &minio.Options{\n    Creds:  credentials.NewStaticV4(\"minioadmin\", \"minioadmin\", \"\"),\n    Secure: false,\n})\nprovider := grubminio.New(client, \"my-bucket\")",{"id":2077,"title":1958,"titles":2078,"content":2079,"level":1518},"/v1.0.18/reference/providers#behaviors-4",[1936,2037,461],"OperationImplementationGetGetObject + StatPutPutObjectDeleteStatObject + RemoveObjectExistsStatObjectListListObjects (channel)",{"id":2081,"title":1963,"titles":2082,"content":2083,"level":1518},"/v1.0.18/reference/providers#features-4",[1936,2037,461],"FeatureSupportMetadataYes (UserMetadata)ContentTypeYesETagYes (from stat)",{"id":2085,"title":1968,"titles":2086,"content":2087,"level":1518},"/v1.0.18/reference/providers#error-mapping-4",[1936,2037,461],"MinIO ErrorGrub ErrorErrorResponse.Code == \"NoSuchKey\"ErrNotFound",{"id":2089,"title":1973,"titles":2090,"content":2091,"level":1518},"/v1.0.18/reference/providers#notes-3",[1936,2037,461],"Delete checks Exists first (RemoveObject succeeds on missing keys)List uses channel-based iteration with recursive optionUses minio-go/v7 — lighter dependency than aws-sdk-go-v2Metadata mapped via ObjectInfo.UserMetadata",{"id":2093,"title":466,"titles":2094,"content":2095,"level":35},"/v1.0.18/reference/providers#google-cloud-storage",[1936,2037],"import \"github.com/zoobz-io/grub/gcs\"",{"id":2097,"title":1953,"titles":2098,"content":2099,"level":1518},"/v1.0.18/reference/providers#constructor-5",[1936,2037,466],"func New(client *storage.Client, bucket string) *Provider client, _ := storage.NewClient(ctx)\nprovider := gcs.New(client, \"my-bucket\")",{"id":2101,"title":1958,"titles":2102,"content":2103,"level":1518},"/v1.0.18/reference/providers#behaviors-5",[1936,2037,466],"OperationImplementationGetNewReader + ReadAllPutNewWriterDeleteDeleteExistsAttrsListObjects iterator",{"id":2105,"title":1963,"titles":2106,"content":2107,"level":1518},"/v1.0.18/reference/providers#features-5",[1936,2037,466],"FeatureSupportMetadataYesContentTypeYes (writer.ContentType)ETagYes (Etag from attrs)",{"id":2109,"title":1968,"titles":2110,"content":2111,"level":1518},"/v1.0.18/reference/providers#error-mapping-5",[1936,2037,466],"GCS ErrorGrub Errorstorage.ErrObjectNotExistErrNotFound",{"id":2113,"title":471,"titles":2114,"content":2115,"level":35},"/v1.0.18/reference/providers#azure-blob-storage",[1936,2037],"import \"github.com/zoobz-io/grub/azure\"",{"id":2117,"title":1953,"titles":2118,"content":2119,"level":1518},"/v1.0.18/reference/providers#constructor-6",[1936,2037,471],"func New(client *azblob.Client, containerName string) *Provider cred, _ := azidentity.NewDefaultAzureCredential(nil)\nclient, _ := azblob.NewClient(\"https://account.blob.core.windows.net/\", cred, nil)\nprovider := azure.New(client, \"my-container\")",{"id":2121,"title":1958,"titles":2122,"content":2123,"level":1518},"/v1.0.18/reference/providers#behaviors-6",[1936,2037,471],"OperationImplementationGetDownloadStreamPutUploadBufferDeleteDeleteExistsGetPropertiesListNewListBlobsFlatPager",{"id":2125,"title":1963,"titles":2126,"content":2127,"level":1518},"/v1.0.18/reference/providers#features-6",[1936,2037,471],"FeatureSupportMetadataYes (may need GetProperties)ContentTypeYesETagYes (from properties)",{"id":2129,"title":1968,"titles":2130,"content":2131,"level":1518},"/v1.0.18/reference/providers#error-mapping-6",[1936,2037,471],"Azure ErrorGrub Errorbloberror.BlobNotFoundErrNotFound",{"id":2133,"title":1973,"titles":2134,"content":2135,"level":1518},"/v1.0.18/reference/providers#notes-4",[1936,2037,471],"Metadata may require separate GetProperties call after downloadAzure uses map[string]*string for metadata (converted internally)",{"id":2137,"title":2138,"titles":2139,"content":2140,"level":19},"/v1.0.18/reference/providers#sql-driver-modules","SQL Driver Modules",[1936],"Thin wrappers that register database drivers.",{"id":2142,"title":480,"titles":2143,"content":2144,"level":35},"/v1.0.18/reference/providers#postgresql",[1936,2138],"import _ \"github.com/zoobz-io/grub/postgres\" Registers github.com/lib/pq driver. import (\n    _ \"github.com/zoobz-io/grub/postgres\"\n    \"github.com/zoobz-io/astql/postgres\"\n)\n\ndb, _ := sqlx.Connect(\"postgres\", dsn)\nusers, _ := grub.NewDatabase[User](db, \"users\", postgres.New())",{"id":2146,"title":485,"titles":2147,"content":2148,"level":35},"/v1.0.18/reference/providers#mariadb",[1936,2138],"import _ \"github.com/zoobz-io/grub/mariadb\" Registers github.com/go-sql-driver/mysql driver (MariaDB uses MySQL wire protocol). import (\n    _ \"github.com/zoobz-io/grub/mariadb\"\n    \"github.com/zoobz-io/astql/mariadb\"\n)\n\ndb, _ := sqlx.Connect(\"mysql\", dsn)",{"id":2150,"title":490,"titles":2151,"content":2152,"level":35},"/v1.0.18/reference/providers#sqlite",[1936,2138],"import _ \"github.com/zoobz-io/grub/sqlite\" Registers modernc.org/sqlite driver (pure Go, no CGO). import (\n    _ \"github.com/zoobz-io/grub/sqlite\"\n    \"github.com/zoobz-io/astql/sqlite\"\n)\n\ndb, _ := sqlx.Connect(\"sqlite\", \"./data.db\")",{"id":2154,"title":2155,"titles":2156,"content":2157,"level":1518},"/v1.0.18/reference/providers#in-memory","In-Memory",[1936,2138,490],"db, _ := sqlx.Connect(\"sqlite\", \":memory:\")",{"id":2159,"title":495,"titles":2160,"content":2161,"level":35},"/v1.0.18/reference/providers#sql-server",[1936,2138],"import _ \"github.com/zoobz-io/grub/mssql\" Registers github.com/microsoft/go-mssqldb driver. import (\n    _ \"github.com/zoobz-io/grub/mssql\"\n    \"github.com/zoobz-io/astql/mssql\"\n)\n\ndb, _ := sqlx.Connect(\"sqlserver\", dsn)",{"id":2163,"title":2164,"titles":2165,"content":29,"level":19},"/v1.0.18/reference/providers#provider-comparison","Provider Comparison",[1936],{"id":2167,"title":2168,"titles":2169,"content":2170,"level":35},"/v1.0.18/reference/providers#key-value-features","Key-Value Features",[1936,2164],"FeatureRedisBadgerBoltTTL✓✓✗Batch atomicity✗✓✓Embedded✗✓✓Distributed✓✗✗Encryption✗✓✗In-memory mode✗✓✗",{"id":2172,"title":2173,"titles":2174,"content":2175,"level":35},"/v1.0.18/reference/providers#blob-features","Blob Features",[1936,2164],"FeatureS3MinIOGCSAzureMetadata✓✓✓✓ContentType✓✓✓✓ETag✓✓✓✓Versioning✓✓✓✓",{"id":2177,"title":320,"titles":2178,"content":2179,"level":35},"/v1.0.18/reference/providers#error-semantics",[1936,2164],"All providers map their native errors to grub semantic errors: Semantic ErrorMeaningErrNotFoundKey/object doesn't existErrTTLNotSupportedProvider can't handle TTLErrOperatorNotSupportedVector provider doesn't support filter operatorErrInvalidQueryFilter contains validation errorsErrFilterNotSupportedVector provider doesn't support metadata-only filtering",{"id":2181,"title":848,"titles":2182,"content":2183,"level":35},"/v1.0.18/reference/providers#context-cancellation",[1936,2164],"ProviderCancellation SupportRedisAll operationsBadgerIterationBoltIterationS3All operationsMinIOAll operationsGCSAll operationsAzureAll operationsQdrantAll operationsPineconeAll operationsMilvusAll operationsWeaviateAll operations",{"id":2185,"title":2186,"titles":2187,"content":29,"level":19},"/v1.0.18/reference/providers#vector-providers","Vector Providers",[1936],{"id":2189,"title":504,"titles":2190,"content":2191,"level":35},"/v1.0.18/reference/providers#qdrant",[1936,2186],"import \"github.com/zoobz-io/grub/qdrant\"",{"id":2193,"title":1953,"titles":2194,"content":2195,"level":1518},"/v1.0.18/reference/providers#constructor-7",[1936,2186,504],"func New(client *qdrant.Client, config Config) *Provider client, _ := qdrant.NewClient(&qdrant.Config{\n    Host: \"localhost\",\n    Port: 6334,\n})\nprovider := qdrant.New(client, qdrant.Config{\n    Collection: \"documents\",\n})",{"id":2197,"title":2198,"titles":2199,"content":2200,"level":1518},"/v1.0.18/reference/providers#config","Config",[1936,2186,504],"type Config struct {\n    Collection string // Required: Qdrant collection name\n}",{"id":2202,"title":1958,"titles":2203,"content":2204,"level":1518},"/v1.0.18/reference/providers#behaviors-7",[1936,2186,504],"OperationImplementationUpsertPoints UpsertGetPoints GetDeletePoints DeleteSearchPoints SearchQueryPoints Search with FilterFilterPoints Scroll with FilterListPoints Scroll",{"id":2206,"title":1963,"titles":2207,"content":2208,"level":1518},"/v1.0.18/reference/providers#features-7",[1936,2186,504],"FeatureSupportFilter operatorsAllID handlingString → uint64 (FNV-1a hash)DistanceConfigured at collection level",{"id":2210,"title":1968,"titles":2211,"content":2212,"level":1518},"/v1.0.18/reference/providers#error-mapping-7",[1936,2186,504],"Qdrant ErrorGrub ErrorPoint not foundErrNotFound",{"id":2214,"title":1973,"titles":2215,"content":2216,"level":1518},"/v1.0.18/reference/providers#notes-5",[1936,2186,504],"String IDs are hashed to uint64 using FNV-1aOriginal string ID stored in payloadCollection must exist before use",{"id":2218,"title":509,"titles":2219,"content":2220,"level":35},"/v1.0.18/reference/providers#pinecone",[1936,2186],"import \"github.com/zoobz-io/grub/pinecone\"",{"id":2222,"title":1953,"titles":2223,"content":2224,"level":1518},"/v1.0.18/reference/providers#constructor-8",[1936,2186,509],"func New(conn *pinecone.IndexConnection, config Config) *Provider client, _ := pinecone.NewClient(pinecone.NewClientParams{\n    ApiKey: os.Getenv(\"PINECONE_API_KEY\"),\n})\nindexConn, _ := client.Index(pinecone.NewIndexConnParams{\n    Host: \"your-index.svc.environment.pinecone.io\",\n})\nprovider := pinecone.New(indexConn, pinecone.Config{\n    Namespace: \"default\",\n})",{"id":2226,"title":2198,"titles":2227,"content":2228,"level":1518},"/v1.0.18/reference/providers#config-1",[1936,2186,509],"type Config struct {\n    Namespace string // Optional: Pinecone namespace\n}",{"id":2230,"title":1958,"titles":2231,"content":2232,"level":1518},"/v1.0.18/reference/providers#behaviors-8",[1936,2186,509],"OperationImplementationUpsertUpsertGetFetchDeleteDeleteSearchQuery with TopKQueryQuery with metadata filterFilterNot supported (returns ErrFilterNotSupported)ListListVectors",{"id":2234,"title":1963,"titles":2235,"content":2236,"level":1518},"/v1.0.18/reference/providers#features-8",[1936,2186,509],"FeatureSupportFilter operatorsLimited (see below)ID handlingNative stringManaged serviceYes",{"id":2238,"title":2239,"titles":2240,"content":2241,"level":1518},"/v1.0.18/reference/providers#filter-operator-support","Filter Operator Support",[1936,2186,509],"OperatorSupportEq✓Ne✓In✓Nin✓Gt/Gte/Lt/Lte✗Like✗Contains✗ Unsupported operators return ErrOperatorNotSupported.",{"id":2243,"title":1968,"titles":2244,"content":2245,"level":1518},"/v1.0.18/reference/providers#error-mapping-8",[1936,2186,509],"Pinecone ErrorGrub ErrorVector not foundErrNotFound",{"id":2247,"title":514,"titles":2248,"content":2249,"level":35},"/v1.0.18/reference/providers#milvus",[1936,2186],"import \"github.com/zoobz-io/grub/milvus\"",{"id":2251,"title":1953,"titles":2252,"content":2253,"level":1518},"/v1.0.18/reference/providers#constructor-9",[1936,2186,514],"func New(client client.Client, config Config) *Provider milvusClient, _ := client.NewClient(ctx, client.Config{\n    Address: \"localhost:19530\",\n})\nprovider := milvus.New(milvusClient, milvus.Config{\n    Collection:    \"documents\",\n    IDField:       \"id\",\n    VectorField:   \"embedding\",\n    MetadataField: \"metadata\",\n})",{"id":2255,"title":2198,"titles":2256,"content":2257,"level":1518},"/v1.0.18/reference/providers#config-2",[1936,2186,514],"type Config struct {\n    Collection    string // Required: Milvus collection name\n    IDField       string // ID field name (default: \"id\")\n    VectorField   string // Vector field name (default: \"embedding\")\n    MetadataField string // Metadata field name (default: \"metadata\")\n}",{"id":2259,"title":1958,"titles":2260,"content":2261,"level":1518},"/v1.0.18/reference/providers#behaviors-9",[1936,2186,514],"OperationImplementationUpsertInsert with delete-if-existsGetQuery by IDDeleteDelete by IDSearchSearch with expressionQuerySearch with translated filterFilterQuery with expressionListQuery all IDs",{"id":2263,"title":1963,"titles":2264,"content":2265,"level":1518},"/v1.0.18/reference/providers#features-9",[1936,2186,514],"FeatureSupportFilter operatorsAllID handlingNative stringConfigurable fieldsYes",{"id":2267,"title":1968,"titles":2268,"content":2269,"level":1518},"/v1.0.18/reference/providers#error-mapping-9",[1936,2186,514],"Milvus ErrorGrub ErrorEntity not foundErrNotFound",{"id":2271,"title":1973,"titles":2272,"content":2273,"level":1518},"/v1.0.18/reference/providers#notes-6",[1936,2186,514],"Collection must exist with appropriate schemaUses Milvus expression language for filtersRequires collection to be loaded",{"id":2275,"title":519,"titles":2276,"content":2277,"level":35},"/v1.0.18/reference/providers#weaviate",[1936,2186],"import \"github.com/zoobz-io/grub/weaviate\"",{"id":2279,"title":1953,"titles":2280,"content":2281,"level":1518},"/v1.0.18/reference/providers#constructor-10",[1936,2186,519],"func New(client *weaviate.Client, config Config) *Provider cfg := weaviate.Config{\n    Host:   \"localhost:8080\",\n    Scheme: \"http\",\n}\nclient, _ := weaviate.NewClient(cfg)\nprovider := weaviate.New(client, weaviate.Config{\n    Class:      \"Document\",\n    Properties: []string{\"category\", \"score\", \"tags\"},\n})",{"id":2283,"title":2198,"titles":2284,"content":2285,"level":1518},"/v1.0.18/reference/providers#config-3",[1936,2186,519],"type Config struct {\n    Class      string   // Required: Weaviate class name\n    Properties []string // Metadata property names to retrieve in searches\n}",{"id":2287,"title":1958,"titles":2288,"content":2289,"level":1518},"/v1.0.18/reference/providers#behaviors-10",[1936,2186,519],"OperationImplementationUpsertData Creator/UpdaterGetData ObjectsGetterDeleteData DeleterSearchGraphQL NearVectorQueryGraphQL with Where filterFilterGraphQL Get with Where filterListGraphQL Get",{"id":2291,"title":1963,"titles":2292,"content":2293,"level":1518},"/v1.0.18/reference/providers#features-10",[1936,2186,519],"FeatureSupportFilter operatorsAllID handlingString → deterministic UUID (SHA1)GraphQL queriesYesProperties configRequired for metadata",{"id":2295,"title":1968,"titles":2296,"content":2297,"level":1518},"/v1.0.18/reference/providers#error-mapping-10",[1936,2186,519],"Weaviate ErrorGrub Error404 / not foundErrNotFound",{"id":2299,"title":1973,"titles":2300,"content":2301,"level":1518},"/v1.0.18/reference/providers#notes-7",[1936,2186,519],"String IDs converted to deterministic UUIDs via SHA1Original string ID stored in _grub_id propertyProperties must be configured to retrieve metadata in search resultsClass/schema must exist before useUses GraphQL for search operations html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}",[2303],{"title":2304,"path":2305,"stem":2306,"children":2307,"page":2321},"V1018","/v1.0.18","v1.0.18",[2308,2310,2322,2337,2350],{"title":6,"path":5,"stem":2309,"description":8},"v1.0.18/1.overview",{"title":2311,"path":2312,"stem":2313,"children":2314,"page":2321},"Learn","/v1.0.18/learn","v1.0.18/2.learn",[2315,2317,2319],{"title":77,"path":76,"stem":2316,"description":79},"v1.0.18/2.learn/1.quickstart",{"title":130,"path":129,"stem":2318,"description":132},"v1.0.18/2.learn/2.concepts",{"title":16,"path":288,"stem":2320,"description":290},"v1.0.18/2.learn/3.architecture",false,{"title":2323,"path":2324,"stem":2325,"children":2326,"page":2321},"Guides","/v1.0.18/guides","v1.0.18/3.guides",[2327,2329,2331,2333,2335],{"title":401,"path":400,"stem":2328,"description":403},"v1.0.18/3.guides/1.providers",{"title":558,"path":557,"stem":2330,"description":560},"v1.0.18/3.guides/2.lifecycle",{"title":741,"path":740,"stem":2332,"description":743},"v1.0.18/3.guides/3.pagination",{"title":853,"path":852,"stem":2334,"description":855},"v1.0.18/3.guides/4.testing",{"title":947,"path":946,"stem":2336,"description":949},"v1.0.18/3.guides/5.best-practices",{"title":2338,"path":2339,"stem":2340,"children":2341,"page":2321},"Cookbook","/v1.0.18/cookbook","v1.0.18/4.cookbook",[2342,2344,2346,2348],{"title":1122,"path":1121,"stem":2343,"description":1124},"v1.0.18/4.cookbook/1.caching",{"title":1206,"path":1205,"stem":2345,"description":1208},"v1.0.18/4.cookbook/2.migrations",{"title":1306,"path":1305,"stem":2347,"description":1308},"v1.0.18/4.cookbook/3.multi-tenant",{"title":1425,"path":1424,"stem":2349,"description":1427},"v1.0.18/4.cookbook/4.vector-search",{"title":2351,"path":2352,"stem":2353,"children":2354,"page":2321},"Reference","/v1.0.18/reference","v1.0.18/5.reference",[2355,2357],{"title":1479,"path":1478,"stem":2356,"description":1481},"v1.0.18/5.reference/1.api",{"title":1936,"path":1935,"stem":2358,"description":1938},"v1.0.18/5.reference/2.providers",[2360],{"title":2304,"path":2305,"stem":2306,"children":2361,"page":2321},[2362,2363,2368,2375,2381],{"title":6,"path":5,"stem":2309},{"title":2311,"path":2312,"stem":2313,"children":2364,"page":2321},[2365,2366,2367],{"title":77,"path":76,"stem":2316},{"title":130,"path":129,"stem":2318},{"title":16,"path":288,"stem":2320},{"title":2323,"path":2324,"stem":2325,"children":2369,"page":2321},[2370,2371,2372,2373,2374],{"title":401,"path":400,"stem":2328},{"title":558,"path":557,"stem":2330},{"title":741,"path":740,"stem":2332},{"title":853,"path":852,"stem":2334},{"title":947,"path":946,"stem":2336},{"title":2338,"path":2339,"stem":2340,"children":2376,"page":2321},[2377,2378,2379,2380],{"title":1122,"path":1121,"stem":2343},{"title":1206,"path":1205,"stem":2345},{"title":1306,"path":1305,"stem":2347},{"title":1425,"path":1424,"stem":2349},{"title":2351,"path":2352,"stem":2353,"children":2382,"page":2321},[2383,2384],{"title":1479,"path":1478,"stem":2356},{"title":1936,"path":1935,"stem":2358},[2386,3971,4019],{"id":2387,"title":2388,"body":2389,"description":29,"extension":3964,"icon":3965,"meta":3966,"navigation":2613,"path":3967,"seo":3968,"stem":3969,"__hash__":3970},"resources/readme.md","README",{"type":2390,"value":2391,"toc":3948},"minimark",[2392,2396,2464,2467,2470,2475,2706,2709,2838,2841,2845,2862,2865,2869,3369,3373,3488,3492,3527,3531,3537,3540,3810,3813,3817,3825,3829,3847,3850,3880,3883,3906,3909,3923,3927,3935,3938,3944],[2393,2394,2395],"h1",{"id":2395},"grub",[2397,2398,2399,2410,2418,2426,2434,2442,2449,2456],"p",{},[2400,2401,2405],"a",{"href":2402,"rel":2403},"https://github.com/zoobz-io/grub/actions/workflows/ci.yml",[2404],"nofollow",[2406,2407],"img",{"alt":2408,"src":2409},"CI","https://github.com/zoobz-io/grub/actions/workflows/ci.yml/badge.svg",[2400,2411,2414],{"href":2412,"rel":2413},"https://codecov.io/gh/zoobz-io/grub",[2404],[2406,2415],{"alt":2416,"src":2417},"codecov","https://codecov.io/gh/zoobz-io/grub/graph/badge.svg?branch=main",[2400,2419,2422],{"href":2420,"rel":2421},"https://goreportcard.com/report/github.com/zoobz-io/grub",[2404],[2406,2423],{"alt":2424,"src":2425},"Go Report Card","https://goreportcard.com/badge/github.com/zoobz-io/grub",[2400,2427,2430],{"href":2428,"rel":2429},"https://github.com/zoobz-io/grub/actions/workflows/codeql.yml",[2404],[2406,2431],{"alt":2432,"src":2433},"CodeQL","https://github.com/zoobz-io/grub/actions/workflows/codeql.yml/badge.svg",[2400,2435,2438],{"href":2436,"rel":2437},"https://pkg.go.dev/github.com/zoobz-io/grub",[2404],[2406,2439],{"alt":2440,"src":2441},"Go Reference","https://pkg.go.dev/badge/github.com/zoobz-io/grub.svg",[2400,2443,2445],{"href":2444},"LICENSE",[2406,2446],{"alt":2447,"src":2448},"License","https://img.shields.io/github/license/zoobz-io/grub",[2400,2450,2452],{"href":2451},"go.mod",[2406,2453],{"alt":2454,"src":2455},"Go Version","https://img.shields.io/github/go-mod/go-version/zoobz-io/grub",[2400,2457,2460],{"href":2458,"rel":2459},"https://github.com/zoobz-io/grub/releases",[2404],[2406,2461],{"alt":2462,"src":2463},"Release","https://img.shields.io/github/v/release/zoobz-io/grub",[2397,2465,2466],{},"Provider-agnostic storage for Go.",[2397,2468,2469],{},"Type-safe CRUD across key-value stores, blob storage, SQL databases, and vector similarity search with a unified interface.",[2471,2472,2474],"h2",{"id":2473},"one-interface-any-backend","One Interface, Any Backend",[2476,2477,2481],"pre",{"className":2478,"code":2479,"language":2480,"meta":29,"style":29},"language-go shiki shiki-themes","// Same code, different providers\nsessions := grub.NewStore[Session](redis.New(client))\nsessions := grub.NewStore[Session](badger.New(db))\nsessions := grub.NewStore[Session](bolt.New(db, \"sessions\"))\n\n// Type-safe operations\nsession, _ := sessions.Get(ctx, \"session:abc\")\nsessions.Set(ctx, \"session:xyz\", &Session{UserID: \"123\"}, time.Hour)\n","go",[2482,2483,2484,2492,2538,2570,2608,2615,2621,2654],"code",{"__ignoreMap":29},[2485,2486,2488],"span",{"class":2487,"line":9},"line",[2485,2489,2491],{"class":2490},"sLkEo","// Same code, different providers\n",[2485,2493,2494,2498,2501,2504,2508,2511,2514,2518,2521,2524,2526,2529,2532,2535],{"class":2487,"line":19},[2485,2495,2497],{"class":2496},"sh8_p","sessions",[2485,2499,2500],{"class":2496}," :=",[2485,2502,2503],{"class":2496}," grub",[2485,2505,2507],{"class":2506},"sq5bi",".",[2485,2509,1502],{"class":2510},"s5klm",[2485,2512,2513],{"class":2506},"[",[2485,2515,2517],{"class":2516},"sYBwO","Session",[2485,2519,2520],{"class":2506},"](",[2485,2522,2523],{"class":2496},"redis",[2485,2525,2507],{"class":2506},[2485,2527,2528],{"class":2510},"New",[2485,2530,2531],{"class":2506},"(",[2485,2533,2534],{"class":2496},"client",[2485,2536,2537],{"class":2506},"))\n",[2485,2539,2540,2542,2544,2546,2548,2550,2552,2554,2556,2559,2561,2563,2565,2568],{"class":2487,"line":35},[2485,2541,2497],{"class":2496},[2485,2543,2500],{"class":2496},[2485,2545,2503],{"class":2496},[2485,2547,2507],{"class":2506},[2485,2549,1502],{"class":2510},[2485,2551,2513],{"class":2506},[2485,2553,2517],{"class":2516},[2485,2555,2520],{"class":2506},[2485,2557,2558],{"class":2496},"badger",[2485,2560,2507],{"class":2506},[2485,2562,2528],{"class":2510},[2485,2564,2531],{"class":2506},[2485,2566,2567],{"class":2496},"db",[2485,2569,2537],{"class":2506},[2485,2571,2572,2574,2576,2578,2580,2582,2584,2586,2588,2591,2593,2595,2597,2599,2602,2606],{"class":2487,"line":1518},[2485,2573,2497],{"class":2496},[2485,2575,2500],{"class":2496},[2485,2577,2503],{"class":2496},[2485,2579,2507],{"class":2506},[2485,2581,1502],{"class":2510},[2485,2583,2513],{"class":2506},[2485,2585,2517],{"class":2516},[2485,2587,2520],{"class":2506},[2485,2589,2590],{"class":2496},"bolt",[2485,2592,2507],{"class":2506},[2485,2594,2528],{"class":2510},[2485,2596,2531],{"class":2506},[2485,2598,2567],{"class":2496},[2485,2600,2601],{"class":2506},",",[2485,2603,2605],{"class":2604},"sxAnc"," \"sessions\"",[2485,2607,2537],{"class":2506},[2485,2609,2611],{"class":2487,"line":2610},5,[2485,2612,2614],{"emptyLinePlaceholder":2613},true,"\n",[2485,2616,2618],{"class":2487,"line":2617},6,[2485,2619,2620],{"class":2490},"// Type-safe operations\n",[2485,2622,2624,2627,2629,2632,2634,2637,2639,2641,2643,2646,2648,2651],{"class":2487,"line":2623},7,[2485,2625,2626],{"class":2496},"session",[2485,2628,2601],{"class":2506},[2485,2630,2631],{"class":2496}," _",[2485,2633,2500],{"class":2496},[2485,2635,2636],{"class":2496}," sessions",[2485,2638,2507],{"class":2506},[2485,2640,571],{"class":2510},[2485,2642,2531],{"class":2506},[2485,2644,2645],{"class":2496},"ctx",[2485,2647,2601],{"class":2506},[2485,2649,2650],{"class":2604}," \"session:abc\"",[2485,2652,2653],{"class":2506},")\n",[2485,2655,2657,2659,2661,2663,2665,2667,2669,2672,2674,2678,2680,2683,2687,2690,2693,2696,2699,2701,2704],{"class":2487,"line":2656},8,[2485,2658,2497],{"class":2496},[2485,2660,2507],{"class":2506},[2485,2662,576],{"class":2510},[2485,2664,2531],{"class":2506},[2485,2666,2645],{"class":2496},[2485,2668,2601],{"class":2506},[2485,2670,2671],{"class":2604}," \"session:xyz\"",[2485,2673,2601],{"class":2506},[2485,2675,2677],{"class":2676},"sW3Qg"," &",[2485,2679,2517],{"class":2516},[2485,2681,2682],{"class":2506},"{",[2485,2684,2686],{"class":2685},"sBGCq","UserID",[2485,2688,2689],{"class":2506},":",[2485,2691,2692],{"class":2604}," \"123\"",[2485,2694,2695],{"class":2506},"},",[2485,2697,2698],{"class":2496}," time",[2485,2700,2507],{"class":2506},[2485,2702,2703],{"class":2496},"Hour",[2485,2705,2653],{"class":2506},[2397,2707,2708],{},"Swap Redis for BadgerDB. Move from S3 to Azure. Switch databases from SQLite to PostgreSQL. Your business logic stays the same.",[2476,2710,2712],{"className":2478,"code":2711,"language":2480,"meta":29,"style":29},"// Key-value, blob, SQL, or vector — same patterns\nstore := grub.NewStore[Config](provider)           // key-value\nbucket := grub.NewBucket[Document](provider)       // blob storage\ndb := grub.NewDatabase[User](conn, \"users\", renderer)            // SQL\nindex := grub.NewIndex[Embedding](provider)        // vector search\n",[2482,2713,2714,2719,2747,2774,2811],{"__ignoreMap":29},[2485,2715,2716],{"class":2487,"line":9},[2485,2717,2718],{"class":2490},"// Key-value, blob, SQL, or vector — same patterns\n",[2485,2720,2721,2724,2726,2728,2730,2732,2734,2736,2738,2741,2744],{"class":2487,"line":19},[2485,2722,2723],{"class":2496},"store",[2485,2725,2500],{"class":2496},[2485,2727,2503],{"class":2496},[2485,2729,2507],{"class":2506},[2485,2731,1502],{"class":2510},[2485,2733,2513],{"class":2506},[2485,2735,2198],{"class":2516},[2485,2737,2520],{"class":2506},[2485,2739,2740],{"class":2496},"provider",[2485,2742,2743],{"class":2506},")",[2485,2745,2746],{"class":2490},"           // key-value\n",[2485,2748,2749,2752,2754,2756,2758,2760,2762,2765,2767,2769,2771],{"class":2487,"line":35},[2485,2750,2751],{"class":2496},"bucket",[2485,2753,2500],{"class":2496},[2485,2755,2503],{"class":2496},[2485,2757,2507],{"class":2506},[2485,2759,1555],{"class":2510},[2485,2761,2513],{"class":2506},[2485,2763,2764],{"class":2516},"Document",[2485,2766,2520],{"class":2506},[2485,2768,2740],{"class":2496},[2485,2770,2743],{"class":2506},[2485,2772,2773],{"class":2490},"       // blob storage\n",[2485,2775,2776,2778,2780,2782,2784,2786,2788,2791,2793,2796,2798,2801,2803,2806,2808],{"class":2487,"line":1518},[2485,2777,2567],{"class":2496},[2485,2779,2500],{"class":2496},[2485,2781,2503],{"class":2496},[2485,2783,2507],{"class":2506},[2485,2785,1597],{"class":2510},[2485,2787,2513],{"class":2506},[2485,2789,2790],{"class":2516},"User",[2485,2792,2520],{"class":2506},[2485,2794,2795],{"class":2496},"conn",[2485,2797,2601],{"class":2506},[2485,2799,2800],{"class":2604}," \"users\"",[2485,2802,2601],{"class":2506},[2485,2804,2805],{"class":2496}," renderer",[2485,2807,2743],{"class":2506},[2485,2809,2810],{"class":2490},"            // SQL\n",[2485,2812,2813,2816,2818,2820,2822,2824,2826,2829,2831,2833,2835],{"class":2487,"line":2610},[2485,2814,2815],{"class":2496},"index",[2485,2817,2500],{"class":2496},[2485,2819,2503],{"class":2496},[2485,2821,2507],{"class":2506},[2485,2823,1749],{"class":2510},[2485,2825,2513],{"class":2506},[2485,2827,2828],{"class":2516},"Embedding",[2485,2830,2520],{"class":2506},[2485,2832,2740],{"class":2496},[2485,2834,2743],{"class":2506},[2485,2836,2837],{"class":2490},"        // vector search\n",[2397,2839,2840],{},"Four storage modes, consistent API, semantic errors across all providers.",[2471,2842,2844],{"id":2843},"install","Install",[2476,2846,2850],{"className":2847,"code":2848,"language":2849,"meta":29,"style":29},"language-bash shiki shiki-themes","go get github.com/zoobz-io/grub\n","bash",[2482,2851,2852],{"__ignoreMap":29},[2485,2853,2854,2856,2859],{"class":2487,"line":9},[2485,2855,2480],{"class":2510},[2485,2857,2858],{"class":2604}," get",[2485,2860,2861],{"class":2604}," github.com/zoobz-io/grub\n",[2397,2863,2864],{},"Requires Go 1.24+.",[2471,2866,2868],{"id":2867},"quick-start","Quick Start",[2476,2870,2872],{"className":2478,"code":2871,"language":2480,"meta":29,"style":29},"package main\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"time\"\n\n    \"github.com/zoobz-io/grub\"\n    \"github.com/zoobz-io/grub/redis\"\n    goredis \"github.com/redis/go-redis/v9\"\n)\n\ntype Session struct {\n    UserID    string `json:\"user_id\"`\n    Token     string `json:\"token\"`\n    ExpiresAt int64  `json:\"expires_at\"`\n}\n\nfunc main() {\n    ctx := context.Background()\n\n    // Connect to Redis\n    client := goredis.NewClient(&goredis.Options{Addr: \"localhost:6379\"})\n    defer client.Close()\n\n    // Create type-safe store\n    sessions := grub.NewStore[Session](redis.New(client))\n\n    // Store with TTL\n    session := &Session{\n        UserID:    \"user:123\",\n        Token:     \"abc123\",\n        ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),\n    }\n    _ = sessions.Set(ctx, \"session:abc123\", session, 24*time.Hour)\n\n    // Retrieve\n    s, _ := sessions.Get(ctx, \"session:abc123\")\n    fmt.Println(s.UserID) // user:123\n}\n",[2482,2873,2874,2883,2887,2896,2901,2906,2911,2915,2920,2926,2936,2941,2946,2961,2973,2985,2997,3003,3008,3022,3041,3046,3052,3094,3110,3115,3121,3153,3158,3164,3179,3193,3206,3251,3257,3300,3305,3311,3339,3364],{"__ignoreMap":29},[2485,2875,2876,2880],{"class":2487,"line":9},[2485,2877,2879],{"class":2878},"sUt3r","package",[2485,2881,2882],{"class":2516}," main\n",[2485,2884,2885],{"class":2487,"line":19},[2485,2886,2614],{"emptyLinePlaceholder":2613},[2485,2888,2889,2892],{"class":2487,"line":35},[2485,2890,2891],{"class":2878},"import",[2485,2893,2895],{"class":2894},"soy-K"," (\n",[2485,2897,2898],{"class":2487,"line":1518},[2485,2899,2900],{"class":2604},"    \"context\"\n",[2485,2902,2903],{"class":2487,"line":2610},[2485,2904,2905],{"class":2604},"    \"fmt\"\n",[2485,2907,2908],{"class":2487,"line":2617},[2485,2909,2910],{"class":2604},"    \"time\"\n",[2485,2912,2913],{"class":2487,"line":2623},[2485,2914,2614],{"emptyLinePlaceholder":2613},[2485,2916,2917],{"class":2487,"line":2656},[2485,2918,2919],{"class":2604},"    \"github.com/zoobz-io/grub\"\n",[2485,2921,2923],{"class":2487,"line":2922},9,[2485,2924,2925],{"class":2604},"    \"github.com/zoobz-io/grub/redis\"\n",[2485,2927,2929,2933],{"class":2487,"line":2928},10,[2485,2930,2932],{"class":2931},"sSYET","    goredis",[2485,2934,2935],{"class":2604}," \"github.com/redis/go-redis/v9\"\n",[2485,2937,2939],{"class":2487,"line":2938},11,[2485,2940,2653],{"class":2894},[2485,2942,2944],{"class":2487,"line":2943},12,[2485,2945,2614],{"emptyLinePlaceholder":2613},[2485,2947,2949,2952,2955,2958],{"class":2487,"line":2948},13,[2485,2950,2951],{"class":2878},"type",[2485,2953,2954],{"class":2516}," Session",[2485,2956,2957],{"class":2878}," struct",[2485,2959,2960],{"class":2506}," {\n",[2485,2962,2964,2967,2970],{"class":2487,"line":2963},14,[2485,2965,2966],{"class":2685},"    UserID",[2485,2968,2969],{"class":2516},"    string",[2485,2971,2972],{"class":2604}," `json:\"user_id\"`\n",[2485,2974,2976,2979,2982],{"class":2487,"line":2975},15,[2485,2977,2978],{"class":2685},"    Token",[2485,2980,2981],{"class":2516},"     string",[2485,2983,2984],{"class":2604}," `json:\"token\"`\n",[2485,2986,2988,2991,2994],{"class":2487,"line":2987},16,[2485,2989,2990],{"class":2685},"    ExpiresAt",[2485,2992,2993],{"class":2516}," int64",[2485,2995,2996],{"class":2604},"  `json:\"expires_at\"`\n",[2485,2998,3000],{"class":2487,"line":2999},17,[2485,3001,3002],{"class":2506},"}\n",[2485,3004,3006],{"class":2487,"line":3005},18,[2485,3007,2614],{"emptyLinePlaceholder":2613},[2485,3009,3011,3014,3017,3020],{"class":2487,"line":3010},19,[2485,3012,3013],{"class":2878},"func",[2485,3015,3016],{"class":2510}," main",[2485,3018,3019],{"class":2506},"()",[2485,3021,2960],{"class":2506},[2485,3023,3025,3028,3030,3033,3035,3038],{"class":2487,"line":3024},20,[2485,3026,3027],{"class":2496},"    ctx",[2485,3029,2500],{"class":2496},[2485,3031,3032],{"class":2496}," context",[2485,3034,2507],{"class":2506},[2485,3036,3037],{"class":2510},"Background",[2485,3039,3040],{"class":2506},"()\n",[2485,3042,3044],{"class":2487,"line":3043},21,[2485,3045,2614],{"emptyLinePlaceholder":2613},[2485,3047,3049],{"class":2487,"line":3048},22,[2485,3050,3051],{"class":2490},"    // Connect to Redis\n",[2485,3053,3055,3058,3060,3063,3065,3068,3070,3073,3076,3078,3081,3083,3086,3088,3091],{"class":2487,"line":3054},23,[2485,3056,3057],{"class":2496},"    client",[2485,3059,2500],{"class":2496},[2485,3061,3062],{"class":2496}," goredis",[2485,3064,2507],{"class":2506},[2485,3066,3067],{"class":2510},"NewClient",[2485,3069,2531],{"class":2506},[2485,3071,3072],{"class":2676},"&",[2485,3074,3075],{"class":2516},"goredis",[2485,3077,2507],{"class":2506},[2485,3079,3080],{"class":2516},"Options",[2485,3082,2682],{"class":2506},[2485,3084,3085],{"class":2685},"Addr",[2485,3087,2689],{"class":2506},[2485,3089,3090],{"class":2604}," \"localhost:6379\"",[2485,3092,3093],{"class":2506},"})\n",[2485,3095,3097,3100,3103,3105,3108],{"class":2487,"line":3096},24,[2485,3098,3099],{"class":2676},"    defer",[2485,3101,3102],{"class":2496}," client",[2485,3104,2507],{"class":2506},[2485,3106,3107],{"class":2510},"Close",[2485,3109,3040],{"class":2506},[2485,3111,3113],{"class":2487,"line":3112},25,[2485,3114,2614],{"emptyLinePlaceholder":2613},[2485,3116,3118],{"class":2487,"line":3117},26,[2485,3119,3120],{"class":2490},"    // Create type-safe store\n",[2485,3122,3124,3127,3129,3131,3133,3135,3137,3139,3141,3143,3145,3147,3149,3151],{"class":2487,"line":3123},27,[2485,3125,3126],{"class":2496},"    sessions",[2485,3128,2500],{"class":2496},[2485,3130,2503],{"class":2496},[2485,3132,2507],{"class":2506},[2485,3134,1502],{"class":2510},[2485,3136,2513],{"class":2506},[2485,3138,2517],{"class":2516},[2485,3140,2520],{"class":2506},[2485,3142,2523],{"class":2496},[2485,3144,2507],{"class":2506},[2485,3146,2528],{"class":2510},[2485,3148,2531],{"class":2506},[2485,3150,2534],{"class":2496},[2485,3152,2537],{"class":2506},[2485,3154,3156],{"class":2487,"line":3155},28,[2485,3157,2614],{"emptyLinePlaceholder":2613},[2485,3159,3161],{"class":2487,"line":3160},29,[2485,3162,3163],{"class":2490},"    // Store with TTL\n",[2485,3165,3167,3170,3172,3174,3176],{"class":2487,"line":3166},30,[2485,3168,3169],{"class":2496},"    session",[2485,3171,2500],{"class":2496},[2485,3173,2677],{"class":2676},[2485,3175,2517],{"class":2516},[2485,3177,3178],{"class":2506},"{\n",[2485,3180,3182,3185,3187,3190],{"class":2487,"line":3181},31,[2485,3183,3184],{"class":2685},"        UserID",[2485,3186,2689],{"class":2506},[2485,3188,3189],{"class":2604},"    \"user:123\"",[2485,3191,3192],{"class":2506},",\n",[2485,3194,3196,3199,3201,3204],{"class":2487,"line":3195},32,[2485,3197,3198],{"class":2685},"        Token",[2485,3200,2689],{"class":2506},[2485,3202,3203],{"class":2604},"     \"abc123\"",[2485,3205,3192],{"class":2506},[2485,3207,3209,3212,3214,3216,3218,3221,3224,3227,3229,3233,3236,3238,3240,3242,3245,3248],{"class":2487,"line":3208},33,[2485,3210,3211],{"class":2685},"        ExpiresAt",[2485,3213,2689],{"class":2506},[2485,3215,2698],{"class":2496},[2485,3217,2507],{"class":2506},[2485,3219,3220],{"class":2510},"Now",[2485,3222,3223],{"class":2506},"().",[2485,3225,3226],{"class":2510},"Add",[2485,3228,2531],{"class":2506},[2485,3230,3232],{"class":3231},"sMAmT","24",[2485,3234,3235],{"class":2496}," *",[2485,3237,2698],{"class":2496},[2485,3239,2507],{"class":2506},[2485,3241,2703],{"class":2496},[2485,3243,3244],{"class":2506},").",[2485,3246,3247],{"class":2510},"Unix",[2485,3249,3250],{"class":2506},"(),\n",[2485,3252,3254],{"class":2487,"line":3253},34,[2485,3255,3256],{"class":2506},"    }\n",[2485,3258,3260,3263,3266,3268,3270,3272,3274,3276,3278,3281,3283,3286,3288,3291,3294,3296,3298],{"class":2487,"line":3259},35,[2485,3261,3262],{"class":2496},"    _",[2485,3264,3265],{"class":2496}," =",[2485,3267,2636],{"class":2496},[2485,3269,2507],{"class":2506},[2485,3271,576],{"class":2510},[2485,3273,2531],{"class":2506},[2485,3275,2645],{"class":2496},[2485,3277,2601],{"class":2506},[2485,3279,3280],{"class":2604}," \"session:abc123\"",[2485,3282,2601],{"class":2506},[2485,3284,3285],{"class":2496}," session",[2485,3287,2601],{"class":2506},[2485,3289,3290],{"class":3231}," 24",[2485,3292,3293],{"class":2496},"*time",[2485,3295,2507],{"class":2506},[2485,3297,2703],{"class":2496},[2485,3299,2653],{"class":2506},[2485,3301,3303],{"class":2487,"line":3302},36,[2485,3304,2614],{"emptyLinePlaceholder":2613},[2485,3306,3308],{"class":2487,"line":3307},37,[2485,3309,3310],{"class":2490},"    // Retrieve\n",[2485,3312,3314,3317,3319,3321,3323,3325,3327,3329,3331,3333,3335,3337],{"class":2487,"line":3313},38,[2485,3315,3316],{"class":2496},"    s",[2485,3318,2601],{"class":2506},[2485,3320,2631],{"class":2496},[2485,3322,2500],{"class":2496},[2485,3324,2636],{"class":2496},[2485,3326,2507],{"class":2506},[2485,3328,571],{"class":2510},[2485,3330,2531],{"class":2506},[2485,3332,2645],{"class":2496},[2485,3334,2601],{"class":2506},[2485,3336,3280],{"class":2604},[2485,3338,2653],{"class":2506},[2485,3340,3342,3345,3347,3350,3352,3355,3357,3359,3361],{"class":2487,"line":3341},39,[2485,3343,3344],{"class":2496},"    fmt",[2485,3346,2507],{"class":2506},[2485,3348,3349],{"class":2510},"Println",[2485,3351,2531],{"class":2506},[2485,3353,3354],{"class":2496},"s",[2485,3356,2507],{"class":2506},[2485,3358,2686],{"class":2496},[2485,3360,2743],{"class":2506},[2485,3362,3363],{"class":2490}," // user:123\n",[2485,3365,3367],{"class":2487,"line":3366},40,[2485,3368,3002],{"class":2506},[2471,3370,3372],{"id":3371},"capabilities","Capabilities",[3374,3375,3376,3392],"table",{},[3377,3378,3379],"thead",{},[3380,3381,3382,3386,3389],"tr",{},[3383,3384,3385],"th",{},"Feature",[3383,3387,3388],{},"Description",[3383,3390,3391],{},"Docs",[3393,3394,3395,3408,3421,3434,3445,3457,3476],"tbody",{},[3380,3396,3397,3400,3403],{},[3398,3399,32],"td",{},[3398,3401,3402],{},"Sessions, cache, config with optional TTL",[3398,3404,3405],{},[2400,3406,401],{"href":3407},"docs/guides/providers",[3380,3409,3410,3412,3415],{},[3398,3411,38],{},[3398,3413,3414],{},"Files and documents with metadata",[3398,3416,3417],{},[2400,3418,3420],{"href":3419},"docs/guides/lifecycle","Lifecycle",[3380,3422,3423,3425,3428],{},[3398,3424,43],{},[3398,3426,3427],{},"Structured records with query capabilities",[3398,3429,3430],{},[2400,3431,3433],{"href":3432},"docs/learn/concepts","Concepts",[3380,3435,3436,3438,3441],{},[3398,3437,48],{},[3398,3439,3440],{},"Similarity search with metadata filtering",[3398,3442,3443],{},[2400,3444,401],{"href":3407},[3380,3446,3447,3449,3452],{},[3398,3448,284],{},[3398,3450,3451],{},"Field-level access for encryption pipelines",[3398,3453,3454],{},[2400,3455,16],{"href":3456},"docs/learn/architecture",[3380,3458,3459,3461,3471],{},[3398,3460,279],{},[3398,3462,3463,3466,3467,3470],{},[2482,3464,3465],{},"ErrNotFound",", ",[2482,3468,3469],{},"ErrDuplicate"," across all providers",[3398,3472,3473],{},[2400,3474,1479],{"href":3475},"docs/reference/api",[3380,3477,3478,3481,3484],{},[3398,3479,3480],{},"Custom Codecs",[3398,3482,3483],{},"JSON default, Gob available, or bring your own",[3398,3485,3486],{},[2400,3487,3433],{"href":3432},[2471,3489,3491],{"id":3490},"why-grub","Why grub?",[3493,3494,3495,3503,3509,3515,3521],"ul",{},[3496,3497,3498,3502],"li",{},[3499,3500,3501],"strong",{},"Type-safe"," — Generics eliminate runtime type assertions",[3496,3504,3505,3508],{},[3499,3506,3507],{},"Swap backends"," — Change providers without touching business logic",[3496,3510,3511,3514],{},[3499,3512,3513],{},"Consistent errors"," — Same error types whether you're using Redis or S3",[3496,3516,3517,3520],{},[3499,3518,3519],{},"Atomic views"," — Field-level access for framework internals (encryption, pipelines)",[3496,3522,3523,3526],{},[3499,3524,3525],{},"Isolated dependencies"," — Each provider is a separate module; only pull what you use",[2471,3528,3530],{"id":3529},"storage-without-coupling","Storage Without Coupling",[2397,3532,3533,3534,2507],{},"Grub enables a pattern: ",[3499,3535,3536],{},"define storage once, swap implementations freely",[2397,3538,3539],{},"Your domain code works with typed stores. Infrastructure decisions — Redis vs embedded, S3 vs local filesystem, PostgreSQL vs SQLite, Pinecone vs Qdrant — become configuration, not architecture.",[2476,3541,3543],{"className":2478,"code":3542,"language":2480,"meta":29,"style":29},"// Domain code doesn't know or care about the backend\ntype SessionStore struct {\n    store *grub.Store[Session]\n}\n\nfunc (s *SessionStore) Save(ctx context.Context, session *Session) error {\n    return s.store.Set(ctx, \"session:\"+session.Token, session, 24*time.Hour)\n}\n\n// Production: Redis\nstore := grub.NewStore[Session](redis.New(redisClient))\n\n// Development: embedded BadgerDB\nstore := grub.NewStore[Session](badger.New(localDB))\n\n// Testing: in-memory\nstore := grub.NewStore[Session](badger.New(memDB))\n",[2482,3544,3545,3550,3561,3582,3586,3590,3637,3686,3690,3694,3699,3730,3734,3739,3770,3774,3779],{"__ignoreMap":29},[2485,3546,3547],{"class":2487,"line":9},[2485,3548,3549],{"class":2490},"// Domain code doesn't know or care about the backend\n",[2485,3551,3552,3554,3557,3559],{"class":2487,"line":19},[2485,3553,2951],{"class":2878},[2485,3555,3556],{"class":2516}," SessionStore",[2485,3558,2957],{"class":2878},[2485,3560,2960],{"class":2506},[2485,3562,3563,3566,3568,3570,3572,3575,3577,3579],{"class":2487,"line":35},[2485,3564,3565],{"class":2685},"    store",[2485,3567,3235],{"class":2676},[2485,3569,2395],{"class":2516},[2485,3571,2507],{"class":2506},[2485,3573,3574],{"class":2516},"Store",[2485,3576,2513],{"class":2506},[2485,3578,2517],{"class":2516},[2485,3580,3581],{"class":2506},"]\n",[2485,3583,3584],{"class":2487,"line":1518},[2485,3585,3002],{"class":2506},[2485,3587,3588],{"class":2487,"line":2610},[2485,3589,2614],{"emptyLinePlaceholder":2613},[2485,3591,3592,3594,3597,3600,3603,3606,3608,3611,3613,3615,3617,3619,3622,3624,3626,3628,3630,3632,3635],{"class":2487,"line":2617},[2485,3593,3013],{"class":2878},[2485,3595,3596],{"class":2506}," (",[2485,3598,3599],{"class":2931},"s ",[2485,3601,3602],{"class":2676},"*",[2485,3604,3605],{"class":2516},"SessionStore",[2485,3607,2743],{"class":2506},[2485,3609,3610],{"class":2510}," Save",[2485,3612,2531],{"class":2506},[2485,3614,2645],{"class":2931},[2485,3616,3032],{"class":2516},[2485,3618,2507],{"class":2506},[2485,3620,3621],{"class":2516},"Context",[2485,3623,2601],{"class":2506},[2485,3625,3285],{"class":2931},[2485,3627,3235],{"class":2676},[2485,3629,2517],{"class":2516},[2485,3631,2743],{"class":2506},[2485,3633,3634],{"class":2516}," error",[2485,3636,2960],{"class":2506},[2485,3638,3639,3642,3645,3647,3649,3651,3653,3655,3657,3659,3662,3665,3667,3670,3672,3674,3676,3678,3680,3682,3684],{"class":2487,"line":2623},[2485,3640,3641],{"class":2676},"    return",[2485,3643,3644],{"class":2496}," s",[2485,3646,2507],{"class":2506},[2485,3648,2723],{"class":2496},[2485,3650,2507],{"class":2506},[2485,3652,576],{"class":2510},[2485,3654,2531],{"class":2506},[2485,3656,2645],{"class":2496},[2485,3658,2601],{"class":2506},[2485,3660,3661],{"class":2604}," \"session:\"",[2485,3663,3664],{"class":2496},"+session",[2485,3666,2507],{"class":2506},[2485,3668,3669],{"class":2496},"Token",[2485,3671,2601],{"class":2506},[2485,3673,3285],{"class":2496},[2485,3675,2601],{"class":2506},[2485,3677,3290],{"class":3231},[2485,3679,3293],{"class":2496},[2485,3681,2507],{"class":2506},[2485,3683,2703],{"class":2496},[2485,3685,2653],{"class":2506},[2485,3687,3688],{"class":2487,"line":2656},[2485,3689,3002],{"class":2506},[2485,3691,3692],{"class":2487,"line":2922},[2485,3693,2614],{"emptyLinePlaceholder":2613},[2485,3695,3696],{"class":2487,"line":2928},[2485,3697,3698],{"class":2490},"// Production: Redis\n",[2485,3700,3701,3703,3705,3707,3709,3711,3713,3715,3717,3719,3721,3723,3725,3728],{"class":2487,"line":2938},[2485,3702,2723],{"class":2496},[2485,3704,2500],{"class":2496},[2485,3706,2503],{"class":2496},[2485,3708,2507],{"class":2506},[2485,3710,1502],{"class":2510},[2485,3712,2513],{"class":2506},[2485,3714,2517],{"class":2516},[2485,3716,2520],{"class":2506},[2485,3718,2523],{"class":2496},[2485,3720,2507],{"class":2506},[2485,3722,2528],{"class":2510},[2485,3724,2531],{"class":2506},[2485,3726,3727],{"class":2496},"redisClient",[2485,3729,2537],{"class":2506},[2485,3731,3732],{"class":2487,"line":2943},[2485,3733,2614],{"emptyLinePlaceholder":2613},[2485,3735,3736],{"class":2487,"line":2948},[2485,3737,3738],{"class":2490},"// Development: embedded BadgerDB\n",[2485,3740,3741,3743,3745,3747,3749,3751,3753,3755,3757,3759,3761,3763,3765,3768],{"class":2487,"line":2963},[2485,3742,2723],{"class":2496},[2485,3744,2500],{"class":2496},[2485,3746,2503],{"class":2496},[2485,3748,2507],{"class":2506},[2485,3750,1502],{"class":2510},[2485,3752,2513],{"class":2506},[2485,3754,2517],{"class":2516},[2485,3756,2520],{"class":2506},[2485,3758,2558],{"class":2496},[2485,3760,2507],{"class":2506},[2485,3762,2528],{"class":2510},[2485,3764,2531],{"class":2506},[2485,3766,3767],{"class":2496},"localDB",[2485,3769,2537],{"class":2506},[2485,3771,3772],{"class":2487,"line":2975},[2485,3773,2614],{"emptyLinePlaceholder":2613},[2485,3775,3776],{"class":2487,"line":2987},[2485,3777,3778],{"class":2490},"// Testing: in-memory\n",[2485,3780,3781,3783,3785,3787,3789,3791,3793,3795,3797,3799,3801,3803,3805,3808],{"class":2487,"line":2999},[2485,3782,2723],{"class":2496},[2485,3784,2500],{"class":2496},[2485,3786,2503],{"class":2496},[2485,3788,2507],{"class":2506},[2485,3790,1502],{"class":2510},[2485,3792,2513],{"class":2506},[2485,3794,2517],{"class":2516},[2485,3796,2520],{"class":2506},[2485,3798,2558],{"class":2496},[2485,3800,2507],{"class":2506},[2485,3802,2528],{"class":2510},[2485,3804,2531],{"class":2506},[2485,3806,3807],{"class":2496},"memDB",[2485,3809,2537],{"class":2506},[2397,3811,3812],{},"One interface. Any backend. Zero vendor lock-in.",[2471,3814,3816],{"id":3815},"documentation","Documentation",[3493,3818,3819],{},[3496,3820,3821,3824],{},[2400,3822,6],{"href":3823},"docs/overview"," — Design philosophy and architecture",[3826,3827,2311],"h3",{"id":3828},"learn",[3493,3830,3831,3837,3842],{},[3496,3832,3833,3836],{},[2400,3834,77],{"href":3835},"docs/learn/quickstart"," — Get started in minutes",[3496,3838,3839,3841],{},[2400,3840,130],{"href":3432}," — Stores, buckets, databases, codecs",[3496,3843,3844,3846],{},[2400,3845,16],{"href":3456}," — Layer model, atomic views, concurrency",[3826,3848,2323],{"id":3849},"guides",[3493,3851,3852,3857,3862,3868,3874],{},[3496,3853,3854,3856],{},[2400,3855,401],{"href":3407}," — Setup and configuration for all backends",[3496,3858,3859,3861],{},[2400,3860,3420],{"href":3419}," — CRUD operations and batch processing",[3496,3863,3864,3867],{},[2400,3865,741],{"href":3866},"docs/guides/pagination"," — Listing and iterating large datasets",[3496,3869,3870,3873],{},[2400,3871,853],{"href":3872},"docs/guides/testing"," — Mocks, embedded DBs, testcontainers",[3496,3875,3876,3879],{},[2400,3877,947],{"href":3878},"docs/guides/best-practices"," — Key design, error handling, performance",[3826,3881,2338],{"id":3882},"cookbook",[3493,3884,3885,3892,3899],{},[3496,3886,3887,3891],{},[2400,3888,3890],{"href":3889},"docs/cookbook/caching","Caching"," — Cache-aside, read-through, TTL strategies",[3496,3893,3894,3898],{},[2400,3895,3897],{"href":3896},"docs/cookbook/migrations","Migrations"," — Switching providers without downtime",[3496,3900,3901,3905],{},[2400,3902,3904],{"href":3903},"docs/cookbook/multi-tenant","Multi-Tenant"," — Tenant isolation patterns",[3826,3907,2351],{"id":3908},"reference",[3493,3910,3911,3917],{},[3496,3912,3913,3916],{},[2400,3914,3915],{"href":3475},"API"," — Complete API documentation",[3496,3918,3919,3922],{},[2400,3920,401],{"href":3921},"docs/reference/providers"," — Provider-specific behaviors and limitations",[2471,3924,3926],{"id":3925},"contributing","Contributing",[2397,3928,3929,3930,3934],{},"See ",[2400,3931,3933],{"href":3932},"CONTRIBUTING","CONTRIBUTING.md"," for guidelines.",[2471,3936,2447],{"id":3937},"license",[2397,3939,3940,3941,3943],{},"MIT License — see ",[2400,3942,2444],{"href":2444}," for details.",[3945,3946,3947],"style",{},"html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}",{"title":29,"searchDepth":19,"depth":19,"links":3949},[3950,3951,3952,3953,3954,3955,3956,3962,3963],{"id":2473,"depth":19,"text":2474},{"id":2843,"depth":19,"text":2844},{"id":2867,"depth":19,"text":2868},{"id":3371,"depth":19,"text":3372},{"id":3490,"depth":19,"text":3491},{"id":3529,"depth":19,"text":3530},{"id":3815,"depth":19,"text":3816,"children":3957},[3958,3959,3960,3961],{"id":3828,"depth":35,"text":2311},{"id":3849,"depth":35,"text":2323},{"id":3882,"depth":35,"text":2338},{"id":3908,"depth":35,"text":2351},{"id":3925,"depth":19,"text":3926},{"id":3937,"depth":19,"text":2447},"md","book-open",{},"/readme",{"title":2388,"description":29},"readme","bg88cC-M_7vLodNtQwoXZ5_NiJ8ZwXfcPdQHtM2HTc8",{"id":3972,"title":3973,"body":3974,"description":29,"extension":3964,"icon":4013,"meta":4014,"navigation":2613,"path":4015,"seo":4016,"stem":4017,"__hash__":4018},"resources/security.md","Security",{"type":2390,"value":3975,"toc":4009},[3976,3980,3984,3991,3994,3998],[2393,3977,3979],{"id":3978},"security-policy","Security Policy",[2471,3981,3983],{"id":3982},"reporting-a-vulnerability","Reporting a Vulnerability",[2397,3985,3986,3987,2507],{},"Please report security vulnerabilities by emailing ",[2400,3988,3990],{"href":3989},"mailto:security@zoobzio.com","security@zoobzio.com",[2397,3992,3993],{},"Do not open a public issue for security vulnerabilities.",[2471,3995,3997],{"id":3996},"response-timeline","Response Timeline",[3493,3999,4000,4003,4006],{},[3496,4001,4002],{},"Acknowledgment: within 48 hours",[3496,4004,4005],{},"Initial assessment: within 1 week",[3496,4007,4008],{},"Fix timeline: depends on severity",{"title":29,"searchDepth":19,"depth":19,"links":4010},[4011,4012],{"id":3982,"depth":19,"text":3983},{"id":3996,"depth":19,"text":3997},"shield",{},"/security",{"title":3973,"description":29},"security","6BvZhKRNN9RxQpAWeuz3P-Iotz_T7Z7ugTXcNo-LTyU",{"id":4020,"title":3926,"body":4021,"description":29,"extension":3964,"icon":2482,"meta":4082,"navigation":2613,"path":4083,"seo":4084,"stem":3925,"__hash__":4085},"resources/contributing.md",{"type":2390,"value":4022,"toc":4078},[4023,4025,4047,4051,4071,4075],[2393,4024,3926],{"id":3925},[4026,4027,4028,4031,4034,4037,4044],"ol",{},[3496,4029,4030],{},"Fork the repository",[3496,4032,4033],{},"Create a feature branch",[3496,4035,4036],{},"Make changes with tests",[3496,4038,4039,4040,4043],{},"Run ",[2482,4041,4042],{},"make check"," to verify",[3496,4045,4046],{},"Submit a pull request",[2471,4048,4050],{"id":4049},"development","Development",[3493,4052,4053,4059,4065],{},[3496,4054,4055,4058],{},[2482,4056,4057],{},"make help"," — list available commands",[3496,4060,4061,4064],{},[2482,4062,4063],{},"make test"," — run all tests",[3496,4066,4067,4070],{},[2482,4068,4069],{},"make lint"," — run linters",[2471,4072,4074],{"id":4073},"code-of-conduct","Code of Conduct",[2397,4076,4077],{},"Be respectful. We're all here to build good software.",{"title":29,"searchDepth":19,"depth":19,"links":4079},[4080,4081],{"id":4049,"depth":19,"text":4050},{"id":4073,"depth":19,"text":4074},{},"/contributing",{"title":3926,"description":29},"Uh51sWT_BFO8Ft7nBSzGxo0pXQxY65rXPhK3VQXlFxg",{"id":4087,"title":558,"author":4088,"body":4089,"description":560,"extension":3964,"meta":7912,"navigation":2613,"path":557,"published":7913,"readtime":7914,"seo":7915,"stem":2330,"tags":7916,"updated":7919,"__hash__":7920},"grub/v1.0.18/3.guides/2.lifecycle.md","zoobzio",{"type":2390,"value":4090,"toc":7866},[4091,4094,4096,4099,4102,4105,4177,4186,4193,4196,4199,4289,4294,4315,4318,4321,4383,4389,4392,4395,4444,4454,4457,4460,4542,4547,4561,4564,4567,4684,4690,4693,4696,4797,4802,4813,4816,4819,4822,4997,5000,5003,5140,5146,5149,5152,5214,5217,5220,5251,5254,5257,5373,5381,5384,5387,5390,5458,5461,5464,5574,5582,5585,5588,5650,5653,5656,5687,5690,5693,5858,5861,5864,5938,5941,5944,6043,6046,6048,6139,6142,6144,6147,6340,6343,6571,6574,6670,6673,6752,6755,6758,6787,6790,6796,6848,6851,6886,6889,6892,6970,6976,6979,7243,7246,7457,7460,7561,7564,7567,7729,7732,7816,7823,7863],[2393,4092,558],{"id":4093},"lifecycle-operations",[2397,4095,564],{},[2471,4097,567],{"id":4098},"store-operations-key-value",[3826,4100,571],{"id":4101},"get",[2397,4103,4104],{},"Retrieves a value by key.",[2476,4106,4108],{"className":2478,"code":4107,"language":2480,"meta":29,"style":29},"session, err := store.Get(ctx, \"session:abc123\")\nif errors.Is(err, grub.ErrNotFound) {\n    // Key doesn't exist\n}\n",[2482,4109,4110,4138,4168,4173],{"__ignoreMap":29},[2485,4111,4112,4114,4116,4119,4121,4124,4126,4128,4130,4132,4134,4136],{"class":2487,"line":9},[2485,4113,2626],{"class":2496},[2485,4115,2601],{"class":2506},[2485,4117,4118],{"class":2496}," err",[2485,4120,2500],{"class":2496},[2485,4122,4123],{"class":2496}," store",[2485,4125,2507],{"class":2506},[2485,4127,571],{"class":2510},[2485,4129,2531],{"class":2506},[2485,4131,2645],{"class":2496},[2485,4133,2601],{"class":2506},[2485,4135,3280],{"class":2604},[2485,4137,2653],{"class":2506},[2485,4139,4140,4143,4146,4148,4151,4153,4156,4158,4160,4162,4164,4166],{"class":2487,"line":19},[2485,4141,4142],{"class":2676},"if",[2485,4144,4145],{"class":2496}," errors",[2485,4147,2507],{"class":2506},[2485,4149,4150],{"class":2510},"Is",[2485,4152,2531],{"class":2506},[2485,4154,4155],{"class":2496},"err",[2485,4157,2601],{"class":2506},[2485,4159,2503],{"class":2496},[2485,4161,2507],{"class":2506},[2485,4163,3465],{"class":2496},[2485,4165,2743],{"class":2506},[2485,4167,2960],{"class":2506},[2485,4169,4170],{"class":2487,"line":35},[2485,4171,4172],{"class":2490},"    // Key doesn't exist\n",[2485,4174,4175],{"class":2487,"line":1518},[2485,4176,3002],{"class":2506},[2397,4178,4179,4182,4183],{},[3499,4180,4181],{},"Returns:"," ",[2482,4184,4185],{},"(*T, error)",[3493,4187,4188],{},[3496,4189,4190,4192],{},[2482,4191,3465],{}," if key doesn't exist",[3826,4194,576],{"id":4195},"set",[2397,4197,4198],{},"Stores a value with optional TTL.",[2476,4200,4202],{"className":2478,"code":4201,"language":2480,"meta":29,"style":29},"// With TTL (expires in 1 hour)\nerr := store.Set(ctx, \"session:abc123\", &session, time.Hour)\n\n// Without TTL (never expires)\nerr := store.Set(ctx, \"config:app\", &config, 0)\n",[2482,4203,4204,4209,4245,4249,4254],{"__ignoreMap":29},[2485,4205,4206],{"class":2487,"line":9},[2485,4207,4208],{"class":2490},"// With TTL (expires in 1 hour)\n",[2485,4210,4211,4213,4215,4217,4219,4221,4223,4225,4227,4229,4231,4233,4235,4237,4239,4241,4243],{"class":2487,"line":19},[2485,4212,4155],{"class":2496},[2485,4214,2500],{"class":2496},[2485,4216,4123],{"class":2496},[2485,4218,2507],{"class":2506},[2485,4220,576],{"class":2510},[2485,4222,2531],{"class":2506},[2485,4224,2645],{"class":2496},[2485,4226,2601],{"class":2506},[2485,4228,3280],{"class":2604},[2485,4230,2601],{"class":2506},[2485,4232,2677],{"class":2676},[2485,4234,2626],{"class":2496},[2485,4236,2601],{"class":2506},[2485,4238,2698],{"class":2496},[2485,4240,2507],{"class":2506},[2485,4242,2703],{"class":2496},[2485,4244,2653],{"class":2506},[2485,4246,4247],{"class":2487,"line":35},[2485,4248,2614],{"emptyLinePlaceholder":2613},[2485,4250,4251],{"class":2487,"line":1518},[2485,4252,4253],{"class":2490},"// Without TTL (never expires)\n",[2485,4255,4256,4258,4260,4262,4264,4266,4268,4270,4272,4275,4277,4279,4282,4284,4287],{"class":2487,"line":2610},[2485,4257,4155],{"class":2496},[2485,4259,2500],{"class":2496},[2485,4261,4123],{"class":2496},[2485,4263,2507],{"class":2506},[2485,4265,576],{"class":2510},[2485,4267,2531],{"class":2506},[2485,4269,2645],{"class":2496},[2485,4271,2601],{"class":2506},[2485,4273,4274],{"class":2604}," \"config:app\"",[2485,4276,2601],{"class":2506},[2485,4278,2677],{"class":2676},[2485,4280,4281],{"class":2496},"config",[2485,4283,2601],{"class":2506},[2485,4285,4286],{"class":3231}," 0",[2485,4288,2653],{"class":2506},[2397,4290,4291],{},[3499,4292,4293],{},"TTL behavior:",[3493,4295,4296,4302,4308],{},[3496,4297,4298,4301],{},[2482,4299,4300],{},"ttl > 0",": Key expires after duration",[3496,4303,4304,4307],{},[2482,4305,4306],{},"ttl == 0",": No expiration",[3496,4309,4310,4311,4314],{},"BoltDB: Returns ",[2482,4312,4313],{},"ErrTTLNotSupported"," if ttl > 0",[3826,4316,581],{"id":4317},"delete",[2397,4319,4320],{},"Removes a key.",[2476,4322,4324],{"className":2478,"code":4323,"language":2480,"meta":29,"style":29},"err := store.Delete(ctx, \"session:abc123\")\nif errors.Is(err, grub.ErrNotFound) {\n    // Key didn't exist\n}\n",[2482,4325,4326,4348,4374,4379],{"__ignoreMap":29},[2485,4327,4328,4330,4332,4334,4336,4338,4340,4342,4344,4346],{"class":2487,"line":9},[2485,4329,4155],{"class":2496},[2485,4331,2500],{"class":2496},[2485,4333,4123],{"class":2496},[2485,4335,2507],{"class":2506},[2485,4337,581],{"class":2510},[2485,4339,2531],{"class":2506},[2485,4341,2645],{"class":2496},[2485,4343,2601],{"class":2506},[2485,4345,3280],{"class":2604},[2485,4347,2653],{"class":2506},[2485,4349,4350,4352,4354,4356,4358,4360,4362,4364,4366,4368,4370,4372],{"class":2487,"line":19},[2485,4351,4142],{"class":2676},[2485,4353,4145],{"class":2496},[2485,4355,2507],{"class":2506},[2485,4357,4150],{"class":2510},[2485,4359,2531],{"class":2506},[2485,4361,4155],{"class":2496},[2485,4363,2601],{"class":2506},[2485,4365,2503],{"class":2496},[2485,4367,2507],{"class":2506},[2485,4369,3465],{"class":2496},[2485,4371,2743],{"class":2506},[2485,4373,2960],{"class":2506},[2485,4375,4376],{"class":2487,"line":35},[2485,4377,4378],{"class":2490},"    // Key didn't exist\n",[2485,4380,4381],{"class":2487,"line":1518},[2485,4382,3002],{"class":2506},[2397,4384,4385,4182,4387,4192],{},[3499,4386,4181],{},[2482,4388,3465],{},[3826,4390,586],{"id":4391},"exists",[2397,4393,4394],{},"Checks if a key exists without loading the value.",[2476,4396,4398],{"className":2478,"code":4397,"language":2480,"meta":29,"style":29},"exists, err := store.Exists(ctx, \"session:abc123\")\nif exists {\n    // Key exists\n}\n",[2482,4399,4400,4426,4435,4440],{"__ignoreMap":29},[2485,4401,4402,4404,4406,4408,4410,4412,4414,4416,4418,4420,4422,4424],{"class":2487,"line":9},[2485,4403,4391],{"class":2496},[2485,4405,2601],{"class":2506},[2485,4407,4118],{"class":2496},[2485,4409,2500],{"class":2496},[2485,4411,4123],{"class":2496},[2485,4413,2507],{"class":2506},[2485,4415,586],{"class":2510},[2485,4417,2531],{"class":2506},[2485,4419,2645],{"class":2496},[2485,4421,2601],{"class":2506},[2485,4423,3280],{"class":2604},[2485,4425,2653],{"class":2506},[2485,4427,4428,4430,4433],{"class":2487,"line":19},[2485,4429,4142],{"class":2676},[2485,4431,4432],{"class":2496}," exists",[2485,4434,2960],{"class":2506},[2485,4436,4437],{"class":2487,"line":35},[2485,4438,4439],{"class":2490},"    // Key exists\n",[2485,4441,4442],{"class":2487,"line":1518},[2485,4443,3002],{"class":2506},[2397,4445,4446,4182,4448,4451,4452],{},[3499,4447,4181],{},[2482,4449,4450],{},"(bool, error)"," — never returns ",[2482,4453,3465],{},[3826,4455,591],{"id":4456},"list",[2397,4458,4459],{},"Lists keys matching a prefix.",[2476,4461,4463],{"className":2478,"code":4462,"language":2480,"meta":29,"style":29},"// List up to 100 keys with prefix \"session:\"\nkeys, err := store.List(ctx, \"session:\", 100)\n\n// List all keys (no limit)\nkeys, err := store.List(ctx, \"\", 0)\n",[2482,4464,4465,4470,4502,4506,4511],{"__ignoreMap":29},[2485,4466,4467],{"class":2487,"line":9},[2485,4468,4469],{"class":2490},"// List up to 100 keys with prefix \"session:\"\n",[2485,4471,4472,4475,4477,4479,4481,4483,4485,4487,4489,4491,4493,4495,4497,4500],{"class":2487,"line":19},[2485,4473,4474],{"class":2496},"keys",[2485,4476,2601],{"class":2506},[2485,4478,4118],{"class":2496},[2485,4480,2500],{"class":2496},[2485,4482,4123],{"class":2496},[2485,4484,2507],{"class":2506},[2485,4486,591],{"class":2510},[2485,4488,2531],{"class":2506},[2485,4490,2645],{"class":2496},[2485,4492,2601],{"class":2506},[2485,4494,3661],{"class":2604},[2485,4496,2601],{"class":2506},[2485,4498,4499],{"class":3231}," 100",[2485,4501,2653],{"class":2506},[2485,4503,4504],{"class":2487,"line":35},[2485,4505,2614],{"emptyLinePlaceholder":2613},[2485,4507,4508],{"class":2487,"line":1518},[2485,4509,4510],{"class":2490},"// List all keys (no limit)\n",[2485,4512,4513,4515,4517,4519,4521,4523,4525,4527,4529,4531,4533,4536,4538,4540],{"class":2487,"line":2610},[2485,4514,4474],{"class":2496},[2485,4516,2601],{"class":2506},[2485,4518,4118],{"class":2496},[2485,4520,2500],{"class":2496},[2485,4522,4123],{"class":2496},[2485,4524,2507],{"class":2506},[2485,4526,591],{"class":2510},[2485,4528,2531],{"class":2506},[2485,4530,2645],{"class":2496},[2485,4532,2601],{"class":2506},[2485,4534,4535],{"class":2604}," \"\"",[2485,4537,2601],{"class":2506},[2485,4539,4286],{"class":3231},[2485,4541,2653],{"class":2506},[2397,4543,4544],{},[3499,4545,4546],{},"Parameters:",[3493,4548,4549,4555],{},[3496,4550,4551,4554],{},[2482,4552,4553],{},"prefix",": Filter keys starting with this string (empty = all)",[3496,4556,4557,4560],{},[2482,4558,4559],{},"limit",": Maximum keys to return (0 = no limit)",[3826,4562,596],{"id":4563},"getbatch",[2397,4565,4566],{},"Retrieves multiple keys at once.",[2476,4568,4570],{"className":2478,"code":4569,"language":2480,"meta":29,"style":29},"keys := []string{\"user:1\", \"user:2\", \"user:3\"}\nresults, err := store.GetBatch(ctx, keys)\n\nfor key, user := range results {\n    fmt.Println(key, user.Name)\n}\n",[2482,4571,4572,4601,4629,4633,4656,4680],{"__ignoreMap":29},[2485,4573,4574,4576,4578,4581,4584,4586,4589,4591,4594,4596,4599],{"class":2487,"line":9},[2485,4575,4474],{"class":2496},[2485,4577,2500],{"class":2496},[2485,4579,4580],{"class":2506}," []",[2485,4582,4583],{"class":2516},"string",[2485,4585,2682],{"class":2506},[2485,4587,4588],{"class":2604},"\"user:1\"",[2485,4590,2601],{"class":2506},[2485,4592,4593],{"class":2604}," \"user:2\"",[2485,4595,2601],{"class":2506},[2485,4597,4598],{"class":2604}," \"user:3\"",[2485,4600,3002],{"class":2506},[2485,4602,4603,4606,4608,4610,4612,4614,4616,4618,4620,4622,4624,4627],{"class":2487,"line":19},[2485,4604,4605],{"class":2496},"results",[2485,4607,2601],{"class":2506},[2485,4609,4118],{"class":2496},[2485,4611,2500],{"class":2496},[2485,4613,4123],{"class":2496},[2485,4615,2507],{"class":2506},[2485,4617,596],{"class":2510},[2485,4619,2531],{"class":2506},[2485,4621,2645],{"class":2496},[2485,4623,2601],{"class":2506},[2485,4625,4626],{"class":2496}," keys",[2485,4628,2653],{"class":2506},[2485,4630,4631],{"class":2487,"line":35},[2485,4632,2614],{"emptyLinePlaceholder":2613},[2485,4634,4635,4638,4641,4643,4646,4648,4651,4654],{"class":2487,"line":1518},[2485,4636,4637],{"class":2676},"for",[2485,4639,4640],{"class":2496}," key",[2485,4642,2601],{"class":2506},[2485,4644,4645],{"class":2496}," user",[2485,4647,2500],{"class":2496},[2485,4649,4650],{"class":2676}," range",[2485,4652,4653],{"class":2496}," results",[2485,4655,2960],{"class":2506},[2485,4657,4658,4660,4662,4664,4666,4669,4671,4673,4675,4678],{"class":2487,"line":2610},[2485,4659,3344],{"class":2496},[2485,4661,2507],{"class":2506},[2485,4663,3349],{"class":2510},[2485,4665,2531],{"class":2506},[2485,4667,4668],{"class":2496},"key",[2485,4670,2601],{"class":2506},[2485,4672,4645],{"class":2496},[2485,4674,2507],{"class":2506},[2485,4676,4677],{"class":2496},"Name",[2485,4679,2653],{"class":2506},[2485,4681,4682],{"class":2487,"line":2617},[2485,4683,3002],{"class":2506},[2397,4685,4686,4689],{},[3499,4687,4688],{},"Behavior:"," Missing keys are omitted from the result map (no error).",[3826,4691,601],{"id":4692},"setbatch",[2397,4694,4695],{},"Stores multiple values at once.",[2476,4697,4699],{"className":2478,"code":4698,"language":2480,"meta":29,"style":29},"items := map[string]*User{\n    \"user:1\": {Name: \"Alice\"},\n    \"user:2\": {Name: \"Bob\"},\n}\nerr := store.SetBatch(ctx, items, time.Hour)\n",[2482,4700,4701,4724,4744,4762,4766],{"__ignoreMap":29},[2485,4702,4703,4706,4708,4711,4713,4715,4718,4720,4722],{"class":2487,"line":9},[2485,4704,4705],{"class":2496},"items",[2485,4707,2500],{"class":2496},[2485,4709,4710],{"class":2878}," map",[2485,4712,2513],{"class":2506},[2485,4714,4583],{"class":2516},[2485,4716,4717],{"class":2506},"]",[2485,4719,3602],{"class":2676},[2485,4721,2790],{"class":2516},[2485,4723,3178],{"class":2506},[2485,4725,4726,4729,4731,4734,4736,4738,4741],{"class":2487,"line":19},[2485,4727,4728],{"class":2604},"    \"user:1\"",[2485,4730,2689],{"class":2506},[2485,4732,4733],{"class":2506}," {",[2485,4735,4677],{"class":2685},[2485,4737,2689],{"class":2506},[2485,4739,4740],{"class":2604}," \"Alice\"",[2485,4742,4743],{"class":2506},"},\n",[2485,4745,4746,4749,4751,4753,4755,4757,4760],{"class":2487,"line":35},[2485,4747,4748],{"class":2604},"    \"user:2\"",[2485,4750,2689],{"class":2506},[2485,4752,4733],{"class":2506},[2485,4754,4677],{"class":2685},[2485,4756,2689],{"class":2506},[2485,4758,4759],{"class":2604}," \"Bob\"",[2485,4761,4743],{"class":2506},[2485,4763,4764],{"class":2487,"line":1518},[2485,4765,3002],{"class":2506},[2485,4767,4768,4770,4772,4774,4776,4778,4780,4782,4784,4787,4789,4791,4793,4795],{"class":2487,"line":2610},[2485,4769,4155],{"class":2496},[2485,4771,2500],{"class":2496},[2485,4773,4123],{"class":2496},[2485,4775,2507],{"class":2506},[2485,4777,601],{"class":2510},[2485,4779,2531],{"class":2506},[2485,4781,2645],{"class":2496},[2485,4783,2601],{"class":2506},[2485,4785,4786],{"class":2496}," items",[2485,4788,2601],{"class":2506},[2485,4790,2698],{"class":2496},[2485,4792,2507],{"class":2506},[2485,4794,2703],{"class":2496},[2485,4796,2653],{"class":2506},[2397,4798,4799],{},[3499,4800,4801],{},"Atomicity varies by provider:",[3493,4803,4804,4807,4810],{},[3496,4805,4806],{},"Redis: Pipelined (each operation independent)",[3496,4808,4809],{},"Badger: WriteBatch (atomic)",[3496,4811,4812],{},"Bolt: Single transaction (atomic)",[2471,4814,606],{"id":4815},"bucket-operations-blob",[3826,4817,571],{"id":4818},"get-1",[2397,4820,4821],{},"Retrieves an object with metadata.",[2476,4823,4825],{"className":2478,"code":4824,"language":2480,"meta":29,"style":29},"obj, err := bucket.Get(ctx, \"docs/report.json\")\nif errors.Is(err, grub.ErrNotFound) {\n    // Object doesn't exist\n}\n\n// Access payload\nfmt.Println(obj.Data.Title)\n\n// Access metadata\nfmt.Println(obj.ContentType)\nfmt.Println(obj.Size)\nfmt.Println(obj.Metadata[\"author\"])\n",[2482,4826,4827,4856,4882,4887,4891,4895,4900,4925,4929,4934,4953,4972],{"__ignoreMap":29},[2485,4828,4829,4832,4834,4836,4838,4841,4843,4845,4847,4849,4851,4854],{"class":2487,"line":9},[2485,4830,4831],{"class":2496},"obj",[2485,4833,2601],{"class":2506},[2485,4835,4118],{"class":2496},[2485,4837,2500],{"class":2496},[2485,4839,4840],{"class":2496}," bucket",[2485,4842,2507],{"class":2506},[2485,4844,571],{"class":2510},[2485,4846,2531],{"class":2506},[2485,4848,2645],{"class":2496},[2485,4850,2601],{"class":2506},[2485,4852,4853],{"class":2604}," \"docs/report.json\"",[2485,4855,2653],{"class":2506},[2485,4857,4858,4860,4862,4864,4866,4868,4870,4872,4874,4876,4878,4880],{"class":2487,"line":19},[2485,4859,4142],{"class":2676},[2485,4861,4145],{"class":2496},[2485,4863,2507],{"class":2506},[2485,4865,4150],{"class":2510},[2485,4867,2531],{"class":2506},[2485,4869,4155],{"class":2496},[2485,4871,2601],{"class":2506},[2485,4873,2503],{"class":2496},[2485,4875,2507],{"class":2506},[2485,4877,3465],{"class":2496},[2485,4879,2743],{"class":2506},[2485,4881,2960],{"class":2506},[2485,4883,4884],{"class":2487,"line":35},[2485,4885,4886],{"class":2490},"    // Object doesn't exist\n",[2485,4888,4889],{"class":2487,"line":1518},[2485,4890,3002],{"class":2506},[2485,4892,4893],{"class":2487,"line":2610},[2485,4894,2614],{"emptyLinePlaceholder":2613},[2485,4896,4897],{"class":2487,"line":2617},[2485,4898,4899],{"class":2490},"// Access payload\n",[2485,4901,4902,4905,4907,4909,4911,4913,4915,4918,4920,4923],{"class":2487,"line":2623},[2485,4903,4904],{"class":2496},"fmt",[2485,4906,2507],{"class":2506},[2485,4908,3349],{"class":2510},[2485,4910,2531],{"class":2506},[2485,4912,4831],{"class":2496},[2485,4914,2507],{"class":2506},[2485,4916,4917],{"class":2496},"Data",[2485,4919,2507],{"class":2506},[2485,4921,4922],{"class":2496},"Title",[2485,4924,2653],{"class":2506},[2485,4926,4927],{"class":2487,"line":2656},[2485,4928,2614],{"emptyLinePlaceholder":2613},[2485,4930,4931],{"class":2487,"line":2922},[2485,4932,4933],{"class":2490},"// Access metadata\n",[2485,4935,4936,4938,4940,4942,4944,4946,4948,4951],{"class":2487,"line":2928},[2485,4937,4904],{"class":2496},[2485,4939,2507],{"class":2506},[2485,4941,3349],{"class":2510},[2485,4943,2531],{"class":2506},[2485,4945,4831],{"class":2496},[2485,4947,2507],{"class":2506},[2485,4949,4950],{"class":2496},"ContentType",[2485,4952,2653],{"class":2506},[2485,4954,4955,4957,4959,4961,4963,4965,4967,4970],{"class":2487,"line":2938},[2485,4956,4904],{"class":2496},[2485,4958,2507],{"class":2506},[2485,4960,3349],{"class":2510},[2485,4962,2531],{"class":2506},[2485,4964,4831],{"class":2496},[2485,4966,2507],{"class":2506},[2485,4968,4969],{"class":2496},"Size",[2485,4971,2653],{"class":2506},[2485,4973,4974,4976,4978,4980,4982,4984,4986,4989,4991,4994],{"class":2487,"line":2943},[2485,4975,4904],{"class":2496},[2485,4977,2507],{"class":2506},[2485,4979,3349],{"class":2510},[2485,4981,2531],{"class":2506},[2485,4983,4831],{"class":2496},[2485,4985,2507],{"class":2506},[2485,4987,4988],{"class":2496},"Metadata",[2485,4990,2513],{"class":2506},[2485,4992,4993],{"class":2604},"\"author\"",[2485,4995,4996],{"class":2506},"])\n",[3826,4998,614],{"id":4999},"put",[2397,5001,5002],{},"Stores an object with metadata.",[2476,5004,5006],{"className":2478,"code":5005,"language":2480,"meta":29,"style":29},"err := bucket.Put(ctx, &grub.Object[Document]{\n    Key:         \"docs/report.json\",\n    ContentType: \"application/json\",\n    Metadata:    map[string]string{\"author\": \"alice\", \"version\": \"1.0\"},\n    Data:        Document{Title: \"Q4 Report\", Content: \"...\"},\n})\n",[2482,5007,5008,5042,5054,5066,5105,5136],{"__ignoreMap":29},[2485,5009,5010,5012,5014,5016,5018,5020,5022,5024,5026,5028,5030,5032,5035,5037,5039],{"class":2487,"line":9},[2485,5011,4155],{"class":2496},[2485,5013,2500],{"class":2496},[2485,5015,4840],{"class":2496},[2485,5017,2507],{"class":2506},[2485,5019,614],{"class":2510},[2485,5021,2531],{"class":2506},[2485,5023,2645],{"class":2496},[2485,5025,2601],{"class":2506},[2485,5027,2677],{"class":2676},[2485,5029,2395],{"class":2516},[2485,5031,2507],{"class":2506},[2485,5033,5034],{"class":2516},"Object",[2485,5036,2513],{"class":2506},[2485,5038,2764],{"class":2516},[2485,5040,5041],{"class":2506},"]{\n",[2485,5043,5044,5047,5049,5052],{"class":2487,"line":19},[2485,5045,5046],{"class":2685},"    Key",[2485,5048,2689],{"class":2506},[2485,5050,5051],{"class":2604},"         \"docs/report.json\"",[2485,5053,3192],{"class":2506},[2485,5055,5056,5059,5061,5064],{"class":2487,"line":35},[2485,5057,5058],{"class":2685},"    ContentType",[2485,5060,2689],{"class":2506},[2485,5062,5063],{"class":2604}," \"application/json\"",[2485,5065,3192],{"class":2506},[2485,5067,5068,5071,5073,5076,5078,5080,5082,5084,5086,5088,5090,5093,5095,5098,5100,5103],{"class":2487,"line":1518},[2485,5069,5070],{"class":2685},"    Metadata",[2485,5072,2689],{"class":2506},[2485,5074,5075],{"class":2878},"    map",[2485,5077,2513],{"class":2506},[2485,5079,4583],{"class":2516},[2485,5081,4717],{"class":2506},[2485,5083,4583],{"class":2516},[2485,5085,2682],{"class":2506},[2485,5087,4993],{"class":2604},[2485,5089,2689],{"class":2506},[2485,5091,5092],{"class":2604}," \"alice\"",[2485,5094,2601],{"class":2506},[2485,5096,5097],{"class":2604}," \"version\"",[2485,5099,2689],{"class":2506},[2485,5101,5102],{"class":2604}," \"1.0\"",[2485,5104,4743],{"class":2506},[2485,5106,5107,5110,5112,5115,5117,5119,5121,5124,5126,5129,5131,5134],{"class":2487,"line":2610},[2485,5108,5109],{"class":2685},"    Data",[2485,5111,2689],{"class":2506},[2485,5113,5114],{"class":2516},"        Document",[2485,5116,2682],{"class":2506},[2485,5118,4922],{"class":2685},[2485,5120,2689],{"class":2506},[2485,5122,5123],{"class":2604}," \"Q4 Report\"",[2485,5125,2601],{"class":2506},[2485,5127,5128],{"class":2685}," Content",[2485,5130,2689],{"class":2506},[2485,5132,5133],{"class":2604}," \"...\"",[2485,5135,4743],{"class":2506},[2485,5137,5138],{"class":2487,"line":2617},[2485,5139,3093],{"class":2506},[2397,5141,5142,5145],{},[3499,5143,5144],{},"Note:"," Size is computed from encoded data, not set manually.",[3826,5147,581],{"id":5148},"delete-1",[2397,5150,5151],{},"Removes an object.",[2476,5153,5155],{"className":2478,"code":5154,"language":2480,"meta":29,"style":29},"err := bucket.Delete(ctx, \"docs/report.json\")\nif errors.Is(err, grub.ErrNotFound) {\n    // Object didn't exist\n}\n",[2482,5156,5157,5179,5205,5210],{"__ignoreMap":29},[2485,5158,5159,5161,5163,5165,5167,5169,5171,5173,5175,5177],{"class":2487,"line":9},[2485,5160,4155],{"class":2496},[2485,5162,2500],{"class":2496},[2485,5164,4840],{"class":2496},[2485,5166,2507],{"class":2506},[2485,5168,581],{"class":2510},[2485,5170,2531],{"class":2506},[2485,5172,2645],{"class":2496},[2485,5174,2601],{"class":2506},[2485,5176,4853],{"class":2604},[2485,5178,2653],{"class":2506},[2485,5180,5181,5183,5185,5187,5189,5191,5193,5195,5197,5199,5201,5203],{"class":2487,"line":19},[2485,5182,4142],{"class":2676},[2485,5184,4145],{"class":2496},[2485,5186,2507],{"class":2506},[2485,5188,4150],{"class":2510},[2485,5190,2531],{"class":2506},[2485,5192,4155],{"class":2496},[2485,5194,2601],{"class":2506},[2485,5196,2503],{"class":2496},[2485,5198,2507],{"class":2506},[2485,5200,3465],{"class":2496},[2485,5202,2743],{"class":2506},[2485,5204,2960],{"class":2506},[2485,5206,5207],{"class":2487,"line":35},[2485,5208,5209],{"class":2490},"    // Object didn't exist\n",[2485,5211,5212],{"class":2487,"line":1518},[2485,5213,3002],{"class":2506},[3826,5215,586],{"id":5216},"exists-1",[2397,5218,5219],{},"Checks if an object exists.",[2476,5221,5223],{"className":2478,"code":5222,"language":2480,"meta":29,"style":29},"exists, err := bucket.Exists(ctx, \"docs/report.json\")\n",[2482,5224,5225],{"__ignoreMap":29},[2485,5226,5227,5229,5231,5233,5235,5237,5239,5241,5243,5245,5247,5249],{"class":2487,"line":9},[2485,5228,4391],{"class":2496},[2485,5230,2601],{"class":2506},[2485,5232,4118],{"class":2496},[2485,5234,2500],{"class":2496},[2485,5236,4840],{"class":2496},[2485,5238,2507],{"class":2506},[2485,5240,586],{"class":2510},[2485,5242,2531],{"class":2506},[2485,5244,2645],{"class":2496},[2485,5246,2601],{"class":2506},[2485,5248,4853],{"class":2604},[2485,5250,2653],{"class":2506},[3826,5252,591],{"id":5253},"list-1",[2397,5255,5256],{},"Lists objects matching a prefix (metadata only).",[2476,5258,5260],{"className":2478,"code":5259,"language":2480,"meta":29,"style":29},"infos, err := bucket.List(ctx, \"docs/\", 100)\n\nfor _, info := range infos {\n    fmt.Printf(\"%s (%d bytes)\\n\", info.Key, info.Size)\n}\n",[2482,5261,5262,5294,5298,5318,5369],{"__ignoreMap":29},[2485,5263,5264,5267,5269,5271,5273,5275,5277,5279,5281,5283,5285,5288,5290,5292],{"class":2487,"line":9},[2485,5265,5266],{"class":2496},"infos",[2485,5268,2601],{"class":2506},[2485,5270,4118],{"class":2496},[2485,5272,2500],{"class":2496},[2485,5274,4840],{"class":2496},[2485,5276,2507],{"class":2506},[2485,5278,591],{"class":2510},[2485,5280,2531],{"class":2506},[2485,5282,2645],{"class":2496},[2485,5284,2601],{"class":2506},[2485,5286,5287],{"class":2604}," \"docs/\"",[2485,5289,2601],{"class":2506},[2485,5291,4499],{"class":3231},[2485,5293,2653],{"class":2506},[2485,5295,5296],{"class":2487,"line":19},[2485,5297,2614],{"emptyLinePlaceholder":2613},[2485,5299,5300,5302,5304,5306,5309,5311,5313,5316],{"class":2487,"line":35},[2485,5301,4637],{"class":2676},[2485,5303,2631],{"class":2496},[2485,5305,2601],{"class":2506},[2485,5307,5308],{"class":2496}," info",[2485,5310,2500],{"class":2496},[2485,5312,4650],{"class":2676},[2485,5314,5315],{"class":2496}," infos",[2485,5317,2960],{"class":2506},[2485,5319,5320,5322,5324,5327,5329,5332,5336,5338,5341,5344,5348,5350,5352,5354,5356,5359,5361,5363,5365,5367],{"class":2487,"line":1518},[2485,5321,3344],{"class":2496},[2485,5323,2507],{"class":2506},[2485,5325,5326],{"class":2510},"Printf",[2485,5328,2531],{"class":2506},[2485,5330,5331],{"class":2604},"\"",[2485,5333,5335],{"class":5334},"scyPU","%s",[2485,5337,3596],{"class":2604},[2485,5339,5340],{"class":5334},"%d",[2485,5342,5343],{"class":2604}," bytes)",[2485,5345,5347],{"class":5346},"suWN2","\\n",[2485,5349,5331],{"class":2604},[2485,5351,2601],{"class":2506},[2485,5353,5308],{"class":2496},[2485,5355,2507],{"class":2506},[2485,5357,5358],{"class":2496},"Key",[2485,5360,2601],{"class":2506},[2485,5362,5308],{"class":2496},[2485,5364,2507],{"class":2506},[2485,5366,4969],{"class":2496},[2485,5368,2653],{"class":2506},[2485,5370,5371],{"class":2487,"line":2610},[2485,5372,3002],{"class":2506},[2397,5374,5375,4182,5377,5380],{},[3499,5376,4181],{},[2482,5378,5379],{},"[]ObjectInfo"," — metadata without payload",[2471,5382,631],{"id":5383},"database-operations-sql",[3826,5385,571],{"id":5386},"get-2",[2397,5388,5389],{},"Retrieves a record by primary key.",[2476,5391,5393],{"className":2478,"code":5392,"language":2480,"meta":29,"style":29},"user, err := db.Get(ctx, \"123\")\nif errors.Is(err, grub.ErrNotFound) {\n    // Record doesn't exist\n}\n",[2482,5394,5395,5423,5449,5454],{"__ignoreMap":29},[2485,5396,5397,5400,5402,5404,5406,5409,5411,5413,5415,5417,5419,5421],{"class":2487,"line":9},[2485,5398,5399],{"class":2496},"user",[2485,5401,2601],{"class":2506},[2485,5403,4118],{"class":2496},[2485,5405,2500],{"class":2496},[2485,5407,5408],{"class":2496}," db",[2485,5410,2507],{"class":2506},[2485,5412,571],{"class":2510},[2485,5414,2531],{"class":2506},[2485,5416,2645],{"class":2496},[2485,5418,2601],{"class":2506},[2485,5420,2692],{"class":2604},[2485,5422,2653],{"class":2506},[2485,5424,5425,5427,5429,5431,5433,5435,5437,5439,5441,5443,5445,5447],{"class":2487,"line":19},[2485,5426,4142],{"class":2676},[2485,5428,4145],{"class":2496},[2485,5430,2507],{"class":2506},[2485,5432,4150],{"class":2510},[2485,5434,2531],{"class":2506},[2485,5436,4155],{"class":2496},[2485,5438,2601],{"class":2506},[2485,5440,2503],{"class":2496},[2485,5442,2507],{"class":2506},[2485,5444,3465],{"class":2496},[2485,5446,2743],{"class":2506},[2485,5448,2960],{"class":2506},[2485,5450,5451],{"class":2487,"line":35},[2485,5452,5453],{"class":2490},"    // Record doesn't exist\n",[2485,5455,5456],{"class":2487,"line":1518},[2485,5457,3002],{"class":2506},[3826,5459,576],{"id":5460},"set-1",[2397,5462,5463],{},"Upserts a record (insert or update on conflict).",[2476,5465,5467],{"className":2478,"code":5466,"language":2480,"meta":29,"style":29},"// Insert new record\nerr := db.Set(ctx, \"123\", &User{ID: \"123\", Name: \"Alice\"})\n\n// Update existing record (same key)\nerr := db.Set(ctx, \"123\", &User{ID: \"123\", Name: \"Alice Smith\"})\n",[2482,5468,5469,5474,5520,5524,5529],{"__ignoreMap":29},[2485,5470,5471],{"class":2487,"line":9},[2485,5472,5473],{"class":2490},"// Insert new record\n",[2485,5475,5476,5478,5480,5482,5484,5486,5488,5490,5492,5494,5496,5498,5500,5502,5505,5507,5509,5511,5514,5516,5518],{"class":2487,"line":19},[2485,5477,4155],{"class":2496},[2485,5479,2500],{"class":2496},[2485,5481,5408],{"class":2496},[2485,5483,2507],{"class":2506},[2485,5485,576],{"class":2510},[2485,5487,2531],{"class":2506},[2485,5489,2645],{"class":2496},[2485,5491,2601],{"class":2506},[2485,5493,2692],{"class":2604},[2485,5495,2601],{"class":2506},[2485,5497,2677],{"class":2676},[2485,5499,2790],{"class":2516},[2485,5501,2682],{"class":2506},[2485,5503,5504],{"class":2685},"ID",[2485,5506,2689],{"class":2506},[2485,5508,2692],{"class":2604},[2485,5510,2601],{"class":2506},[2485,5512,5513],{"class":2685}," Name",[2485,5515,2689],{"class":2506},[2485,5517,4740],{"class":2604},[2485,5519,3093],{"class":2506},[2485,5521,5522],{"class":2487,"line":35},[2485,5523,2614],{"emptyLinePlaceholder":2613},[2485,5525,5526],{"class":2487,"line":1518},[2485,5527,5528],{"class":2490},"// Update existing record (same key)\n",[2485,5530,5531,5533,5535,5537,5539,5541,5543,5545,5547,5549,5551,5553,5555,5557,5559,5561,5563,5565,5567,5569,5572],{"class":2487,"line":2610},[2485,5532,4155],{"class":2496},[2485,5534,2500],{"class":2496},[2485,5536,5408],{"class":2496},[2485,5538,2507],{"class":2506},[2485,5540,576],{"class":2510},[2485,5542,2531],{"class":2506},[2485,5544,2645],{"class":2496},[2485,5546,2601],{"class":2506},[2485,5548,2692],{"class":2604},[2485,5550,2601],{"class":2506},[2485,5552,2677],{"class":2676},[2485,5554,2790],{"class":2516},[2485,5556,2682],{"class":2506},[2485,5558,5504],{"class":2685},[2485,5560,2689],{"class":2506},[2485,5562,2692],{"class":2604},[2485,5564,2601],{"class":2506},[2485,5566,5513],{"class":2685},[2485,5568,2689],{"class":2506},[2485,5570,5571],{"class":2604}," \"Alice Smith\"",[2485,5573,3093],{"class":2506},[2397,5575,5576,5578,5579,2507],{},[3499,5577,4688],{}," Always upserts. Uses ",[2482,5580,5581],{},"INSERT ... ON CONFLICT DO UPDATE",[3826,5583,581],{"id":5584},"delete-2",[2397,5586,5587],{},"Removes a record by primary key.",[2476,5589,5591],{"className":2478,"code":5590,"language":2480,"meta":29,"style":29},"err := db.Delete(ctx, \"123\")\nif errors.Is(err, grub.ErrNotFound) {\n    // Record didn't exist\n}\n",[2482,5592,5593,5615,5641,5646],{"__ignoreMap":29},[2485,5594,5595,5597,5599,5601,5603,5605,5607,5609,5611,5613],{"class":2487,"line":9},[2485,5596,4155],{"class":2496},[2485,5598,2500],{"class":2496},[2485,5600,5408],{"class":2496},[2485,5602,2507],{"class":2506},[2485,5604,581],{"class":2510},[2485,5606,2531],{"class":2506},[2485,5608,2645],{"class":2496},[2485,5610,2601],{"class":2506},[2485,5612,2692],{"class":2604},[2485,5614,2653],{"class":2506},[2485,5616,5617,5619,5621,5623,5625,5627,5629,5631,5633,5635,5637,5639],{"class":2487,"line":19},[2485,5618,4142],{"class":2676},[2485,5620,4145],{"class":2496},[2485,5622,2507],{"class":2506},[2485,5624,4150],{"class":2510},[2485,5626,2531],{"class":2506},[2485,5628,4155],{"class":2496},[2485,5630,2601],{"class":2506},[2485,5632,2503],{"class":2496},[2485,5634,2507],{"class":2506},[2485,5636,3465],{"class":2496},[2485,5638,2743],{"class":2506},[2485,5640,2960],{"class":2506},[2485,5642,5643],{"class":2487,"line":35},[2485,5644,5645],{"class":2490},"    // Record didn't exist\n",[2485,5647,5648],{"class":2487,"line":1518},[2485,5649,3002],{"class":2506},[3826,5651,586],{"id":5652},"exists-2",[2397,5654,5655],{},"Checks if a record exists.",[2476,5657,5659],{"className":2478,"code":5658,"language":2480,"meta":29,"style":29},"exists, err := db.Exists(ctx, \"123\")\n",[2482,5660,5661],{"__ignoreMap":29},[2485,5662,5663,5665,5667,5669,5671,5673,5675,5677,5679,5681,5683,5685],{"class":2487,"line":9},[2485,5664,4391],{"class":2496},[2485,5666,2601],{"class":2506},[2485,5668,4118],{"class":2496},[2485,5670,2500],{"class":2496},[2485,5672,5408],{"class":2496},[2485,5674,2507],{"class":2506},[2485,5676,586],{"class":2510},[2485,5678,2531],{"class":2506},[2485,5680,2645],{"class":2496},[2485,5682,2601],{"class":2506},[2485,5684,2692],{"class":2604},[2485,5686,2653],{"class":2506},[3826,5688,651],{"id":5689},"query-builder",[2397,5691,5692],{},"Returns a query builder for fetching multiple records.",[2476,5694,5696],{"className":2478,"code":5695,"language":2480,"meta":29,"style":29},"// Using the query builder\nusers, err := db.Query().\n    Where(\"status\", \"=\", \"active\").\n    Exec(ctx, map[string]any{\"active\": \"enabled\"})\n\n// With parameters\nusers, err := db.Query().\n    Where(\"role\", \"=\", \"role\").\n    Exec(ctx, map[string]any{\"role\": \"admin\"})\n",[2482,5697,5698,5703,5723,5746,5780,5784,5789,5807,5827],{"__ignoreMap":29},[2485,5699,5700],{"class":2487,"line":9},[2485,5701,5702],{"class":2490},"// Using the query builder\n",[2485,5704,5705,5708,5710,5712,5714,5716,5718,5720],{"class":2487,"line":19},[2485,5706,5707],{"class":2496},"users",[2485,5709,2601],{"class":2506},[2485,5711,4118],{"class":2496},[2485,5713,2500],{"class":2496},[2485,5715,5408],{"class":2496},[2485,5717,2507],{"class":2506},[2485,5719,1626],{"class":2510},[2485,5721,5722],{"class":2506},"().\n",[2485,5724,5725,5728,5730,5733,5735,5738,5740,5743],{"class":2487,"line":35},[2485,5726,5727],{"class":2510},"    Where",[2485,5729,2531],{"class":2506},[2485,5731,5732],{"class":2604},"\"status\"",[2485,5734,2601],{"class":2506},[2485,5736,5737],{"class":2604}," \"=\"",[2485,5739,2601],{"class":2506},[2485,5741,5742],{"class":2604}," \"active\"",[2485,5744,5745],{"class":2506},").\n",[2485,5747,5748,5751,5753,5755,5757,5759,5761,5763,5765,5768,5770,5773,5775,5778],{"class":2487,"line":1518},[2485,5749,5750],{"class":2510},"    Exec",[2485,5752,2531],{"class":2506},[2485,5754,2645],{"class":2496},[2485,5756,2601],{"class":2506},[2485,5758,4710],{"class":2878},[2485,5760,2513],{"class":2506},[2485,5762,4583],{"class":2516},[2485,5764,4717],{"class":2506},[2485,5766,5767],{"class":2516},"any",[2485,5769,2682],{"class":2506},[2485,5771,5772],{"class":2604},"\"active\"",[2485,5774,2689],{"class":2506},[2485,5776,5777],{"class":2604}," \"enabled\"",[2485,5779,3093],{"class":2506},[2485,5781,5782],{"class":2487,"line":2610},[2485,5783,2614],{"emptyLinePlaceholder":2613},[2485,5785,5786],{"class":2487,"line":2617},[2485,5787,5788],{"class":2490},"// With parameters\n",[2485,5790,5791,5793,5795,5797,5799,5801,5803,5805],{"class":2487,"line":2623},[2485,5792,5707],{"class":2496},[2485,5794,2601],{"class":2506},[2485,5796,4118],{"class":2496},[2485,5798,2500],{"class":2496},[2485,5800,5408],{"class":2496},[2485,5802,2507],{"class":2506},[2485,5804,1626],{"class":2510},[2485,5806,5722],{"class":2506},[2485,5808,5809,5811,5813,5816,5818,5820,5822,5825],{"class":2487,"line":2656},[2485,5810,5727],{"class":2510},[2485,5812,2531],{"class":2506},[2485,5814,5815],{"class":2604},"\"role\"",[2485,5817,2601],{"class":2506},[2485,5819,5737],{"class":2604},[2485,5821,2601],{"class":2506},[2485,5823,5824],{"class":2604}," \"role\"",[2485,5826,5745],{"class":2506},[2485,5828,5829,5831,5833,5835,5837,5839,5841,5843,5845,5847,5849,5851,5853,5856],{"class":2487,"line":2922},[2485,5830,5750],{"class":2510},[2485,5832,2531],{"class":2506},[2485,5834,2645],{"class":2496},[2485,5836,2601],{"class":2506},[2485,5838,4710],{"class":2878},[2485,5840,2513],{"class":2506},[2485,5842,4583],{"class":2516},[2485,5844,4717],{"class":2506},[2485,5846,5767],{"class":2516},[2485,5848,2682],{"class":2506},[2485,5850,5815],{"class":2604},[2485,5852,2689],{"class":2506},[2485,5854,5855],{"class":2604}," \"admin\"",[2485,5857,3093],{"class":2506},[3826,5859,656],{"id":5860},"select-builder",[2397,5862,5863],{},"Returns a select builder for fetching a single record.",[2476,5865,5867],{"className":2478,"code":5866,"language":2480,"meta":29,"style":29},"user, err := db.Select().\n    Where(\"email\", \"=\", \"email\").\n    Exec(ctx, map[string]any{\"email\": \"alice@example.com\"})\n",[2482,5868,5869,5887,5907],{"__ignoreMap":29},[2485,5870,5871,5873,5875,5877,5879,5881,5883,5885],{"class":2487,"line":9},[2485,5872,5399],{"class":2496},[2485,5874,2601],{"class":2506},[2485,5876,4118],{"class":2496},[2485,5878,2500],{"class":2496},[2485,5880,5408],{"class":2496},[2485,5882,2507],{"class":2506},[2485,5884,1631],{"class":2510},[2485,5886,5722],{"class":2506},[2485,5888,5889,5891,5893,5896,5898,5900,5902,5905],{"class":2487,"line":19},[2485,5890,5727],{"class":2510},[2485,5892,2531],{"class":2506},[2485,5894,5895],{"class":2604},"\"email\"",[2485,5897,2601],{"class":2506},[2485,5899,5737],{"class":2604},[2485,5901,2601],{"class":2506},[2485,5903,5904],{"class":2604}," \"email\"",[2485,5906,5745],{"class":2506},[2485,5908,5909,5911,5913,5915,5917,5919,5921,5923,5925,5927,5929,5931,5933,5936],{"class":2487,"line":35},[2485,5910,5750],{"class":2510},[2485,5912,2531],{"class":2506},[2485,5914,2645],{"class":2496},[2485,5916,2601],{"class":2506},[2485,5918,4710],{"class":2878},[2485,5920,2513],{"class":2506},[2485,5922,4583],{"class":2516},[2485,5924,4717],{"class":2506},[2485,5926,5767],{"class":2516},[2485,5928,2682],{"class":2506},[2485,5930,5895],{"class":2604},[2485,5932,2689],{"class":2506},[2485,5934,5935],{"class":2604}," \"alice@example.com\"",[2485,5937,3093],{"class":2506},[3826,5939,661],{"id":5940},"modify-builder",[2397,5942,5943],{},"Returns an update builder for modifying records.",[2476,5945,5947],{"className":2478,"code":5946,"language":2480,"meta":29,"style":29},"user, err := db.Modify().\n    Set(\"status\", \"new_status\").\n    Where(\"id\", \"=\", \"user_id\").\n    Exec(ctx, map[string]any{\"new_status\": \"inactive\", \"user_id\": \"123\"})\n",[2482,5948,5949,5967,5983,6003],{"__ignoreMap":29},[2485,5950,5951,5953,5955,5957,5959,5961,5963,5965],{"class":2487,"line":9},[2485,5952,5399],{"class":2496},[2485,5954,2601],{"class":2506},[2485,5956,4118],{"class":2496},[2485,5958,2500],{"class":2496},[2485,5960,5408],{"class":2496},[2485,5962,2507],{"class":2506},[2485,5964,1646],{"class":2510},[2485,5966,5722],{"class":2506},[2485,5968,5969,5972,5974,5976,5978,5981],{"class":2487,"line":19},[2485,5970,5971],{"class":2510},"    Set",[2485,5973,2531],{"class":2506},[2485,5975,5732],{"class":2604},[2485,5977,2601],{"class":2506},[2485,5979,5980],{"class":2604}," \"new_status\"",[2485,5982,5745],{"class":2506},[2485,5984,5985,5987,5989,5992,5994,5996,5998,6001],{"class":2487,"line":35},[2485,5986,5727],{"class":2510},[2485,5988,2531],{"class":2506},[2485,5990,5991],{"class":2604},"\"id\"",[2485,5993,2601],{"class":2506},[2485,5995,5737],{"class":2604},[2485,5997,2601],{"class":2506},[2485,5999,6000],{"class":2604}," \"user_id\"",[2485,6002,5745],{"class":2506},[2485,6004,6005,6007,6009,6011,6013,6015,6017,6019,6021,6023,6025,6028,6030,6033,6035,6037,6039,6041],{"class":2487,"line":1518},[2485,6006,5750],{"class":2510},[2485,6008,2531],{"class":2506},[2485,6010,2645],{"class":2496},[2485,6012,2601],{"class":2506},[2485,6014,4710],{"class":2878},[2485,6016,2513],{"class":2506},[2485,6018,4583],{"class":2516},[2485,6020,4717],{"class":2506},[2485,6022,5767],{"class":2516},[2485,6024,2682],{"class":2506},[2485,6026,6027],{"class":2604},"\"new_status\"",[2485,6029,2689],{"class":2506},[2485,6031,6032],{"class":2604}," \"inactive\"",[2485,6034,2601],{"class":2506},[2485,6036,6000],{"class":2604},[2485,6038,2689],{"class":2506},[2485,6040,2692],{"class":2604},[2485,6042,3093],{"class":2506},[3826,6044,666],{"id":6045},"statement-execution",[2397,6047,1662],{},[2476,6049,6051],{"className":2478,"code":6050,"language":2480,"meta":29,"style":29},"// Query all records\nusers, err := db.ExecQuery(ctx, grub.QueryAll, nil)\n\n// Count records\ncount, err := db.ExecAggregate(ctx, grub.CountAll, nil)\n",[2482,6052,6053,6058,6094,6098,6103],{"__ignoreMap":29},[2485,6054,6055],{"class":2487,"line":9},[2485,6056,6057],{"class":2490},"// Query all records\n",[2485,6059,6060,6062,6064,6066,6068,6070,6072,6074,6076,6078,6080,6082,6084,6087,6089,6092],{"class":2487,"line":19},[2485,6061,5707],{"class":2496},[2485,6063,2601],{"class":2506},[2485,6065,4118],{"class":2496},[2485,6067,2500],{"class":2496},[2485,6069,5408],{"class":2496},[2485,6071,2507],{"class":2506},[2485,6073,1665],{"class":2510},[2485,6075,2531],{"class":2506},[2485,6077,2645],{"class":2496},[2485,6079,2601],{"class":2506},[2485,6081,2503],{"class":2496},[2485,6083,2507],{"class":2506},[2485,6085,6086],{"class":2496},"QueryAll",[2485,6088,2601],{"class":2506},[2485,6090,6091],{"class":2878}," nil",[2485,6093,2653],{"class":2506},[2485,6095,6096],{"class":2487,"line":35},[2485,6097,2614],{"emptyLinePlaceholder":2613},[2485,6099,6100],{"class":2487,"line":1518},[2485,6101,6102],{"class":2490},"// Count records\n",[2485,6104,6105,6108,6110,6112,6114,6116,6118,6120,6122,6124,6126,6128,6130,6133,6135,6137],{"class":2487,"line":2610},[2485,6106,6107],{"class":2496},"count",[2485,6109,2601],{"class":2506},[2485,6111,4118],{"class":2496},[2485,6113,2500],{"class":2496},[2485,6115,5408],{"class":2496},[2485,6117,2507],{"class":2506},[2485,6119,1680],{"class":2510},[2485,6121,2531],{"class":2506},[2485,6123,2645],{"class":2496},[2485,6125,2601],{"class":2506},[2485,6127,2503],{"class":2496},[2485,6129,2507],{"class":2506},[2485,6131,6132],{"class":2496},"CountAll",[2485,6134,2601],{"class":2506},[2485,6136,6091],{"class":2878},[2485,6138,2653],{"class":2506},[2471,6140,234],{"id":6141},"lifecycle-hooks",[2397,6143,672],{},[3826,6145,239],{"id":6146},"hook-interfaces",[2476,6148,6150],{"className":2478,"code":6149,"language":2480,"meta":29,"style":29},"type BeforeSave interface {\n    BeforeSave(ctx context.Context) error\n}\n\ntype AfterSave interface {\n    AfterSave(ctx context.Context) error\n}\n\ntype AfterLoad interface {\n    AfterLoad(ctx context.Context) error\n}\n\ntype BeforeDelete interface {\n    BeforeDelete(ctx context.Context) error\n}\n\ntype AfterDelete interface {\n    AfterDelete(ctx context.Context) error\n}\n",[2482,6151,6152,6164,6184,6188,6192,6203,6222,6226,6230,6241,6260,6264,6268,6279,6298,6302,6306,6317,6336],{"__ignoreMap":29},[2485,6153,6154,6156,6159,6162],{"class":2487,"line":9},[2485,6155,2951],{"class":2878},[2485,6157,6158],{"class":2516}," BeforeSave",[2485,6160,6161],{"class":2878}," interface",[2485,6163,2960],{"class":2506},[2485,6165,6166,6169,6171,6173,6175,6177,6179,6181],{"class":2487,"line":19},[2485,6167,6168],{"class":2510},"    BeforeSave",[2485,6170,2531],{"class":2506},[2485,6172,2645],{"class":2931},[2485,6174,3032],{"class":2516},[2485,6176,2507],{"class":2506},[2485,6178,3621],{"class":2516},[2485,6180,2743],{"class":2506},[2485,6182,6183],{"class":2516}," error\n",[2485,6185,6186],{"class":2487,"line":35},[2485,6187,3002],{"class":2506},[2485,6189,6190],{"class":2487,"line":1518},[2485,6191,2614],{"emptyLinePlaceholder":2613},[2485,6193,6194,6196,6199,6201],{"class":2487,"line":2610},[2485,6195,2951],{"class":2878},[2485,6197,6198],{"class":2516}," AfterSave",[2485,6200,6161],{"class":2878},[2485,6202,2960],{"class":2506},[2485,6204,6205,6208,6210,6212,6214,6216,6218,6220],{"class":2487,"line":2617},[2485,6206,6207],{"class":2510},"    AfterSave",[2485,6209,2531],{"class":2506},[2485,6211,2645],{"class":2931},[2485,6213,3032],{"class":2516},[2485,6215,2507],{"class":2506},[2485,6217,3621],{"class":2516},[2485,6219,2743],{"class":2506},[2485,6221,6183],{"class":2516},[2485,6223,6224],{"class":2487,"line":2623},[2485,6225,3002],{"class":2506},[2485,6227,6228],{"class":2487,"line":2656},[2485,6229,2614],{"emptyLinePlaceholder":2613},[2485,6231,6232,6234,6237,6239],{"class":2487,"line":2922},[2485,6233,2951],{"class":2878},[2485,6235,6236],{"class":2516}," AfterLoad",[2485,6238,6161],{"class":2878},[2485,6240,2960],{"class":2506},[2485,6242,6243,6246,6248,6250,6252,6254,6256,6258],{"class":2487,"line":2928},[2485,6244,6245],{"class":2510},"    AfterLoad",[2485,6247,2531],{"class":2506},[2485,6249,2645],{"class":2931},[2485,6251,3032],{"class":2516},[2485,6253,2507],{"class":2506},[2485,6255,3621],{"class":2516},[2485,6257,2743],{"class":2506},[2485,6259,6183],{"class":2516},[2485,6261,6262],{"class":2487,"line":2938},[2485,6263,3002],{"class":2506},[2485,6265,6266],{"class":2487,"line":2943},[2485,6267,2614],{"emptyLinePlaceholder":2613},[2485,6269,6270,6272,6275,6277],{"class":2487,"line":2948},[2485,6271,2951],{"class":2878},[2485,6273,6274],{"class":2516}," BeforeDelete",[2485,6276,6161],{"class":2878},[2485,6278,2960],{"class":2506},[2485,6280,6281,6284,6286,6288,6290,6292,6294,6296],{"class":2487,"line":2963},[2485,6282,6283],{"class":2510},"    BeforeDelete",[2485,6285,2531],{"class":2506},[2485,6287,2645],{"class":2931},[2485,6289,3032],{"class":2516},[2485,6291,2507],{"class":2506},[2485,6293,3621],{"class":2516},[2485,6295,2743],{"class":2506},[2485,6297,6183],{"class":2516},[2485,6299,6300],{"class":2487,"line":2975},[2485,6301,3002],{"class":2506},[2485,6303,6304],{"class":2487,"line":2987},[2485,6305,2614],{"emptyLinePlaceholder":2613},[2485,6307,6308,6310,6313,6315],{"class":2487,"line":2999},[2485,6309,2951],{"class":2878},[2485,6311,6312],{"class":2516}," AfterDelete",[2485,6314,6161],{"class":2878},[2485,6316,2960],{"class":2506},[2485,6318,6319,6322,6324,6326,6328,6330,6332,6334],{"class":2487,"line":3005},[2485,6320,6321],{"class":2510},"    AfterDelete",[2485,6323,2531],{"class":2506},[2485,6325,2645],{"class":2931},[2485,6327,3032],{"class":2516},[2485,6329,2507],{"class":2506},[2485,6331,3621],{"class":2516},[2485,6333,2743],{"class":2506},[2485,6335,6183],{"class":2516},[2485,6337,6338],{"class":2487,"line":3010},[2485,6339,3002],{"class":2506},[3826,6341,679],{"id":6342},"example-validation-and-normalization",[2476,6344,6346],{"className":2478,"code":6345,"language":2480,"meta":29,"style":29},"type User struct {\n    ID    string `db:\"id\" constraints:\"primarykey\"`\n    Name  string `db:\"name\"`\n    Email string `db:\"email\"`\n}\n\n// Validate before any write\nfunc (u *User) BeforeSave(ctx context.Context) error {\n    if u.Email == \"\" {\n        return errors.New(\"email is required\")\n    }\n    return nil\n}\n\n// Normalize after any read\nfunc (u *User) AfterLoad(ctx context.Context) error {\n    u.Email = strings.ToLower(u.Email)\n    return nil\n}\n",[2482,6347,6348,6359,6369,6380,6391,6395,6399,6404,6437,6457,6475,6479,6486,6490,6494,6499,6531,6561,6567],{"__ignoreMap":29},[2485,6349,6350,6352,6355,6357],{"class":2487,"line":9},[2485,6351,2951],{"class":2878},[2485,6353,6354],{"class":2516}," User",[2485,6356,2957],{"class":2878},[2485,6358,2960],{"class":2506},[2485,6360,6361,6364,6366],{"class":2487,"line":19},[2485,6362,6363],{"class":2685},"    ID",[2485,6365,2969],{"class":2516},[2485,6367,6368],{"class":2604}," `db:\"id\" constraints:\"primarykey\"`\n",[2485,6370,6371,6374,6377],{"class":2487,"line":35},[2485,6372,6373],{"class":2685},"    Name",[2485,6375,6376],{"class":2516},"  string",[2485,6378,6379],{"class":2604}," `db:\"name\"`\n",[2485,6381,6382,6385,6388],{"class":2487,"line":1518},[2485,6383,6384],{"class":2685},"    Email",[2485,6386,6387],{"class":2516}," string",[2485,6389,6390],{"class":2604}," `db:\"email\"`\n",[2485,6392,6393],{"class":2487,"line":2610},[2485,6394,3002],{"class":2506},[2485,6396,6397],{"class":2487,"line":2617},[2485,6398,2614],{"emptyLinePlaceholder":2613},[2485,6400,6401],{"class":2487,"line":2623},[2485,6402,6403],{"class":2490},"// Validate before any write\n",[2485,6405,6406,6408,6410,6413,6415,6417,6419,6421,6423,6425,6427,6429,6431,6433,6435],{"class":2487,"line":2656},[2485,6407,3013],{"class":2878},[2485,6409,3596],{"class":2506},[2485,6411,6412],{"class":2931},"u ",[2485,6414,3602],{"class":2676},[2485,6416,2790],{"class":2516},[2485,6418,2743],{"class":2506},[2485,6420,6158],{"class":2510},[2485,6422,2531],{"class":2506},[2485,6424,2645],{"class":2931},[2485,6426,3032],{"class":2516},[2485,6428,2507],{"class":2506},[2485,6430,3621],{"class":2516},[2485,6432,2743],{"class":2506},[2485,6434,3634],{"class":2516},[2485,6436,2960],{"class":2506},[2485,6438,6439,6442,6445,6447,6450,6453,6455],{"class":2487,"line":2922},[2485,6440,6441],{"class":2676},"    if",[2485,6443,6444],{"class":2496}," u",[2485,6446,2507],{"class":2506},[2485,6448,6449],{"class":2496},"Email",[2485,6451,6452],{"class":2676}," ==",[2485,6454,4535],{"class":2604},[2485,6456,2960],{"class":2506},[2485,6458,6459,6462,6464,6466,6468,6470,6473],{"class":2487,"line":2928},[2485,6460,6461],{"class":2676},"        return",[2485,6463,4145],{"class":2496},[2485,6465,2507],{"class":2506},[2485,6467,2528],{"class":2510},[2485,6469,2531],{"class":2506},[2485,6471,6472],{"class":2604},"\"email is required\"",[2485,6474,2653],{"class":2506},[2485,6476,6477],{"class":2487,"line":2938},[2485,6478,3256],{"class":2506},[2485,6480,6481,6483],{"class":2487,"line":2943},[2485,6482,3641],{"class":2676},[2485,6484,6485],{"class":2878}," nil\n",[2485,6487,6488],{"class":2487,"line":2948},[2485,6489,3002],{"class":2506},[2485,6491,6492],{"class":2487,"line":2963},[2485,6493,2614],{"emptyLinePlaceholder":2613},[2485,6495,6496],{"class":2487,"line":2975},[2485,6497,6498],{"class":2490},"// Normalize after any read\n",[2485,6500,6501,6503,6505,6507,6509,6511,6513,6515,6517,6519,6521,6523,6525,6527,6529],{"class":2487,"line":2987},[2485,6502,3013],{"class":2878},[2485,6504,3596],{"class":2506},[2485,6506,6412],{"class":2931},[2485,6508,3602],{"class":2676},[2485,6510,2790],{"class":2516},[2485,6512,2743],{"class":2506},[2485,6514,6236],{"class":2510},[2485,6516,2531],{"class":2506},[2485,6518,2645],{"class":2931},[2485,6520,3032],{"class":2516},[2485,6522,2507],{"class":2506},[2485,6524,3621],{"class":2516},[2485,6526,2743],{"class":2506},[2485,6528,3634],{"class":2516},[2485,6530,2960],{"class":2506},[2485,6532,6533,6536,6538,6540,6542,6545,6547,6550,6552,6555,6557,6559],{"class":2487,"line":2999},[2485,6534,6535],{"class":2496},"    u",[2485,6537,2507],{"class":2506},[2485,6539,6449],{"class":2496},[2485,6541,3265],{"class":2496},[2485,6543,6544],{"class":2496}," strings",[2485,6546,2507],{"class":2506},[2485,6548,6549],{"class":2510},"ToLower",[2485,6551,2531],{"class":2506},[2485,6553,6554],{"class":2496},"u",[2485,6556,2507],{"class":2506},[2485,6558,6449],{"class":2496},[2485,6560,2653],{"class":2506},[2485,6562,6563,6565],{"class":2487,"line":3005},[2485,6564,3641],{"class":2676},[2485,6566,6485],{"class":2878},[2485,6568,6569],{"class":2487,"line":3010},[2485,6570,3002],{"class":2506},[2397,6572,6573],{},"These hooks fire automatically on all typed operations:",[2476,6575,6577],{"className":2478,"code":6576,"language":2480,"meta":29,"style":29},"// BeforeSave fires before the write — returns error if email is empty\nerr := store.Set(ctx, \"user:1\", &User{Name: \"Alice\"}, 0)\n// err: \"email is required\"\n\n// AfterLoad fires after decode — email is normalized\nuser, err := store.Get(ctx, \"user:1\")\n// user.Email is lowercase\n",[2482,6578,6579,6584,6625,6630,6634,6639,6665],{"__ignoreMap":29},[2485,6580,6581],{"class":2487,"line":9},[2485,6582,6583],{"class":2490},"// BeforeSave fires before the write — returns error if email is empty\n",[2485,6585,6586,6588,6590,6592,6594,6596,6598,6600,6602,6605,6607,6609,6611,6613,6615,6617,6619,6621,6623],{"class":2487,"line":19},[2485,6587,4155],{"class":2496},[2485,6589,2500],{"class":2496},[2485,6591,4123],{"class":2496},[2485,6593,2507],{"class":2506},[2485,6595,576],{"class":2510},[2485,6597,2531],{"class":2506},[2485,6599,2645],{"class":2496},[2485,6601,2601],{"class":2506},[2485,6603,6604],{"class":2604}," \"user:1\"",[2485,6606,2601],{"class":2506},[2485,6608,2677],{"class":2676},[2485,6610,2790],{"class":2516},[2485,6612,2682],{"class":2506},[2485,6614,4677],{"class":2685},[2485,6616,2689],{"class":2506},[2485,6618,4740],{"class":2604},[2485,6620,2695],{"class":2506},[2485,6622,4286],{"class":3231},[2485,6624,2653],{"class":2506},[2485,6626,6627],{"class":2487,"line":35},[2485,6628,6629],{"class":2490},"// err: \"email is required\"\n",[2485,6631,6632],{"class":2487,"line":1518},[2485,6633,2614],{"emptyLinePlaceholder":2613},[2485,6635,6636],{"class":2487,"line":2610},[2485,6637,6638],{"class":2490},"// AfterLoad fires after decode — email is normalized\n",[2485,6640,6641,6643,6645,6647,6649,6651,6653,6655,6657,6659,6661,6663],{"class":2487,"line":2617},[2485,6642,5399],{"class":2496},[2485,6644,2601],{"class":2506},[2485,6646,4118],{"class":2496},[2485,6648,2500],{"class":2496},[2485,6650,4123],{"class":2496},[2485,6652,2507],{"class":2506},[2485,6654,571],{"class":2510},[2485,6656,2531],{"class":2506},[2485,6658,2645],{"class":2496},[2485,6660,2601],{"class":2506},[2485,6662,6604],{"class":2604},[2485,6664,2653],{"class":2506},[2485,6666,6667],{"class":2487,"line":2623},[2485,6668,6669],{"class":2490},"// user.Email is lowercase\n",[3826,6671,684],{"id":6672},"hook-firing-points",[3374,6674,6675,6690],{},[3377,6676,6677],{},[3380,6678,6679,6682,6685,6688],{},[3383,6680,6681],{},"Storage Type",[3383,6683,6684],{},"Save Hooks",[3383,6686,6687],{},"Load Hooks",[3383,6689,249],{},[3393,6691,6692,6707,6720,6736],{},[3380,6693,6694,6699,6702,6705],{},[3398,6695,3574,6696],{},[2485,6697,6698],{},"T",[3398,6700,6701],{},"Set, SetBatch",[3398,6703,6704],{},"Get, GetBatch",[3398,6706,581],{},[3380,6708,6709,6714,6716,6718],{},[3398,6710,6711,6712],{},"Bucket",[2485,6713,6698],{},[3398,6715,614],{},[3398,6717,571],{},[3398,6719,581],{},[3380,6721,6722,6727,6730,6733],{},[3398,6723,6724,6725],{},"Database",[2485,6726,6698],{},[3398,6728,6729],{},"Set, SetTx, Insert, InsertFull (all builder paths)",[3398,6731,6732],{},"Get, GetTx, Query, Select, Modify, ExecQuery, ExecSelect, ExecUpdate (and Tx variants)",[3398,6734,6735],{},"Delete, DeleteTx",[3380,6737,6738,6743,6746,6749],{},[3398,6739,6740,6741],{},"Index",[2485,6742,6698],{},[3398,6744,6745],{},"Upsert, UpsertBatch",[3398,6747,6748],{},"Get, Search, Query, Filter",[3398,6750,6751],{},"Delete, DeleteBatch",[3826,6753,689],{"id":6754},"batch-behavior",[2397,6756,6757],{},"For batch operations, hooks fire per-item:",[3493,6759,6760,6771,6779],{},[3496,6761,6762,4182,6765,6767,6768,6770],{},[3499,6763,6764],{},"SetBatch:",[2482,6766,1869],{}," runs on each item before encoding. If any fails, the entire batch is aborted. ",[2482,6769,1874],{}," runs on each item after the batch succeeds.",[3496,6772,6773,4182,6776,6778],{},[3499,6774,6775],{},"GetBatch:",[2482,6777,1879],{}," runs on each decoded item.",[3496,6780,6781,4182,6784,6786],{},[3499,6782,6783],{},"UpsertBatch:",[2482,6785,1869],{}," runs on each metadata item before encoding.",[3826,6788,249],{"id":6789},"delete-hooks",[2397,6791,6792,6793,6795],{},"Delete hooks are invoked on a zero-value ",[2482,6794,6698],{}," because delete operations only receive a key/ID. They act as static guards or side-effects:",[2476,6797,6799],{"className":2478,"code":6798,"language":2480,"meta":29,"style":29},"func (u *User) BeforeDelete(ctx context.Context) error {\n    // No instance state available — use for static guards\n    return nil\n}\n",[2482,6800,6801,6833,6838,6844],{"__ignoreMap":29},[2485,6802,6803,6805,6807,6809,6811,6813,6815,6817,6819,6821,6823,6825,6827,6829,6831],{"class":2487,"line":9},[2485,6804,3013],{"class":2878},[2485,6806,3596],{"class":2506},[2485,6808,6412],{"class":2931},[2485,6810,3602],{"class":2676},[2485,6812,2790],{"class":2516},[2485,6814,2743],{"class":2506},[2485,6816,6274],{"class":2510},[2485,6818,2531],{"class":2506},[2485,6820,2645],{"class":2931},[2485,6822,3032],{"class":2516},[2485,6824,2507],{"class":2506},[2485,6826,3621],{"class":2516},[2485,6828,2743],{"class":2506},[2485,6830,3634],{"class":2516},[2485,6832,2960],{"class":2506},[2485,6834,6835],{"class":2487,"line":19},[2485,6836,6837],{"class":2490},"    // No instance state available — use for static guards\n",[2485,6839,6840,6842],{"class":2487,"line":35},[2485,6841,3641],{"class":2676},[2485,6843,6485],{"class":2878},[2485,6845,6846],{"class":2487,"line":1518},[2485,6847,3002],{"class":2506},[3826,6849,698],{"id":6850},"what-hooks-dont-cover",[3493,6852,6853,6858,6870,6876],{},[3496,6854,6855,6857],{},[3499,6856,3519],{}," do not trigger hooks (they operate below the type-aware layer)",[3496,6859,6860,6862,6863,6866,6867,2743],{},[3499,6861,1680],{}," does not trigger hooks (returns ",[2482,6864,6865],{},"float64",", not ",[2482,6868,6869],{},"*T",[3496,6871,6872,6875],{},[3499,6873,6874],{},"List/Exists"," operations do not trigger hooks (no T instance involved)",[3496,6877,6878,6880,6881,6866,6884,2743],{},[3499,6879,1651],{}," (delete builder) does not trigger hooks (returns ",[2482,6882,6883],{},"int64",[2482,6885,6869],{},[2471,6887,703],{"id":6888},"common-patterns",[3826,6890,707],{"id":6891},"check-then-act",[2476,6893,6895],{"className":2478,"code":6894,"language":2480,"meta":29,"style":29},"exists, _ := store.Exists(ctx, key)\nif !exists {\n    // Create default\n    store.Set(ctx, key, defaultValue, 0)\n}\n",[2482,6896,6897,6923,6934,6939,6966],{"__ignoreMap":29},[2485,6898,6899,6901,6903,6905,6907,6909,6911,6913,6915,6917,6919,6921],{"class":2487,"line":9},[2485,6900,4391],{"class":2496},[2485,6902,2601],{"class":2506},[2485,6904,2631],{"class":2496},[2485,6906,2500],{"class":2496},[2485,6908,4123],{"class":2496},[2485,6910,2507],{"class":2506},[2485,6912,586],{"class":2510},[2485,6914,2531],{"class":2506},[2485,6916,2645],{"class":2496},[2485,6918,2601],{"class":2506},[2485,6920,4640],{"class":2496},[2485,6922,2653],{"class":2506},[2485,6924,6925,6927,6930,6932],{"class":2487,"line":19},[2485,6926,4142],{"class":2676},[2485,6928,6929],{"class":2676}," !",[2485,6931,4391],{"class":2496},[2485,6933,2960],{"class":2506},[2485,6935,6936],{"class":2487,"line":35},[2485,6937,6938],{"class":2490},"    // Create default\n",[2485,6940,6941,6943,6945,6947,6949,6951,6953,6955,6957,6960,6962,6964],{"class":2487,"line":1518},[2485,6942,3565],{"class":2496},[2485,6944,2507],{"class":2506},[2485,6946,576],{"class":2510},[2485,6948,2531],{"class":2506},[2485,6950,2645],{"class":2496},[2485,6952,2601],{"class":2506},[2485,6954,4640],{"class":2496},[2485,6956,2601],{"class":2506},[2485,6958,6959],{"class":2496}," defaultValue",[2485,6961,2601],{"class":2506},[2485,6963,4286],{"class":3231},[2485,6965,2653],{"class":2506},[2485,6967,6968],{"class":2487,"line":2610},[2485,6969,3002],{"class":2506},[2397,6971,6972,6975],{},[3499,6973,6974],{},"Warning:"," Not atomic. For atomic operations, use provider-specific features.",[3826,6977,712],{"id":6978},"get-or-create",[2476,6980,6982],{"className":2478,"code":6981,"language":2480,"meta":29,"style":29},"func GetOrCreate[T any](ctx context.Context, store *grub.Store[T], key string, create func() *T) (*T, error) {\n    val, err := store.Get(ctx, key)\n    if err == nil {\n        return val, nil\n    }\n    if !errors.Is(err, grub.ErrNotFound) {\n        return nil, err\n    }\n\n    val = create()\n    if err := store.Set(ctx, key, val, 0); err != nil {\n        return nil, err\n    }\n    return val, nil\n}\n",[2482,6983,6984,7061,7088,7100,7111,7115,7144,7155,7159,7163,7173,7215,7225,7229,7239],{"__ignoreMap":29},[2485,6985,6986,6988,6991,6993,6995,6998,7000,7002,7004,7006,7008,7010,7012,7014,7016,7018,7020,7022,7024,7027,7029,7031,7033,7036,7039,7041,7043,7045,7047,7049,7051,7053,7055,7057,7059],{"class":2487,"line":9},[2485,6987,3013],{"class":2878},[2485,6989,6990],{"class":2510}," GetOrCreate",[2485,6992,2513],{"class":2506},[2485,6994,6698],{"class":2931},[2485,6996,6997],{"class":2516}," any",[2485,6999,2520],{"class":2506},[2485,7001,2645],{"class":2931},[2485,7003,3032],{"class":2516},[2485,7005,2507],{"class":2506},[2485,7007,3621],{"class":2516},[2485,7009,2601],{"class":2506},[2485,7011,4123],{"class":2931},[2485,7013,3235],{"class":2676},[2485,7015,2395],{"class":2516},[2485,7017,2507],{"class":2506},[2485,7019,3574],{"class":2516},[2485,7021,2513],{"class":2506},[2485,7023,6698],{"class":2516},[2485,7025,7026],{"class":2506},"],",[2485,7028,4640],{"class":2931},[2485,7030,6387],{"class":2516},[2485,7032,2601],{"class":2506},[2485,7034,7035],{"class":2931}," create",[2485,7037,7038],{"class":2878}," func",[2485,7040,3019],{"class":2506},[2485,7042,3235],{"class":2676},[2485,7044,6698],{"class":2516},[2485,7046,2743],{"class":2506},[2485,7048,3596],{"class":2506},[2485,7050,3602],{"class":2676},[2485,7052,6698],{"class":2516},[2485,7054,2601],{"class":2506},[2485,7056,3634],{"class":2516},[2485,7058,2743],{"class":2506},[2485,7060,2960],{"class":2506},[2485,7062,7063,7066,7068,7070,7072,7074,7076,7078,7080,7082,7084,7086],{"class":2487,"line":19},[2485,7064,7065],{"class":2496},"    val",[2485,7067,2601],{"class":2506},[2485,7069,4118],{"class":2496},[2485,7071,2500],{"class":2496},[2485,7073,4123],{"class":2496},[2485,7075,2507],{"class":2506},[2485,7077,571],{"class":2510},[2485,7079,2531],{"class":2506},[2485,7081,2645],{"class":2496},[2485,7083,2601],{"class":2506},[2485,7085,4640],{"class":2496},[2485,7087,2653],{"class":2506},[2485,7089,7090,7092,7094,7096,7098],{"class":2487,"line":35},[2485,7091,6441],{"class":2676},[2485,7093,4118],{"class":2496},[2485,7095,6452],{"class":2676},[2485,7097,6091],{"class":2878},[2485,7099,2960],{"class":2506},[2485,7101,7102,7104,7107,7109],{"class":2487,"line":1518},[2485,7103,6461],{"class":2676},[2485,7105,7106],{"class":2496}," val",[2485,7108,2601],{"class":2506},[2485,7110,6485],{"class":2878},[2485,7112,7113],{"class":2487,"line":2610},[2485,7114,3256],{"class":2506},[2485,7116,7117,7119,7121,7124,7126,7128,7130,7132,7134,7136,7138,7140,7142],{"class":2487,"line":2617},[2485,7118,6441],{"class":2676},[2485,7120,6929],{"class":2676},[2485,7122,7123],{"class":2496},"errors",[2485,7125,2507],{"class":2506},[2485,7127,4150],{"class":2510},[2485,7129,2531],{"class":2506},[2485,7131,4155],{"class":2496},[2485,7133,2601],{"class":2506},[2485,7135,2503],{"class":2496},[2485,7137,2507],{"class":2506},[2485,7139,3465],{"class":2496},[2485,7141,2743],{"class":2506},[2485,7143,2960],{"class":2506},[2485,7145,7146,7148,7150,7152],{"class":2487,"line":2623},[2485,7147,6461],{"class":2676},[2485,7149,6091],{"class":2878},[2485,7151,2601],{"class":2506},[2485,7153,7154],{"class":2496}," err\n",[2485,7156,7157],{"class":2487,"line":2656},[2485,7158,3256],{"class":2506},[2485,7160,7161],{"class":2487,"line":2922},[2485,7162,2614],{"emptyLinePlaceholder":2613},[2485,7164,7165,7167,7169,7171],{"class":2487,"line":2928},[2485,7166,7065],{"class":2496},[2485,7168,3265],{"class":2496},[2485,7170,7035],{"class":2510},[2485,7172,3040],{"class":2506},[2485,7174,7175,7177,7179,7181,7183,7185,7187,7189,7191,7193,7195,7197,7199,7201,7203,7206,7208,7211,7213],{"class":2487,"line":2938},[2485,7176,6441],{"class":2676},[2485,7178,4118],{"class":2496},[2485,7180,2500],{"class":2496},[2485,7182,4123],{"class":2496},[2485,7184,2507],{"class":2506},[2485,7186,576],{"class":2510},[2485,7188,2531],{"class":2506},[2485,7190,2645],{"class":2496},[2485,7192,2601],{"class":2506},[2485,7194,4640],{"class":2496},[2485,7196,2601],{"class":2506},[2485,7198,7106],{"class":2496},[2485,7200,2601],{"class":2506},[2485,7202,4286],{"class":3231},[2485,7204,7205],{"class":2506},");",[2485,7207,4118],{"class":2496},[2485,7209,7210],{"class":2676}," !=",[2485,7212,6091],{"class":2878},[2485,7214,2960],{"class":2506},[2485,7216,7217,7219,7221,7223],{"class":2487,"line":2943},[2485,7218,6461],{"class":2676},[2485,7220,6091],{"class":2878},[2485,7222,2601],{"class":2506},[2485,7224,7154],{"class":2496},[2485,7226,7227],{"class":2487,"line":2948},[2485,7228,3256],{"class":2506},[2485,7230,7231,7233,7235,7237],{"class":2487,"line":2963},[2485,7232,3641],{"class":2676},[2485,7234,7106],{"class":2496},[2485,7236,2601],{"class":2506},[2485,7238,6485],{"class":2878},[2485,7240,7241],{"class":2487,"line":2975},[2485,7242,3002],{"class":2506},[3826,7244,717],{"id":7245},"batch-processing",[2476,7247,7249],{"className":2478,"code":7248,"language":2480,"meta":29,"style":29},"// Process in batches to avoid memory issues\nconst batchSize = 100\n\nkeys, _ := store.List(ctx, \"user:\", 0)\n\nfor i := 0; i \u003C len(keys); i += batchSize {\n    end := min(i+batchSize, len(keys))\n    batch := keys[i:end]\n\n    results, _ := store.GetBatch(ctx, batch)\n    for key, user := range results {\n        // Process user\n    }\n}\n",[2482,7250,7251,7256,7270,7274,7305,7309,7347,7372,7393,7397,7425,7444,7449,7453],{"__ignoreMap":29},[2485,7252,7253],{"class":2487,"line":9},[2485,7254,7255],{"class":2490},"// Process in batches to avoid memory issues\n",[2485,7257,7258,7261,7265,7267],{"class":2487,"line":19},[2485,7259,7260],{"class":2878},"const",[2485,7262,7264],{"class":7263},"sfm-E"," batchSize",[2485,7266,3265],{"class":2496},[2485,7268,7269],{"class":3231}," 100\n",[2485,7271,7272],{"class":2487,"line":35},[2485,7273,2614],{"emptyLinePlaceholder":2613},[2485,7275,7276,7278,7280,7282,7284,7286,7288,7290,7292,7294,7296,7299,7301,7303],{"class":2487,"line":1518},[2485,7277,4474],{"class":2496},[2485,7279,2601],{"class":2506},[2485,7281,2631],{"class":2496},[2485,7283,2500],{"class":2496},[2485,7285,4123],{"class":2496},[2485,7287,2507],{"class":2506},[2485,7289,591],{"class":2510},[2485,7291,2531],{"class":2506},[2485,7293,2645],{"class":2496},[2485,7295,2601],{"class":2506},[2485,7297,7298],{"class":2604}," \"user:\"",[2485,7300,2601],{"class":2506},[2485,7302,4286],{"class":3231},[2485,7304,2653],{"class":2506},[2485,7306,7307],{"class":2487,"line":2610},[2485,7308,2614],{"emptyLinePlaceholder":2613},[2485,7310,7311,7313,7316,7318,7320,7323,7325,7328,7332,7334,7336,7338,7340,7343,7345],{"class":2487,"line":2617},[2485,7312,4637],{"class":2676},[2485,7314,7315],{"class":2496}," i",[2485,7317,2500],{"class":2496},[2485,7319,4286],{"class":3231},[2485,7321,7322],{"class":2506},";",[2485,7324,7315],{"class":2496},[2485,7326,7327],{"class":2676}," \u003C",[2485,7329,7331],{"class":7330},"skxcq"," len",[2485,7333,2531],{"class":2506},[2485,7335,4474],{"class":2496},[2485,7337,7205],{"class":2506},[2485,7339,7315],{"class":2496},[2485,7341,7342],{"class":2496}," +=",[2485,7344,7264],{"class":2496},[2485,7346,2960],{"class":2506},[2485,7348,7349,7352,7354,7357,7359,7362,7364,7366,7368,7370],{"class":2487,"line":2623},[2485,7350,7351],{"class":2496},"    end",[2485,7353,2500],{"class":2496},[2485,7355,7356],{"class":7330}," min",[2485,7358,2531],{"class":2506},[2485,7360,7361],{"class":2496},"i+batchSize",[2485,7363,2601],{"class":2506},[2485,7365,7331],{"class":7330},[2485,7367,2531],{"class":2506},[2485,7369,4474],{"class":2496},[2485,7371,2537],{"class":2506},[2485,7373,7374,7377,7379,7381,7383,7386,7388,7391],{"class":2487,"line":2656},[2485,7375,7376],{"class":2496},"    batch",[2485,7378,2500],{"class":2496},[2485,7380,4626],{"class":2496},[2485,7382,2513],{"class":2506},[2485,7384,7385],{"class":2496},"i",[2485,7387,2689],{"class":2506},[2485,7389,7390],{"class":2496},"end",[2485,7392,3581],{"class":2506},[2485,7394,7395],{"class":2487,"line":2922},[2485,7396,2614],{"emptyLinePlaceholder":2613},[2485,7398,7399,7402,7404,7406,7408,7410,7412,7414,7416,7418,7420,7423],{"class":2487,"line":2928},[2485,7400,7401],{"class":2496},"    results",[2485,7403,2601],{"class":2506},[2485,7405,2631],{"class":2496},[2485,7407,2500],{"class":2496},[2485,7409,4123],{"class":2496},[2485,7411,2507],{"class":2506},[2485,7413,596],{"class":2510},[2485,7415,2531],{"class":2506},[2485,7417,2645],{"class":2496},[2485,7419,2601],{"class":2506},[2485,7421,7422],{"class":2496}," batch",[2485,7424,2653],{"class":2506},[2485,7426,7427,7430,7432,7434,7436,7438,7440,7442],{"class":2487,"line":2938},[2485,7428,7429],{"class":2676},"    for",[2485,7431,4640],{"class":2496},[2485,7433,2601],{"class":2506},[2485,7435,4645],{"class":2496},[2485,7437,2500],{"class":2496},[2485,7439,4650],{"class":2676},[2485,7441,4653],{"class":2496},[2485,7443,2960],{"class":2506},[2485,7445,7446],{"class":2487,"line":2943},[2485,7447,7448],{"class":2490},"        // Process user\n",[2485,7450,7451],{"class":2487,"line":2948},[2485,7452,3256],{"class":2506},[2485,7454,7455],{"class":2487,"line":2963},[2485,7456,3002],{"class":2506},[3826,7458,722],{"id":7459},"conditional-delete",[2476,7461,7463],{"className":2478,"code":7462,"language":2480,"meta":29,"style":29},"// Delete only if value matches condition\nval, err := store.Get(ctx, key)\nif err != nil {\n    return err\n}\nif val.Status == \"expired\" {\n    return store.Delete(ctx, key)\n}\n",[2482,7464,7465,7470,7497,7509,7515,7519,7537,7557],{"__ignoreMap":29},[2485,7466,7467],{"class":2487,"line":9},[2485,7468,7469],{"class":2490},"// Delete only if value matches condition\n",[2485,7471,7472,7475,7477,7479,7481,7483,7485,7487,7489,7491,7493,7495],{"class":2487,"line":19},[2485,7473,7474],{"class":2496},"val",[2485,7476,2601],{"class":2506},[2485,7478,4118],{"class":2496},[2485,7480,2500],{"class":2496},[2485,7482,4123],{"class":2496},[2485,7484,2507],{"class":2506},[2485,7486,571],{"class":2510},[2485,7488,2531],{"class":2506},[2485,7490,2645],{"class":2496},[2485,7492,2601],{"class":2506},[2485,7494,4640],{"class":2496},[2485,7496,2653],{"class":2506},[2485,7498,7499,7501,7503,7505,7507],{"class":2487,"line":35},[2485,7500,4142],{"class":2676},[2485,7502,4118],{"class":2496},[2485,7504,7210],{"class":2676},[2485,7506,6091],{"class":2878},[2485,7508,2960],{"class":2506},[2485,7510,7511,7513],{"class":2487,"line":1518},[2485,7512,3641],{"class":2676},[2485,7514,7154],{"class":2496},[2485,7516,7517],{"class":2487,"line":2610},[2485,7518,3002],{"class":2506},[2485,7520,7521,7523,7525,7527,7530,7532,7535],{"class":2487,"line":2617},[2485,7522,4142],{"class":2676},[2485,7524,7106],{"class":2496},[2485,7526,2507],{"class":2506},[2485,7528,7529],{"class":2496},"Status",[2485,7531,6452],{"class":2676},[2485,7533,7534],{"class":2604}," \"expired\"",[2485,7536,2960],{"class":2506},[2485,7538,7539,7541,7543,7545,7547,7549,7551,7553,7555],{"class":2487,"line":2623},[2485,7540,3641],{"class":2676},[2485,7542,4123],{"class":2496},[2485,7544,2507],{"class":2506},[2485,7546,581],{"class":2510},[2485,7548,2531],{"class":2506},[2485,7550,2645],{"class":2496},[2485,7552,2601],{"class":2506},[2485,7554,4640],{"class":2496},[2485,7556,2653],{"class":2506},[2485,7558,7559],{"class":2487,"line":2656},[2485,7560,3002],{"class":2506},[2471,7562,727],{"id":7563},"error-handling",[3826,7565,731],{"id":7566},"standard-error-checks",[2476,7568,7570],{"className":2478,"code":7569,"language":2480,"meta":29,"style":29},"val, err := store.Get(ctx, key)\nswitch {\ncase err == nil:\n    // Success\ncase errors.Is(err, grub.ErrNotFound):\n    // Key doesn't exist\ncase errors.Is(err, context.DeadlineExceeded):\n    // Timeout\ncase errors.Is(err, context.Canceled):\n    // Canceled\ndefault:\n    // Provider error (network, etc.)\n}\n",[2482,7571,7572,7598,7605,7619,7624,7649,7653,7678,7683,7708,7713,7720,7725],{"__ignoreMap":29},[2485,7573,7574,7576,7578,7580,7582,7584,7586,7588,7590,7592,7594,7596],{"class":2487,"line":9},[2485,7575,7474],{"class":2496},[2485,7577,2601],{"class":2506},[2485,7579,4118],{"class":2496},[2485,7581,2500],{"class":2496},[2485,7583,4123],{"class":2496},[2485,7585,2507],{"class":2506},[2485,7587,571],{"class":2510},[2485,7589,2531],{"class":2506},[2485,7591,2645],{"class":2496},[2485,7593,2601],{"class":2506},[2485,7595,4640],{"class":2496},[2485,7597,2653],{"class":2506},[2485,7599,7600,7603],{"class":2487,"line":19},[2485,7601,7602],{"class":2676},"switch",[2485,7604,2960],{"class":2506},[2485,7606,7607,7610,7612,7614,7616],{"class":2487,"line":35},[2485,7608,7609],{"class":2676},"case",[2485,7611,4118],{"class":2496},[2485,7613,6452],{"class":2676},[2485,7615,6091],{"class":2878},[2485,7617,7618],{"class":2506},":\n",[2485,7620,7621],{"class":2487,"line":1518},[2485,7622,7623],{"class":2490},"    // Success\n",[2485,7625,7626,7628,7630,7632,7634,7636,7638,7640,7642,7644,7646],{"class":2487,"line":2610},[2485,7627,7609],{"class":2676},[2485,7629,4145],{"class":2496},[2485,7631,2507],{"class":2506},[2485,7633,4150],{"class":2510},[2485,7635,2531],{"class":2506},[2485,7637,4155],{"class":2496},[2485,7639,2601],{"class":2506},[2485,7641,2503],{"class":2496},[2485,7643,2507],{"class":2506},[2485,7645,3465],{"class":2496},[2485,7647,7648],{"class":2506},"):\n",[2485,7650,7651],{"class":2487,"line":2617},[2485,7652,4172],{"class":2490},[2485,7654,7655,7657,7659,7661,7663,7665,7667,7669,7671,7673,7676],{"class":2487,"line":2623},[2485,7656,7609],{"class":2676},[2485,7658,4145],{"class":2496},[2485,7660,2507],{"class":2506},[2485,7662,4150],{"class":2510},[2485,7664,2531],{"class":2506},[2485,7666,4155],{"class":2496},[2485,7668,2601],{"class":2506},[2485,7670,3032],{"class":2496},[2485,7672,2507],{"class":2506},[2485,7674,7675],{"class":2496},"DeadlineExceeded",[2485,7677,7648],{"class":2506},[2485,7679,7680],{"class":2487,"line":2656},[2485,7681,7682],{"class":2490},"    // Timeout\n",[2485,7684,7685,7687,7689,7691,7693,7695,7697,7699,7701,7703,7706],{"class":2487,"line":2922},[2485,7686,7609],{"class":2676},[2485,7688,4145],{"class":2496},[2485,7690,2507],{"class":2506},[2485,7692,4150],{"class":2510},[2485,7694,2531],{"class":2506},[2485,7696,4155],{"class":2496},[2485,7698,2601],{"class":2506},[2485,7700,3032],{"class":2496},[2485,7702,2507],{"class":2506},[2485,7704,7705],{"class":2496},"Canceled",[2485,7707,7648],{"class":2506},[2485,7709,7710],{"class":2487,"line":2928},[2485,7711,7712],{"class":2490},"    // Canceled\n",[2485,7714,7715,7718],{"class":2487,"line":2938},[2485,7716,7717],{"class":2676},"default",[2485,7719,7618],{"class":2506},[2485,7721,7722],{"class":2487,"line":2943},[2485,7723,7724],{"class":2490},"    // Provider error (network, etc.)\n",[2485,7726,7727],{"class":2487,"line":2948},[2485,7728,3002],{"class":2506},[3826,7730,736],{"id":7731},"wrapping-errors",[2476,7733,7735],{"className":2478,"code":7734,"language":2480,"meta":29,"style":29},"val, err := store.Get(ctx, key)\nif err != nil {\n    return fmt.Errorf(\"loading config %s: %w\", key, err)\n}\n",[2482,7736,7737,7763,7775,7812],{"__ignoreMap":29},[2485,7738,7739,7741,7743,7745,7747,7749,7751,7753,7755,7757,7759,7761],{"class":2487,"line":9},[2485,7740,7474],{"class":2496},[2485,7742,2601],{"class":2506},[2485,7744,4118],{"class":2496},[2485,7746,2500],{"class":2496},[2485,7748,4123],{"class":2496},[2485,7750,2507],{"class":2506},[2485,7752,571],{"class":2510},[2485,7754,2531],{"class":2506},[2485,7756,2645],{"class":2496},[2485,7758,2601],{"class":2506},[2485,7760,4640],{"class":2496},[2485,7762,2653],{"class":2506},[2485,7764,7765,7767,7769,7771,7773],{"class":2487,"line":19},[2485,7766,4142],{"class":2676},[2485,7768,4118],{"class":2496},[2485,7770,7210],{"class":2676},[2485,7772,6091],{"class":2878},[2485,7774,2960],{"class":2506},[2485,7776,7777,7779,7782,7784,7787,7789,7792,7794,7797,7800,7802,7804,7806,7808,7810],{"class":2487,"line":35},[2485,7778,3641],{"class":2676},[2485,7780,7781],{"class":2496}," fmt",[2485,7783,2507],{"class":2506},[2485,7785,7786],{"class":2510},"Errorf",[2485,7788,2531],{"class":2506},[2485,7790,7791],{"class":2604},"\"loading config ",[2485,7793,5335],{"class":5334},[2485,7795,7796],{"class":2604},": ",[2485,7798,7799],{"class":5334},"%w",[2485,7801,5331],{"class":2604},[2485,7803,2601],{"class":2506},[2485,7805,4640],{"class":2496},[2485,7807,2601],{"class":2506},[2485,7809,4118],{"class":2496},[2485,7811,2653],{"class":2506},[2485,7813,7814],{"class":2487,"line":1518},[2485,7815,3002],{"class":2506},[2397,7817,7818,7819,7822],{},"The wrapped error preserves ",[2482,7820,7821],{},"errors.Is"," behavior:",[2476,7824,7826],{"className":2478,"code":7825,"language":2480,"meta":29,"style":29},"if errors.Is(err, grub.ErrNotFound) {\n    // Still works\n}\n",[2482,7827,7828,7854,7859],{"__ignoreMap":29},[2485,7829,7830,7832,7834,7836,7838,7840,7842,7844,7846,7848,7850,7852],{"class":2487,"line":9},[2485,7831,4142],{"class":2676},[2485,7833,4145],{"class":2496},[2485,7835,2507],{"class":2506},[2485,7837,4150],{"class":2510},[2485,7839,2531],{"class":2506},[2485,7841,4155],{"class":2496},[2485,7843,2601],{"class":2506},[2485,7845,2503],{"class":2496},[2485,7847,2507],{"class":2506},[2485,7849,3465],{"class":2496},[2485,7851,2743],{"class":2506},[2485,7853,2960],{"class":2506},[2485,7855,7856],{"class":2487,"line":19},[2485,7857,7858],{"class":2490},"    // Still works\n",[2485,7860,7861],{"class":2487,"line":35},[2485,7862,3002],{"class":2506},[3945,7864,7865],{},"html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html pre.shiki code .suWN2, html code.shiki .suWN2{--shiki-default:var(--shiki-tag)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sfm-E, html code.shiki .sfm-E{--shiki-default:var(--shiki-variable)}html pre.shiki code .skxcq, html code.shiki .skxcq{--shiki-default:var(--shiki-builtin)}",{"title":29,"searchDepth":19,"depth":19,"links":7867},[7868,7877,7884,7894,7902,7908],{"id":4098,"depth":19,"text":567,"children":7869},[7870,7871,7872,7873,7874,7875,7876],{"id":4101,"depth":35,"text":571},{"id":4195,"depth":35,"text":576},{"id":4317,"depth":35,"text":581},{"id":4391,"depth":35,"text":586},{"id":4456,"depth":35,"text":591},{"id":4563,"depth":35,"text":596},{"id":4692,"depth":35,"text":601},{"id":4815,"depth":19,"text":606,"children":7878},[7879,7880,7881,7882,7883],{"id":4818,"depth":35,"text":571},{"id":4999,"depth":35,"text":614},{"id":5148,"depth":35,"text":581},{"id":5216,"depth":35,"text":586},{"id":5253,"depth":35,"text":591},{"id":5383,"depth":19,"text":631,"children":7885},[7886,7887,7888,7889,7890,7891,7892,7893],{"id":5386,"depth":35,"text":571},{"id":5460,"depth":35,"text":576},{"id":5584,"depth":35,"text":581},{"id":5652,"depth":35,"text":586},{"id":5689,"depth":35,"text":651},{"id":5860,"depth":35,"text":656},{"id":5940,"depth":35,"text":661},{"id":6045,"depth":35,"text":666},{"id":6141,"depth":19,"text":234,"children":7895},[7896,7897,7898,7899,7900,7901],{"id":6146,"depth":35,"text":239},{"id":6342,"depth":35,"text":679},{"id":6672,"depth":35,"text":684},{"id":6754,"depth":35,"text":689},{"id":6789,"depth":35,"text":249},{"id":6850,"depth":35,"text":698},{"id":6888,"depth":19,"text":703,"children":7903},[7904,7905,7906,7907],{"id":6891,"depth":35,"text":707},{"id":6978,"depth":35,"text":712},{"id":7245,"depth":35,"text":717},{"id":7459,"depth":35,"text":722},{"id":7563,"depth":19,"text":727,"children":7909},[7910,7911],{"id":7566,"depth":35,"text":731},{"id":7731,"depth":35,"text":736},{},"2025-01-07T00:00:00.000Z",null,{"title":558,"description":560},[199,7917,7918,3420],"Hooks","CRUD","2026-01-28T00:00:00.000Z","lDXHQt__o6Hw-ffWaoaxKmjJpBED_wNIXvjt2a3REyc",[7922,7923],{"title":401,"path":400,"stem":2328,"description":403,"children":-1},{"title":741,"path":740,"stem":2332,"description":743,"children":-1},1776265146770]