[{"data":1,"prerenderedAt":5857},["ShallowReactive",2],{"search-sections-soy":3,"nav-soy":1741,"content-tree-soy":1794,"footer-resources":1818,"content-/v1.0.8/guides/lifecycle":4819,"surround-/v1.0.8/guides/lifecycle":5854},[4,10,14,20,25,30,35,41,46,51,56,61,66,69,74,79,84,89,94,99,104,109,113,118,122,127,132,137,141,146,151,156,161,166,170,175,180,185,190,195,200,205,210,215,220,225,229,233,238,242,247,252,257,261,266,271,276,280,284,289,293,298,302,306,311,316,320,324,328,333,338,343,348,353,358,363,367,371,376,380,384,388,392,397,402,406,411,416,421,425,429,434,439,443,447,452,457,462,467,472,476,481,486,491,496,501,505,510,514,518,523,527,531,535,540,544,549,554,559,564,569,573,577,582,587,592,597,602,606,611,616,620,625,629,633,637,641,645,650,654,659,664,668,672,676,681,685,689,693,697,701,706,711,716,721,726,731,736,741,746,750,755,760,765,770,775,780,785,790,795,800,805,809,813,817,821,825,830,835,840,845,849,853,858,863,867,872,876,880,885,890,895,900,905,910,915,920,925,930,935,939,944,949,954,959,964,969,974,979,984,989,994,998,1003,1007,1012,1017,1021,1027,1032,1037,1042,1047,1052,1057,1062,1067,1072,1075,1079,1083,1087,1092,1097,1102,1107,1112,1117,1122,1127,1132,1137,1142,1147,1150,1155,1160,1165,1170,1175,1180,1185,1190,1195,1200,1205,1210,1215,1220,1225,1230,1235,1240,1245,1250,1255,1260,1265,1270,1275,1280,1285,1290,1294,1299,1304,1309,1314,1319,1324,1329,1334,1339,1344,1348,1352,1356,1360,1365,1369,1374,1379,1383,1388,1391,1396,1401,1406,1411,1416,1420,1424,1428,1432,1437,1442,1445,1449,1454,1459,1463,1467,1471,1476,1479,1482,1486,1490,1494,1499,1502,1506,1510,1514,1517,1522,1527,1532,1536,1540,1544,1549,1554,1559,1563,1567,1571,1576,1581,1586,1590,1594,1599,1604,1608,1613,1618,1623,1628,1633,1638,1643,1648,1653,1658,1663,1668,1673,1678,1683,1688,1693,1696,1701,1706,1711,1716,1721,1726,1731,1736],{"id":5,"title":6,"titles":7,"content":8,"level":9},"/v1.0.8/overview","Overview",[],"Type-safe SQL query builder for Go with multi-database support",1,{"id":11,"title":6,"titles":12,"content":13,"level":9},"/v1.0.8/overview#overview",[],"Query builders in Go often mean string concatenation or runtime reflection on every query. Soy offers a different approach: type-safe query building with reflection performed once at initialization, not on hot paths. import \"github.com/zoobz-io/astql/pkg/postgres\"\n\n// Define your model\ntype User struct {\n    ID    int    `db:\"id\" type:\"serial\" constraints:\"primary key\"`\n    Email string `db:\"email\" type:\"text\" constraints:\"not null unique\"`\n    Name  string `db:\"name\" type:\"text\"`\n    Age   int    `db:\"age\" type:\"int\"`\n}\n\n// Create a soy instance with a database provider\nusers, err := soy.New[User](db, \"users\", postgres.New())\nif err != nil {\n    log.Fatal(err)\n}\n\n// Query with type safety\nuser, err := users.Select().\n    Where(\"email\", \"=\", \"user_email\").\n    Exec(ctx, map[string]any{\"user_email\": \"alice@example.com\"}) Named parameters, compile-time type guarantees, zero reflection on query execution.",{"id":15,"title":16,"titles":17,"content":18,"level":19},"/v1.0.8/overview#architecture","Architecture",[6],"┌─────────────────────────────────────────────────────────────┐\n│                        Soy[T]                            │\n│                                                             │\n│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │\n│  │  Sentinel   │  │   ASTQL     │  │     sqlx.DB         │  │\n│  │  (types)    │  │  (schema)   │  │   (execution)       │  │\n│  └─────────────┘  └─────────────┘  └─────────────────────┘  │\n│         │               │                    │              │\n│         └───────────────┴────────────────────┘              │\n│                         │                                   │\n│  ┌──────────────────────┴───────────────────────────────┐   │\n│  │              Query Builders                          │   │\n│  │  Select │ Query │ Insert │ Update │ Delete │ Agg    │   │\n│  └──────────────────────────────────────────────────────┘   │\n└─────────────────────────────────────────────────────────────┘ Soy wraps three components: Sentinel - Type inspection library, caches struct metadataASTQL - Abstract SQL with schema validationsqlx - Database execution layer Reflection occurs once during New[T](). All subsequent queries use cached metadata.",2,{"id":21,"title":22,"titles":23,"content":24,"level":19},"/v1.0.8/overview#philosophy","Philosophy",[6],"Soy prioritises safety without sacrificing ergonomics: // Type-safe field references\nusers.Select().Where(\"email\", \"=\", \"email_param\")  // Field \"email\" validated against schema\n\n// Named parameters prevent SQL injection\nusers.Query().Where(\"age\", \">=\", \"min_age\").Exec(ctx, map[string]any{\n    \"min_age\": 18,\n})\n\n// Required WHERE for destructive operations\nusers.Modify().Set(\"name\", \"new_name\").Exec(ctx, params)  // Error: WHERE required The schema is derived from struct tags at initialisation. Invalid field names or parameters fail fast.",{"id":26,"title":27,"titles":28,"content":29,"level":19},"/v1.0.8/overview#capabilities","Capabilities",[6],"A unified query builder opens possibilities: Basic Queries - SELECT single records or multiple with pagination, ordering, and complex WHERE conditions. Mutations - INSERT with ON CONFLICT handling, UPDATE with required WHERE, DELETE with batch support. Aggregates - COUNT, SUM, AVG, MIN, MAX with GROUP BY and HAVING clauses. Compound Queries - UNION, INTERSECT, EXCEPT for combining result sets. Spec System - JSON-serializable query specifications for external construction (LLM-generated queries, config files). Soy provides the query layer. How you use it is up to you.",{"id":31,"title":32,"titles":33,"content":34,"level":19},"/v1.0.8/overview#priorities","Priorities",[6],"",{"id":36,"title":37,"titles":38,"content":39,"level":40},"/v1.0.8/overview#type-safety","Type Safety",[6,32],"Fields are validated at compile time via generics. The struct type T flows through the entire chain: users, _ := soy.New[User](db, \"users\")\n\n// Returns *User\nuser, err := users.Select().Where(\"id\", \"=\", \"user_id\").Exec(ctx, params)\n\n// Returns []*User\nresults, err := users.Query().OrderBy(\"name\", \"asc\").Exec(ctx, params) Schema validation catches invalid field names before execution: // Error: field \"invalid_field\" not in schema\nusers.Select().Where(\"invalid_field\", \"=\", \"param\")",3,{"id":42,"title":43,"titles":44,"content":45,"level":40},"/v1.0.8/overview#safety","Safety",[6,32],"Destructive operations require explicit WHERE clauses: // Error: UPDATE requires WHERE clause\nusers.Modify().Set(\"status\", \"inactive\").Exec(ctx, params)\n\n// Correct\nusers.Modify().Set(\"status\", \"inactive\").Where(\"id\", \"=\", \"user_id\").Exec(ctx, params) Named parameters prevent SQL injection. Raw SQL is never constructed from user input.",{"id":47,"title":48,"titles":49,"content":50,"level":40},"/v1.0.8/overview#performance","Performance",[6,32],"Lazy reflection - Struct metadata cached on first useNamed parameters - PostgreSQL prepared statement reuseMinimal allocations - Builder pattern reuses internal statePer-query isolation - Each builder chain is independent",{"id":52,"title":53,"titles":54,"content":55,"level":19},"/v1.0.8/overview#multi-database-support","Multi-Database Support",[6],"Soy supports multiple databases through ASTQL providers: import (\n    \"github.com/zoobz-io/astql/pkg/postgres\"\n    \"github.com/zoobz-io/astql/pkg/mariadb\"\n    \"github.com/zoobz-io/astql/pkg/sqlite\"\n    \"github.com/zoobz-io/astql/pkg/mssql\"\n)\n\n// Use the appropriate provider for your database\nusers, err := soy.New[User](db, \"users\", postgres.New())\nusers, err := soy.New[User](db, \"users\", mariadb.New())\nusers, err := soy.New[User](db, \"users\", sqlite.New())\nusers, err := soy.New[User](db, \"users\", mssql.New())",{"id":57,"title":58,"titles":59,"content":60,"level":19},"/v1.0.8/overview#database-specific-features","Database-Specific Features",[6],"Some features are PostgreSQL-specific: DISTINCT ON - Get first row per distinct column combinationRow locking - FOR UPDATE, FOR SHARE, FOR NO KEY UPDATE, FOR KEY SHAREArray operators - @>, \u003C@, && for containment and overlappgvector - \u003C->, \u003C#>, \u003C=>, \u003C+> for similarity searchNULLS FIRST/LAST - Control NULL ordering in ORDER BYFILTER aggregates - Conditional aggregation with FILTER clause 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 pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}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 .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}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 .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 .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}",{"id":62,"title":63,"titles":64,"content":65,"level":9},"/v1.0.8/learn/quickstart","Quickstart",[],"Get started with soy in minutes",{"id":67,"title":63,"titles":68,"content":34,"level":9},"/v1.0.8/learn/quickstart#quickstart",[],{"id":70,"title":71,"titles":72,"content":73,"level":19},"/v1.0.8/learn/quickstart#requirements","Requirements",[63],"Go 1.24 or later.",{"id":75,"title":76,"titles":77,"content":78,"level":19},"/v1.0.8/learn/quickstart#installation","Installation",[63],"go get github.com/zoobz-io/soy",{"id":80,"title":81,"titles":82,"content":83,"level":19},"/v1.0.8/learn/quickstart#basic-usage","Basic Usage",[63],"package main\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"log\"\n\n    \"github.com/jmoiron/sqlx\"\n    _ \"github.com/lib/pq\"\n    \"github.com/zoobz-io/astql/pkg/postgres\"\n    \"github.com/zoobz-io/soy\"\n)\n\n// Define your model with struct tags\ntype User struct {\n    ID    int    `db:\"id\" type:\"serial\" constraints:\"primary key\"`\n    Email string `db:\"email\" type:\"text\" constraints:\"not null unique\"`\n    Name  string `db:\"name\" type:\"text\"`\n    Age   int    `db:\"age\" type:\"int\"`\n}\n\nfunc main() {\n    // Connect to PostgreSQL\n    db, err := sqlx.Connect(\"postgres\", \"postgres://localhost/mydb?sslmode=disable\")\n    if err != nil {\n        log.Fatal(err)\n    }\n    defer db.Close()\n\n    // Create soy instance with PostgreSQL provider\n    users, err := soy.New[User](db, \"users\", postgres.New())\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    ctx := context.Background()\n\n    // Insert a user\n    newUser := &User{Email: \"alice@example.com\", Name: \"Alice\", Age: 30}\n    created, err := users.Insert().Exec(ctx, newUser)\n    if err != nil {\n        log.Fatal(err)\n    }\n    fmt.Printf(\"Created user: %d\\n\", created.ID)\n\n    // Query by email\n    found, err := users.Select().\n        Where(\"email\", \"=\", \"email_param\").\n        Exec(ctx, map[string]any{\"email_param\": \"alice@example.com\"})\n    if err != nil {\n        log.Fatal(err)\n    }\n    fmt.Printf(\"Found: %s\\n\", found.Name)\n\n    // Update the user\n    updated, err := users.Modify().\n        Set(\"age\", \"new_age\").\n        Where(\"id\", \"=\", \"user_id\").\n        Exec(ctx, map[string]any{\"new_age\": 31, \"user_id\": created.ID})\n    if err != nil {\n        log.Fatal(err)\n    }\n    fmt.Printf(\"Updated age: %d\\n\", updated.Age)\n\n    // Query all users\n    all, err := users.Query().\n        OrderBy(\"name\", \"asc\").\n        Exec(ctx, nil)\n    if err != nil {\n        log.Fatal(err)\n    }\n    fmt.Printf(\"Total users: %d\\n\", len(all))\n}",{"id":85,"title":86,"titles":87,"content":88,"level":19},"/v1.0.8/learn/quickstart#whats-happening","What's Happening",[63],"New[User](db, \"users\", postgres.New()) inspects the struct tags and builds a schema with the PostgreSQL providerInsert().Build().Exec() generates an INSERT with RETURNINGSelect().Where().Exec() generates a SELECT with named parametersModify().Set().Where().Exec() generates an UPDATE (WHERE required)Query().OrderBy().Exec() generates a SELECT returning multiple rows",{"id":90,"title":91,"titles":92,"content":93,"level":19},"/v1.0.8/learn/quickstart#other-databases","Other Databases",[63],"Soy supports multiple databases through ASTQL providers: import (\n    \"github.com/zoobz-io/astql/pkg/postgres\"\n    \"github.com/zoobz-io/astql/pkg/mariadb\"\n    \"github.com/zoobz-io/astql/pkg/sqlite\"\n    \"github.com/zoobz-io/astql/pkg/mssql\"\n)\n\nusers, err := soy.New[User](db, \"users\", mariadb.New()) // MariaDB\nusers, err := soy.New[User](db, \"users\", sqlite.New()) // SQLite\nusers, err := soy.New[User](db, \"users\", mssql.New())  // MSSQL",{"id":95,"title":96,"titles":97,"content":98,"level":19},"/v1.0.8/learn/quickstart#struct-tags","Struct Tags",[63],"Soy uses struct tags to define the schema: TagPurposeExampledbColumn namedb:\"email\"typeSQL column typetype:\"text\", type:\"int\", type:\"serial\"constraintsColumn constraintsconstraints:\"primary key\", constraints:\"not null unique\"defaultDefault valuedefault:\"now()\"checkCheck constraintcheck:\"age >= 0\"indexCreate indexindex:\"true\"referencesForeign keyreferences:\"users(id)\"",{"id":100,"title":101,"titles":102,"content":103,"level":19},"/v1.0.8/learn/quickstart#next-steps","Next Steps",[63],"Core Concepts - Understand queries, conditions, and buildersQueries Guide - SELECT with filtering and orderingMutations Guide - INSERT, UPDATE, DELETE operations 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 .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}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 .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 .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 .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);}",{"id":105,"title":106,"titles":107,"content":108,"level":9},"/v1.0.8/learn/concepts","Core Concepts",[],"Queries, conditions, builders, and specs - the building blocks of soy",{"id":110,"title":106,"titles":111,"content":112,"level":9},"/v1.0.8/learn/concepts#core-concepts",[],"Soy has four primitives: the soy instance, query builders, conditions, and specs. Understanding these unlocks the full API.",{"id":114,"title":115,"titles":116,"content":117,"level":19},"/v1.0.8/learn/concepts#soy-instance","Soy Instance",[106],"A Soy[T] instance is the entry point for all queries. Create one per table: users, err := soy.New[User](db, \"users\")\norders, err := soy.New[Order](db, \"orders\") The type parameter T flows through all operations, ensuring type safety: user, err := users.Select().Exec(ctx, params)   // Returns *User\norder, err := orders.Select().Exec(ctx, params) // Returns *Order Create instances as package-level variables or singletons. Each instance caches schema metadata. Dynamic creation leads to redundant reflection. // Good: fixed set of instances\nvar (\n    Users  *soy.Soy[User]\n    Orders *soy.Soy[Order]\n)\n\nfunc Init(db *sqlx.DB) error {\n    var err error\n    Users, err = soy.New[User](db, \"users\")\n    if err != nil {\n        return err\n    }\n    Orders, err = soy.New[Order](db, \"orders\")\n    return err\n}\n\n// Bad: dynamic instance per request\nfunc HandleRequest(userID string) {\n    users, _ := soy.New[User](db, \"users\") // Redundant reflection\n    // ...\n}",{"id":119,"title":120,"titles":121,"content":34,"level":19},"/v1.0.8/learn/concepts#queries","Queries",[106],{"id":123,"title":124,"titles":125,"content":126,"level":40},"/v1.0.8/learn/concepts#select-vs-query","Select vs Query",[106,120],"Soy provides two SELECT builders: MethodReturnsUse CaseSelect()*TSingle record (uses LIMIT 1)Query()[]*TMultiple records // Single user by ID\nuser, err := users.Select().\n    Where(\"id\", \"=\", \"user_id\").\n    Exec(ctx, map[string]any{\"user_id\": 123})\n\n// All active users\nactiveUsers, err := users.Query().\n    Where(\"status\", \"=\", \"status_param\").\n    Exec(ctx, map[string]any{\"status_param\": \"active\"})",{"id":128,"title":129,"titles":130,"content":131,"level":40},"/v1.0.8/learn/concepts#field-selection","Field Selection",[106,120],"Select specific fields: // Only id and email\nusers.Select().Fields(\"id\", \"email\") Invalid field names produce errors at build time.",{"id":133,"title":134,"titles":135,"content":136,"level":40},"/v1.0.8/learn/concepts#execution-methods","Execution Methods",[106,120],"All builders provide: MethodReturnsPurposeExec(ctx, params)Result, errorExecute queryExecTx(ctx, tx, params)Result, errorExecute within transactionRender()*astql.QueryResult, errorGet generated SQL without executingMustRender()*astql.QueryResultPanic on error (for tests/init) // Inspect SQL before execution\nresult, err := users.Select().Where(\"id\", \"=\", \"id\").Render()\nfmt.Printf(\"SQL: %s\\n\", result.SQL)\n// Output: SQL: SELECT id, email, name, age FROM users WHERE id = :id LIMIT 1",{"id":138,"title":139,"titles":140,"content":34,"level":19},"/v1.0.8/learn/concepts#conditions","Conditions",[106],{"id":142,"title":143,"titles":144,"content":145,"level":40},"/v1.0.8/learn/concepts#simple-conditions","Simple Conditions",[106,139],"WHERE clauses use three arguments: field, operator, parameter name. users.Select().Where(\"age\", \">=\", \"min_age\") The parameter name maps to values in the params map: params := map[string]any{\"min_age\": 18}\nuser, err := users.Select().Where(\"age\", \">=\", \"min_age\").Exec(ctx, params)",{"id":147,"title":148,"titles":149,"content":150,"level":40},"/v1.0.8/learn/concepts#supported-operators","Supported Operators",[106,139],"CategoryOperatorsComparison=, !=, >, >=, \u003C, \u003C=PatternLIKE, NOT LIKE, ILIKE, NOT ILIKESetIN, NOT INRegex~, ~*, !~, !~*Array@>, \u003C@, &&Vector\u003C->, \u003C#>, \u003C=>, \u003C+>",{"id":152,"title":153,"titles":154,"content":155,"level":40},"/v1.0.8/learn/concepts#condition-helpers","Condition Helpers",[106,139],"Use helpers for complex conditions: // Simple condition\nC(\"field\", \"operator\", \"param\")\n\n// NULL checks\nNull(\"field\")     // field IS NULL\nNotNull(\"field\")  // field IS NOT NULL",{"id":157,"title":158,"titles":159,"content":160,"level":40},"/v1.0.8/learn/concepts#andor-groups","AND/OR Groups",[106,139],"Combine conditions: // AND group\nusers.Query().WhereAnd(\n    soy.C(\"age\", \">=\", \"min_age\"),\n    soy.C(\"status\", \"=\", \"status\"),\n)\n// WHERE (age >= :min_age AND status = :status)\n\n// OR group\nusers.Query().WhereOr(\n    soy.C(\"role\", \"=\", \"admin\"),\n    soy.C(\"role\", \"=\", \"moderator\"),\n)\n// WHERE (role = :admin OR role = :moderator)\n\n// Combined\nusers.Query().\n    WhereAnd(\n        soy.C(\"age\", \">=\", \"min_age\"),\n        soy.C(\"status\", \"=\", \"status\"),\n    ).\n    WhereOr(\n        soy.C(\"role\", \"=\", \"admin\"),\n        soy.C(\"role\", \"=\", \"mod\"),\n    )\n// WHERE (age >= :min_age AND status = :status) AND (role = :admin OR role = :mod)",{"id":162,"title":163,"titles":164,"content":165,"level":40},"/v1.0.8/learn/concepts#null-conditions","NULL Conditions",[106,139],"// IS NULL\nusers.Query().WhereNull(\"deleted_at\")\n\n// IS NOT NULL\nusers.Query().WhereNotNull(\"email\")",{"id":167,"title":168,"titles":169,"content":34,"level":19},"/v1.0.8/learn/concepts#ordering","Ordering",[106],{"id":171,"title":172,"titles":173,"content":174,"level":40},"/v1.0.8/learn/concepts#basic-ordering","Basic Ordering",[106,168],"users.Query().OrderBy(\"name\", \"asc\")\nusers.Query().OrderBy(\"created_at\", \"desc\")",{"id":176,"title":177,"titles":178,"content":179,"level":40},"/v1.0.8/learn/concepts#null-handling","NULL Handling",[106,168],"Control where NULLs appear: // NULLs first\nusers.Query().OrderByNulls(\"score\", \"desc\", \"first\")\n\n// NULLs last\nusers.Query().OrderByNulls(\"score\", \"asc\", \"last\")",{"id":181,"title":182,"titles":183,"content":184,"level":40},"/v1.0.8/learn/concepts#expression-ordering","Expression Ordering",[106,168],"For pgvector similarity or custom expressions: // Order by vector distance\nusers.Query().OrderByExpr(\"embedding\", \"\u003C->\", \"query_vec\", \"asc\")",{"id":186,"title":187,"titles":188,"content":189,"level":19},"/v1.0.8/learn/concepts#pagination","Pagination",[106],"users.Query().\n    OrderBy(\"created_at\", \"desc\").\n    Limit(10).\n    Offset(20)",{"id":191,"title":192,"titles":193,"content":194,"level":19},"/v1.0.8/learn/concepts#builders","Builders",[106],"Each operation has a dedicated builder: BuilderMethodPurposeSelect[T]Select()Single record queriesQuery[T]Query()Multi-record queriesCreate[T]Insert()INSERT operationsUpdate[T]Modify()UPDATE operationsDelete[T]Remove()DELETE operationsAggregate[T]Count(), Sum(), etc.Aggregate functionsCompound[T]Via Query().Union()Set operations",{"id":196,"title":197,"titles":198,"content":199,"level":40},"/v1.0.8/learn/concepts#builder-pattern","Builder Pattern",[106,192],"Builders use method chaining with deferred execution: query := users.Query().\n    Where(\"status\", \"=\", \"status\").\n    OrderBy(\"name\", \"asc\").\n    Limit(10)\n\n// SQL not generated yet\n\nresults, err := query.Exec(ctx, params)\n// SQL generated and executed",{"id":201,"title":202,"titles":203,"content":204,"level":40},"/v1.0.8/learn/concepts#error-handling","Error Handling",[106,192],"Builders capture errors internally and report them at execution: query := users.Query().\n    Where(\"invalid_field\", \"=\", \"param\")  // Error captured here\n\nresults, err := query.Exec(ctx, params)   // Error returned here\n// err: field \"invalid_field\" not in schema This allows fluent chaining without checking errors at each step.",{"id":206,"title":207,"titles":208,"content":209,"level":19},"/v1.0.8/learn/concepts#specs","Specs",[106],"Specs are JSON-serializable query definitions. Use them for: LLM-generated queriesConfiguration filesAPI request bodies spec := soy.QuerySpec{\n    Fields: []string{\"id\", \"email\"},\n    Where: []soy.ConditionSpec{\n        {Field: \"age\", Operator: \">=\", Param: \"min_age\"},\n    },\n    OrderBy: []soy.OrderBySpec{\n        {Field: \"name\", Direction: \"asc\"},\n    },\n    Limit: intPtr(10),\n}\n\nquery := users.QueryFromSpec(spec)\nresults, err := query.Exec(ctx, params) See Specs Guide for complete spec documentation.",{"id":211,"title":212,"titles":213,"content":214,"level":19},"/v1.0.8/learn/concepts#transactions","Transactions",[106],"Execute queries within transactions: tx, err := db.BeginTxx(ctx, nil)\nif err != nil {\n    return err\n}\ndefer tx.Rollback()\n\nuser, err := users.Select().\n    Where(\"id\", \"=\", \"user_id\").\n    ExecTx(ctx, tx, params)\nif err != nil {\n    return err\n}\n\n_, err = users.Modify().\n    Set(\"login_count\", \"new_count\").\n    Where(\"id\", \"=\", \"user_id\").\n    ExecTx(ctx, tx, params)\nif err != nil {\n    return err\n}\n\nreturn tx.Commit()",{"id":216,"title":217,"titles":218,"content":219,"level":19},"/v1.0.8/learn/concepts#lifecycle-callbacks","Lifecycle Callbacks",[106],"Register callbacks on a Soy[T] instance to intercept records as they are read or written: // Normalize email after every scan\nusers.OnScan(func(ctx context.Context, result *User) error {\n    result.Email = strings.ToLower(result.Email)\n    return nil\n})\n\n// Validate before every insert\nusers.OnRecord(func(ctx context.Context, record *User) error {\n    if record.Email == \"\" {\n        return errors.New(\"email is required\")\n    }\n    return nil\n}) OnScan fires after each row is scanned into *T — in Select, Query, Update, Create, and Compound paths.OnRecord fires before each *T is written — in Create paths (single, batch, upsert).Callbacks are nil by default with zero overhead when unregistered.Returning an error aborts the operation. See the Lifecycle Guide for details.",{"id":221,"title":222,"titles":223,"content":224,"level":19},"/v1.0.8/learn/concepts#row-locking","Row Locking",[106],"Lock rows for concurrent access: // Exclusive lock\nusers.Select().Where(\"id\", \"=\", \"id\").ForUpdate()\n\n// Shared lock\nusers.Select().Where(\"id\", \"=\", \"id\").ForShare()\n\n// Less restrictive locks\nusers.Select().Where(\"id\", \"=\", \"id\").ForNoKeyUpdate()\nusers.Select().Where(\"id\", \"=\", \"id\").ForKeyShare() 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 .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)}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}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 .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 .suWN2, html code.shiki .suWN2{--shiki-default:var(--shiki-tag)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}",{"id":226,"title":16,"titles":227,"content":228,"level":9},"/v1.0.8/learn/architecture",[],"System design and internal structure of soy",{"id":230,"title":16,"titles":231,"content":232,"level":9},"/v1.0.8/learn/architecture#architecture",[],"Soy is a thin orchestration layer that combines three libraries to provide type-safe, schema-validated SQL queries.",{"id":234,"title":235,"titles":236,"content":237,"level":19},"/v1.0.8/learn/architecture#component-overview","Component Overview",[16],"┌─────────────────────────────────────────────────────────────────┐\n│                          Soy[T]                                 │\n│                                                                 │\n│  ┌───────────────┐  ┌───────────────┐  ┌───────────────────┐   │\n│  │   Sentinel    │  │    ASTQL      │  │      sqlx         │   │\n│  │  (metadata)   │  │   (schema)    │  │   (execution)     │   │\n│  └───────────────┘  └───────────────┘  └───────────────────┘   │\n│         │                  │                    │               │\n│         └──────────────────┴────────────────────┘               │\n│                            │                                    │\n│  ┌─────────────────────────┴─────────────────────────────────┐  │\n│  │                    Query Builders                         │  │\n│  │  Select │ Query │ Insert │ Modify │ Remove │ Aggregates  │  │\n│  └───────────────────────────────────────────────────────────┘  │\n└─────────────────────────────────────────────────────────────────┘",{"id":239,"title":240,"titles":241,"content":34,"level":19},"/v1.0.8/learn/architecture#dependencies","Dependencies",[16],{"id":243,"title":244,"titles":245,"content":246,"level":40},"/v1.0.8/learn/architecture#sentinel","Sentinel",[16,240],"github.com/zoobz-io/sentinel Sentinel extracts struct metadata from Go types. It reads db and type tags to build a schema representation. type User struct {\n    ID    int64  `db:\"id\" type:\"bigserial primary key\"`\n    Email string `db:\"email\" type:\"text unique not null\"`\n} Sentinel inspects this once at soy.New[User]() and caches the field names, types, and constraints. This metadata is then passed to ASTQL for query validation.",{"id":248,"title":249,"titles":250,"content":251,"level":40},"/v1.0.8/learn/architecture#astql","ASTQL",[16,240],"github.com/zoobz-io/astql ASTQL is an abstract SQL builder with schema validation. It: Validates field names against the schemaBuilds an AST representation of the queryRenders to dialect-specific SQL via providers ASTQL providers handle database differences: import \"github.com/zoobz-io/astql/pkg/postgres\"\n\n// PostgreSQL: uses $1, $2 placeholders\nusers, _ := soy.New[User](db, \"users\", postgres.New())\n\n// MariaDB: uses ? placeholders, different RETURNING syntax\nusers, _ := soy.New[User](db, \"users\", mariadb.New())",{"id":253,"title":254,"titles":255,"content":256,"level":40},"/v1.0.8/learn/architecture#sqlx","sqlx",[16,240],"github.com/jmoiron/sqlx sqlx executes queries and scans results into structs. Soy uses: sqlx.ExtContext for query executionNamed parameter bindingStruct scanning for results",{"id":258,"title":259,"titles":260,"content":34,"level":19},"/v1.0.8/learn/architecture#data-flow","Data Flow",[16],{"id":262,"title":263,"titles":264,"content":265,"level":40},"/v1.0.8/learn/architecture#initialisation-cold-path","Initialisation (Cold Path)",[16,259],"soy.New[T](db, table, provider)\n    │\n    ├─→ Sentinel: Extract struct metadata\n    │       └─→ Cache field names, types, constraints\n    │\n    ├─→ ASTQL: Create schema instance\n    │       └─→ Register fields and parameters\n    │\n    └─→ Return Soy[T] with cached metadata This happens once per table. All reflection occurs here.",{"id":267,"title":268,"titles":269,"content":270,"level":40},"/v1.0.8/learn/architecture#query-building-hot-path","Query Building (Hot Path)",[16,259],"users.Select().Where(\"email\", \"=\", \"email_param\")\n    │\n    ├─→ Create new SelectBuilder (reuses cached metadata)\n    │\n    ├─→ Validate \"email\" against schema (map lookup)\n    │\n    ├─→ Validate \"=\" operator (map lookup)\n    │\n    ├─→ Register \"email_param\" as parameter\n    │\n    └─→ Return SelectBuilder (no SQL generated yet) No reflection, no allocations beyond the builder struct.",{"id":272,"title":273,"titles":274,"content":275,"level":40},"/v1.0.8/learn/architecture#execution","Execution",[16,259],"builder.Exec(ctx, params)\n    │\n    ├─→ ASTQL: Render SQL string\n    │       └─→ Provider transforms to dialect\n    │\n    ├─→ sqlx: Bind named parameters\n    │\n    ├─→ sqlx: Execute query\n    │\n    └─→ sqlx: Scan result into *T or []*T",{"id":277,"title":197,"titles":278,"content":279,"level":19},"/v1.0.8/learn/architecture#builder-pattern",[16],"Each operation returns a new builder that carries forward the state: // Each method returns a new builder\nb1 := users.Select()\nb2 := b1.Where(\"email\", \"=\", \"email_param\")\nb3 := b2.OrderBy(\"created_at\", \"desc\") Builders are: Immutable — each method returns a new instanceChainable — methods can be called in sequenceLazy — SQL is only generated at Exec()",{"id":281,"title":202,"titles":282,"content":283,"level":19},"/v1.0.8/learn/architecture#error-handling",[16],"Errors can occur at two points: Build time — invalid field names, operators, or parametersExec time — database errors Build-time errors are accumulated in the builder: builder := users.Select().\n    Where(\"invalid_field\", \"=\", \"param\")  // Error stored in builder\n\n_, err := builder.Exec(ctx, params)       // Error returned here This allows chaining to continue without interruption while ensuring errors are not silently ignored.",{"id":285,"title":286,"titles":287,"content":288,"level":19},"/v1.0.8/learn/architecture#event-system","Event System",[16],"Soy integrates with capitan for structured events: // Signals emitted during query execution\nQueryStarted   // When a query begins\nQueryCompleted // When a query succeeds\nQueryFailed    // When a query fails Events include contextual data: Table nameOperation type (SELECT, INSERT, etc.)Duration in millisecondsRows affected/returnedError message (on failure)",{"id":290,"title":37,"titles":291,"content":292,"level":19},"/v1.0.8/learn/architecture#type-safety",[16],"Go generics flow through the entire API: // T is bound at creation\nusers, _ := soy.New[User](db, \"users\", provider)\n\n// Select returns *User\nuser, err := users.Select().Exec(ctx, params)\n\n// Query returns []*User\nall, err := users.Query().Exec(ctx, params)\n\n// Insert accepts *User, returns *User\ncreated, err := users.Insert().Exec(ctx, &User{...}) The compiler ensures type consistency. You cannot accidentally mix models or get untyped results.",{"id":294,"title":295,"titles":296,"content":297,"level":19},"/v1.0.8/learn/architecture#performance-characteristics","Performance Characteristics",[16],"OperationCostNotessoy.New[T]()O(n) reflectionOnce per table, cached.Where()O(1) map lookupSchema validation.Exec()O(n) SQL generationPlus database round-tripResult scanningO(n)sqlx handles this Query building is designed to be allocation-free on the hot path. The cost of building a query should be negligible compared to network latency. 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 .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)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}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)}",{"id":299,"title":120,"titles":300,"content":301,"level":9},"/v1.0.8/guides/queries",[],"SELECT queries with filtering, ordering, grouping, and distinct",{"id":303,"title":120,"titles":304,"content":305,"level":9},"/v1.0.8/guides/queries#queries",[],"Soy provides two SELECT builders: Select() for single records and Query() for multiple. Both share the same filtering and ordering capabilities. See API Reference for complete method signatures.",{"id":307,"title":308,"titles":309,"content":310,"level":19},"/v1.0.8/guides/queries#single-record-queries","Single Record Queries",[120],"Select() returns a single record or an error if not found: user, err := users.Select().\n    Where(\"id\", \"=\", \"user_id\").\n    Exec(ctx, map[string]any{\"user_id\": 123})\n\nif err != nil {\n    // Includes \"no rows found\" when record doesn't exist\n    return err\n}\n// user is guaranteed non-nil here",{"id":312,"title":313,"titles":314,"content":315,"level":19},"/v1.0.8/guides/queries#multi-record-queries","Multi-Record Queries",[120],"Query() returns a slice: activeUsers, err := users.Query().\n    Where(\"status\", \"=\", \"status\").\n    Exec(ctx, map[string]any{\"status\": \"active\"})\n\n// Returns []*User, empty slice if no matches",{"id":317,"title":129,"titles":318,"content":319,"level":19},"/v1.0.8/guides/queries#field-selection",[120],"Select specific columns: // Only these fields populated\nusers.Select().Fields(\"id\", \"email\", \"name\") Unselected fields have zero values in the returned struct.",{"id":321,"title":322,"titles":323,"content":34,"level":19},"/v1.0.8/guides/queries#where-conditions","WHERE Conditions",[120],{"id":325,"title":143,"titles":326,"content":327,"level":40},"/v1.0.8/guides/queries#simple-conditions",[120,322],"users.Query().Where(\"field\", \"operator\", \"param_name\") The parameter name maps to the params map at execution: users.Query().\n    Where(\"age\", \">=\", \"min_age\").\n    Where(\"status\", \"=\", \"status\").\n    Exec(ctx, map[string]any{\n        \"min_age\": 18,\n        \"status\":  \"active\",\n    }) Multiple Where() calls produce AND: WHERE age >= :min_age AND status = :status",{"id":329,"title":330,"titles":331,"content":332,"level":40},"/v1.0.8/guides/queries#and-groups","AND Groups",[120,322],"Explicit AND grouping: users.Query().WhereAnd(\n    soy.C(\"age\", \">=\", \"min\"),\n    soy.C(\"age\", \"\u003C=\", \"max\"),\n    soy.C(\"status\", \"=\", \"status\"),\n) WHERE (age >= :min AND age \u003C= :max AND status = :status)",{"id":334,"title":335,"titles":336,"content":337,"level":40},"/v1.0.8/guides/queries#or-groups","OR Groups",[120,322],"users.Query().WhereOr(\n    soy.C(\"role\", \"=\", \"admin\"),\n    soy.C(\"role\", \"=\", \"moderator\"),\n    soy.C(\"role\", \"=\", \"editor\"),\n) WHERE (role = :admin OR role = :moderator OR role = :editor)",{"id":339,"title":340,"titles":341,"content":342,"level":40},"/v1.0.8/guides/queries#combined-conditions","Combined Conditions",[120,322],"users.Query().\n    Where(\"active\", \"=\", \"is_active\").\n    WhereOr(\n        soy.C(\"role\", \"=\", \"admin\"),\n        soy.C(\"role\", \"=\", \"mod\"),\n    ) WHERE active = :is_active AND (role = :admin OR role = :mod)",{"id":344,"title":345,"titles":346,"content":347,"level":40},"/v1.0.8/guides/queries#null-checks","NULL Checks",[120,322],"// IS NULL\nusers.Query().WhereNull(\"deleted_at\")\n\n// IS NOT NULL\nusers.Query().WhereNotNull(\"verified_at\")",{"id":349,"title":350,"titles":351,"content":352,"level":40},"/v1.0.8/guides/queries#in-operator","IN Operator",[120,322],"users.Query().Where(\"status\", \"IN\", \"statuses\")\n// Execute with: {\"statuses\": []string{\"active\", \"pending\"}}",{"id":354,"title":355,"titles":356,"content":357,"level":40},"/v1.0.8/guides/queries#pattern-matching","Pattern Matching",[120,322],"// LIKE (case-sensitive)\nusers.Query().Where(\"name\", \"LIKE\", \"pattern\")\n// {\"pattern\": \"%alice%\"}\n\n// ILIKE (case-insensitive, PostgreSQL)\nusers.Query().Where(\"email\", \"ILIKE\", \"domain\")\n// {\"domain\": \"%@example.com\"}",{"id":359,"title":360,"titles":361,"content":362,"level":40},"/v1.0.8/guides/queries#array-operations","Array Operations",[120,322],"// Contains\nusers.Query().Where(\"tags\", \"@>\", \"required_tags\")\n// {\"required_tags\": []string{\"vip\", \"verified\"}}\n\n// Contained by\nusers.Query().Where(\"permissions\", \"\u003C@\", \"allowed\")\n\n// Overlap\nusers.Query().Where(\"interests\", \"&&\", \"topics\")",{"id":364,"title":365,"titles":366,"content":34,"level":19},"/v1.0.8/guides/queries#order-by","ORDER BY",[120],{"id":368,"title":172,"titles":369,"content":370,"level":40},"/v1.0.8/guides/queries#basic-ordering",[120,365],"users.Query().OrderBy(\"created_at\", \"desc\")\nusers.Query().OrderBy(\"name\", \"asc\")",{"id":372,"title":373,"titles":374,"content":375,"level":40},"/v1.0.8/guides/queries#multiple-columns","Multiple Columns",[120,365],"users.Query().\n    OrderBy(\"status\", \"asc\").\n    OrderBy(\"created_at\", \"desc\") ORDER BY status ASC, created_at DESC",{"id":377,"title":177,"titles":378,"content":379,"level":40},"/v1.0.8/guides/queries#null-handling",[120,365],"// NULLs appear first\nusers.Query().OrderByNulls(\"score\", \"desc\", \"first\")\n\n// NULLs appear last\nusers.Query().OrderByNulls(\"score\", \"asc\", \"last\")",{"id":381,"title":182,"titles":382,"content":383,"level":40},"/v1.0.8/guides/queries#expression-ordering",[120,365],"For computed values or pgvector: // Order by vector distance\nusers.Query().OrderByExpr(\"embedding\", \"\u003C->\", \"query_vector\", \"asc\")",{"id":385,"title":187,"titles":386,"content":387,"level":19},"/v1.0.8/guides/queries#pagination",[120],"users.Query().\n    OrderBy(\"created_at\", \"desc\").\n    Limit(20).\n    Offset(40)  // Page 3, 20 per page",{"id":389,"title":390,"titles":391,"content":34,"level":19},"/v1.0.8/guides/queries#distinct","DISTINCT",[120],{"id":393,"title":394,"titles":395,"content":396,"level":40},"/v1.0.8/guides/queries#simple-distinct","Simple Distinct",[120,390],"users.Query().Distinct()",{"id":398,"title":399,"titles":400,"content":401,"level":40},"/v1.0.8/guides/queries#distinct-on-postgresql","DISTINCT ON (PostgreSQL)",[120,390],"Get first row per distinct column combination: // First order per customer\norders.Query().\n    DistinctOn(\"customer_id\").\n    OrderBy(\"customer_id\", \"asc\").\n    OrderBy(\"created_at\", \"desc\") SELECT DISTINCT ON (customer_id) * FROM orders\nORDER BY customer_id ASC, created_at DESC",{"id":403,"title":404,"titles":405,"content":34,"level":19},"/v1.0.8/guides/queries#group-by-and-having","GROUP BY and HAVING",[120],{"id":407,"title":408,"titles":409,"content":410,"level":40},"/v1.0.8/guides/queries#basic-grouping","Basic Grouping",[120,404],"users.Query().\n    Fields(\"status\", \"COUNT(*) as count\").\n    GroupBy(\"status\")",{"id":412,"title":413,"titles":414,"content":415,"level":40},"/v1.0.8/guides/queries#having-with-conditions","HAVING with Conditions",[120,404],"users.Query().\n    Fields(\"status\", \"COUNT(*) as count\").\n    GroupBy(\"status\").\n    Having(\"count\", \">=\", \"min_count\")",{"id":417,"title":418,"titles":419,"content":420,"level":40},"/v1.0.8/guides/queries#aggregate-having","Aggregate HAVING",[120,404],"users.Query().\n    Fields(\"department_id\").\n    GroupBy(\"department_id\").\n    HavingAgg(\"COUNT\", \"*\", \">\", \"threshold\") HAVING COUNT(*) > :threshold",{"id":422,"title":222,"titles":423,"content":424,"level":19},"/v1.0.8/guides/queries#row-locking",[120],"For transactional consistency: // Exclusive lock - other transactions wait\nuser, err := users.Select().\n    Where(\"id\", \"=\", \"id\").\n    ForUpdate().\n    ExecTx(ctx, tx, params)\n\n// Shared lock - allows other shared locks\nusers.Select().ForShare()\n\n// Less restrictive locks\nusers.Select().ForNoKeyUpdate()\nusers.Select().ForKeyShare() Lock modes: MethodLock TypeBlocksForUpdate()ExclusiveAll other locksForNoKeyUpdate()Exclusive (non-key)FOR UPDATE, FOR NO KEY UPDATEForShare()SharedFOR UPDATE, FOR NO KEY UPDATEForKeyShare()Key shareFOR UPDATE",{"id":426,"title":217,"titles":427,"content":428,"level":19},"/v1.0.8/guides/queries#lifecycle-callbacks",[120],"Register an OnScan callback to transform or validate every record as it is scanned. See the Lifecycle Guide for full details. users.OnScan(func(ctx context.Context, result *User) error {\n    result.Email = strings.ToLower(result.Email)\n    return nil\n})\n\n// Fires per row in both Select and Query paths\nuser, _ := users.Select().Where(\"id\", \"=\", \"id\").Exec(ctx, params)\n// user.Email is lowercased\n\nallUsers, _ := users.Query().Exec(ctx, nil)\n// every user.Email is lowercased",{"id":430,"title":431,"titles":432,"content":433,"level":19},"/v1.0.8/guides/queries#complete-example","Complete Example",[120],"// Complex query with multiple features\nresults, err := users.Query().\n    Fields(\"id\", \"name\", \"email\", \"score\").\n    Where(\"status\", \"=\", \"status\").\n    WhereNotNull(\"email\").\n    WhereOr(\n        soy.C(\"role\", \"=\", \"admin\"),\n        soy.C(\"score\", \">=\", \"threshold\"),\n    ).\n    OrderByNulls(\"score\", \"desc\", \"last\").\n    OrderBy(\"name\", \"asc\").\n    Limit(50).\n    Offset(0).\n    Exec(ctx, map[string]any{\n        \"status\":    \"active\",\n        \"admin\":     \"admin\",\n        \"threshold\": 100,\n    }) SELECT id, name, email, score\nFROM users\nWHERE status = :status\n  AND email IS NOT NULL\n  AND (role = :admin OR score >= :threshold)\nORDER BY score DESC NULLS LAST, name ASC\nLIMIT 50 OFFSET 0 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 .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 .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}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 .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}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)}",{"id":435,"title":436,"titles":437,"content":438,"level":9},"/v1.0.8/guides/mutations","Mutations",[],"INSERT, UPDATE, and DELETE operations with safety features",{"id":440,"title":436,"titles":441,"content":442,"level":9},"/v1.0.8/guides/mutations#mutations",[],"Soy provides three mutation builders: Insert(), Modify(), and Remove(). Each includes safety features to prevent common mistakes. See API Reference for complete method signatures.",{"id":444,"title":445,"titles":446,"content":34,"level":19},"/v1.0.8/guides/mutations#insert-operations","INSERT Operations",[436],{"id":448,"title":449,"titles":450,"content":451,"level":40},"/v1.0.8/guides/mutations#basic-insert","Basic Insert",[436,445],"user := &User{\n    Email: \"alice@example.com\",\n    Name:  \"Alice\",\n    Age:   30,\n}\n\ncreated, err := users.Insert().Exec(ctx, user)\n// created has the database-generated ID\nfmt.Println(created.ID) INSERT automatically includes RETURNING * to populate generated columns.",{"id":453,"title":454,"titles":455,"content":456,"level":40},"/v1.0.8/guides/mutations#batch-insert","Batch Insert",[436,445],"newUsers := []*User{\n    {Email: \"bob@example.com\", Name: \"Bob\", Age: 25},\n    {Email: \"carol@example.com\", Name: \"Carol\", Age: 35},\n}\n\ncount, err := users.Insert().ExecBatch(ctx, newUsers)\n// count is the number of records inserted (int64)",{"id":458,"title":459,"titles":460,"content":461,"level":40},"/v1.0.8/guides/mutations#on-conflict-do-nothing","ON CONFLICT - Do Nothing",[436,445],"Silently skip conflicts: user := &User{Email: \"alice@example.com\", Name: \"Alice\"}\n\ncreated, err := users.Insert().\n    OnConflict(\"email\").\n    DoNothing().\n    Exec(ctx, user)\n\n// created is nil if conflict occurred",{"id":463,"title":464,"titles":465,"content":466,"level":40},"/v1.0.8/guides/mutations#on-conflict-do-update-upsert","ON CONFLICT - Do Update (Upsert)",[436,445],"Update on conflict: user := &User{Email: \"alice@example.com\", Name: \"Alice Updated\", Age: 31}\n\nupserted, err := users.Insert().\n    OnConflict(\"email\").\n    DoUpdate().\n    Set(\"name\", \"name\").\n    Set(\"age\", \"age\").\n    Build().\n    Exec(ctx, user) INSERT INTO users (email, name, age) VALUES (:email, :name, :age)\nON CONFLICT (email) DO UPDATE SET name = :name, age = :age\nRETURNING *",{"id":468,"title":469,"titles":470,"content":471,"level":40},"/v1.0.8/guides/mutations#multiple-conflict-columns","Multiple Conflict Columns",[436,445],"users.Insert().\n    OnConflict(\"tenant_id\", \"email\").\n    DoUpdate().\n    Set(\"name\", \"name\").\n    Build()",{"id":473,"title":474,"titles":475,"content":34,"level":19},"/v1.0.8/guides/mutations#update-operations","UPDATE Operations",[436],{"id":477,"title":478,"titles":479,"content":480,"level":40},"/v1.0.8/guides/mutations#basic-update","Basic Update",[436,474],"updated, err := users.Modify().\n    Set(\"name\", \"new_name\").\n    Set(\"age\", \"new_age\").\n    Where(\"id\", \"=\", \"user_id\").\n    Exec(ctx, map[string]any{\n        \"new_name\": \"Alice Smith\",\n        \"new_age\":  31,\n        \"user_id\":  123,\n    }) UPDATE returns the updated record via RETURNING *.",{"id":482,"title":483,"titles":484,"content":485,"level":40},"/v1.0.8/guides/mutations#required-where","Required WHERE",[436,474],"UPDATE requires at least one WHERE condition. This prevents accidental full-table updates: // Error: UPDATE requires WHERE clause\nusers.Modify().Set(\"status\", \"inactive\").Exec(ctx, params)\n\n// Correct\nusers.Modify().\n    Set(\"status\", \"inactive\").\n    Where(\"id\", \"=\", \"user_id\").\n    Exec(ctx, params)",{"id":487,"title":488,"titles":489,"content":490,"level":40},"/v1.0.8/guides/mutations#computed-assignments","Computed Assignments",[436,474],"Use SetExpr for atomic increments, decrements, and other expression-based updates: updated, err := users.Modify().\n    SetExpr(\"login_count\", \"+\", \"increment\").\n    Where(\"id\", \"=\", \"user_id\").\n    Exec(ctx, map[string]any{\n        \"increment\": 1,\n        \"user_id\":   123,\n    }) UPDATE users SET \"login_count\" = \"login_count\" + :increment WHERE \"id\" = :user_id RETURNING * SetExpr can be combined with Set in the same query: users.Modify().\n    Set(\"name\", \"new_name\").\n    SetExpr(\"age\", \"+\", \"increment\").\n    Where(\"id\", \"=\", \"user_id\").\n    Exec(ctx, params) Supported arithmetic operators: +, -, *, /, %.",{"id":492,"title":493,"titles":494,"content":495,"level":40},"/v1.0.8/guides/mutations#multiple-conditions","Multiple Conditions",[436,474],"users.Modify().\n    Set(\"verified\", \"verified\").\n    Where(\"email\", \"=\", \"email\").\n    WhereNull(\"verified_at\").\n    Exec(ctx, map[string]any{\n        \"verified\": true,\n        \"email\":    \"alice@example.com\",\n    })",{"id":497,"title":498,"titles":499,"content":500,"level":40},"/v1.0.8/guides/mutations#batch-update","Batch Update",[436,474],"Update multiple records with different values: updates := []map[string]any{\n    {\"user_id\": 1, \"new_score\": 100},\n    {\"user_id\": 2, \"new_score\": 200},\n    {\"user_id\": 3, \"new_score\": 300},\n}\n\nrowsAffected, err := users.Modify().\n    Set(\"score\", \"new_score\").\n    Where(\"id\", \"=\", \"user_id\").\n    ExecBatch(ctx, updates)",{"id":502,"title":503,"titles":504,"content":34,"level":19},"/v1.0.8/guides/mutations#delete-operations","DELETE Operations",[436],{"id":506,"title":507,"titles":508,"content":509,"level":40},"/v1.0.8/guides/mutations#basic-delete","Basic Delete",[436,503],"rowsDeleted, err := users.Remove().\n    Where(\"id\", \"=\", \"user_id\").\n    Exec(ctx, map[string]any{\"user_id\": 123}) DELETE returns the number of affected rows.",{"id":511,"title":483,"titles":512,"content":513,"level":40},"/v1.0.8/guides/mutations#required-where-1",[436,503],"DELETE requires at least one WHERE condition. This prevents accidental table truncation: // Error: DELETE requires WHERE clause\nusers.Remove().Exec(ctx, nil)\n\n// Correct\nusers.Remove().Where(\"status\", \"=\", \"status\").Exec(ctx, map[string]any{\"status\": \"deleted\"})",{"id":515,"title":493,"titles":516,"content":517,"level":40},"/v1.0.8/guides/mutations#multiple-conditions-1",[436,503],"users.Remove().\n    Where(\"status\", \"=\", \"status\").\n    Where(\"created_at\", \"\u003C\", \"cutoff\").\n    Exec(ctx, map[string]any{\n        \"status\": \"inactive\",\n        \"cutoff\": time.Now().AddDate(0, -6, 0),\n    })",{"id":519,"title":520,"titles":521,"content":522,"level":40},"/v1.0.8/guides/mutations#batch-delete","Batch Delete",[436,503],"Delete with different conditions per batch: deletions := []map[string]any{\n    {\"user_id\": 1},\n    {\"user_id\": 2},\n    {\"user_id\": 3},\n}\n\ntotalDeleted, err := users.Remove().\n    Where(\"id\", \"=\", \"user_id\").\n    ExecBatch(ctx, deletions)",{"id":524,"title":217,"titles":525,"content":526,"level":19},"/v1.0.8/guides/mutations#lifecycle-callbacks",[436],"Register callbacks to intercept records before writes or after scans. See the Lifecycle Guide for full details. // Validate before insert\nusers.OnRecord(func(ctx context.Context, record *User) error {\n    if record.Email == \"\" {\n        return errors.New(\"email is required\")\n    }\n    return nil\n})\n\n// Normalize after scan (fires on INSERT RETURNING, UPDATE RETURNING)\nusers.OnScan(func(ctx context.Context, result *User) error {\n    result.Email = strings.ToLower(result.Email)\n    return nil\n})",{"id":528,"title":212,"titles":529,"content":530,"level":19},"/v1.0.8/guides/mutations#transactions",[436],"Execute mutations within transactions: tx, err := db.BeginTxx(ctx, nil)\nif err != nil {\n    return err\n}\ndefer tx.Rollback()\n\n// Insert order\norder, err := orders.Insert().Build().ExecTx(ctx, tx, newOrder)\nif err != nil {\n    return err\n}\n\n// Insert order items\nfor _, item := range items {\n    item.OrderID = order.ID\n    _, err := orderItems.Insert().Build().ExecTx(ctx, tx, item)\n    if err != nil {\n        return err\n    }\n}\n\n// Update inventory\n_, err = inventory.Modify().\n    Set(\"quantity\", \"new_qty\").\n    Where(\"product_id\", \"=\", \"product_id\").\n    ExecTx(ctx, tx, params)\nif err != nil {\n    return err\n}\n\nreturn tx.Commit()",{"id":532,"title":202,"titles":533,"content":534,"level":19},"/v1.0.8/guides/mutations#error-handling",[436],"Mutations capture builder errors and report them at execution: result, err := users.Modify().\n    Set(\"invalid_field\", \"value\").  // Error captured\n    Where(\"id\", \"=\", \"id\").\n    Exec(ctx, params)\n\n// err: field \"invalid_field\" not in schema",{"id":536,"title":537,"titles":538,"content":539,"level":19},"/v1.0.8/guides/mutations#inspecting-sql","Inspecting SQL",[436],"Use Render() to see generated SQL: result, err := users.Modify().\n    Set(\"name\", \"new_name\").\n    Where(\"id\", \"=\", \"user_id\").\n    Render()\n\nfmt.Println(result.SQL)\n// UPDATE users SET name = :new_name WHERE id = :user_id RETURNING *",{"id":541,"title":431,"titles":542,"content":543,"level":19},"/v1.0.8/guides/mutations#complete-example",[436],"// Upsert with transaction\nfunc UpsertUser(ctx context.Context, db *sqlx.DB, user *User) (*User, error) {\n    tx, err := db.BeginTxx(ctx, nil)\n    if err != nil {\n        return nil, err\n    }\n    defer tx.Rollback()\n\n    // Try insert with conflict handling\n    result, err := users.Insert().\n        OnConflict(\"email\").\n        DoUpdate().\n        Set(\"name\", \"name\").\n        Set(\"age\", \"age\").\n        Set(\"updated_at\", \"now\").\n        Build().\n        ExecTx(ctx, tx, map[string]any{\n            \"email\": user.Email,\n            \"name\":  user.Name,\n            \"age\":   user.Age,\n            \"now\":   time.Now(),\n        })\n    if err != nil {\n        return nil, err\n    }\n\n    // Log the change\n    _, err = auditLogs.Insert().Build().ExecTx(ctx, tx, &AuditLog{\n        UserID:    result.ID,\n        Action:    \"upsert\",\n        Timestamp: time.Now(),\n    })\n    if err != nil {\n        return nil, err\n    }\n\n    if err := tx.Commit(); err != nil {\n        return nil, err\n    }\n\n    return result, nil\n}",{"id":545,"title":546,"titles":547,"content":548,"level":19},"/v1.0.8/guides/mutations#dialect-specific-behaviour","Dialect-Specific Behaviour",[436],"Different databases have varying levels of support for mutation features. Soy automatically adapts to each dialect's capabilities.",{"id":550,"title":551,"titles":552,"content":553,"level":40},"/v1.0.8/guides/mutations#feature-support-matrix","Feature Support Matrix",[436,546],"FeaturePostgreSQLMariaDBSQLiteMSSQLON CONFLICT / UpsertNativeON DUPLICATE KEYNativeFallback*RETURNING on INSERTYesYesYesNoRETURNING on UPDATEYesNo**YesNoRETURNING on DELETEYesYesYesNo * MSSQL uses UPDATE-then-INSERT fallback for upsert operations.\n** MariaDB tracks MDEV-5092 for future support.",{"id":555,"title":556,"titles":557,"content":558,"level":40},"/v1.0.8/guides/mutations#mssql-upsert-fallback","MSSQL Upsert Fallback",[436,546],"MSSQL doesn't support ON CONFLICT syntax. When you use OnConflict() with MSSQL, Soy automatically: Attempts an UPDATE using the conflict columns as WHERE conditionsIf no rows are affected, performs an INSERTReturns the resulting record via a SELECT // This works transparently across all dialects\nresult, err := users.Insert().\n    OnConflict(\"email\").\n    DoUpdate().\n    Set(\"name\", \"name\").\n    Build().\n    Exec(ctx, user)",{"id":560,"title":561,"titles":562,"content":563,"level":40},"/v1.0.8/guides/mutations#mariadb-update-behaviour","MariaDB UPDATE Behaviour",[436,546],"MariaDB doesn't support RETURNING on UPDATE statements. When executing Modify(): The UPDATE is executed without RETURNINGA follow-up SELECT retrieves the updated record using the WHERE conditions This is handled automatically - your code remains the same across dialects. 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 .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 .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}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 .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)}",{"id":565,"title":566,"titles":567,"content":568,"level":9},"/v1.0.8/guides/aggregates","Aggregates",[],"COUNT, SUM, AVG, MIN, MAX with GROUP BY and HAVING",{"id":570,"title":566,"titles":571,"content":572,"level":9},"/v1.0.8/guides/aggregates#aggregates",[],"Soy provides aggregate functions for counting and computing statistics. All aggregates return float64 to handle various numeric types uniformly. See API Reference for complete method signatures.",{"id":574,"title":575,"titles":576,"content":34,"level":19},"/v1.0.8/guides/aggregates#count","COUNT",[566],{"id":578,"title":579,"titles":580,"content":581,"level":40},"/v1.0.8/guides/aggregates#count-all-rows","Count All Rows",[566,575],"count, err := users.Count().Exec(ctx, nil)\n// count is float64\nfmt.Printf(\"Total users: %.0f\\n\", count)",{"id":583,"title":584,"titles":585,"content":586,"level":40},"/v1.0.8/guides/aggregates#count-with-where","Count with WHERE",[566,575],"count, err := users.Count().\n    Where(\"status\", \"=\", \"status\").\n    Exec(ctx, map[string]any{\"status\": \"active\"})",{"id":588,"title":589,"titles":590,"content":591,"level":19},"/v1.0.8/guides/aggregates#sum","SUM",[566],"Sum a numeric field: total, err := orders.Sum(\"amount\").\n    Where(\"status\", \"=\", \"status\").\n    Exec(ctx, map[string]any{\"status\": \"completed\"})\n\nfmt.Printf(\"Total revenue: $%.2f\\n\", total)",{"id":593,"title":594,"titles":595,"content":596,"level":19},"/v1.0.8/guides/aggregates#avg","AVG",[566],"Calculate average: avgAge, err := users.Avg(\"age\").\n    Where(\"status\", \"=\", \"status\").\n    Exec(ctx, map[string]any{\"status\": \"active\"})",{"id":598,"title":599,"titles":600,"content":601,"level":19},"/v1.0.8/guides/aggregates#min-and-max","MIN and MAX",[566],"// Minimum\nminPrice, err := products.Min(\"price\").Exec(ctx, nil)\n\n// Maximum\nmaxScore, err := scores.Max(\"value\").\n    Where(\"game_id\", \"=\", \"game\").\n    Exec(ctx, map[string]any{\"game\": 123})",{"id":603,"title":177,"titles":604,"content":605,"level":19},"/v1.0.8/guides/aggregates#null-handling",[566],"Aggregate functions return 0.0 when the result is NULL (e.g., no matching rows): sum, err := orders.Sum(\"amount\").\n    Where(\"user_id\", \"=\", \"user\").\n    Exec(ctx, map[string]any{\"user\": 999})  // User with no orders\n\n// sum == 0.0, err == nil",{"id":607,"title":608,"titles":609,"content":610,"level":19},"/v1.0.8/guides/aggregates#group-by","GROUP BY",[566],"Use Query with aggregate expressions for grouped results: type StatusCount struct {\n    Status string `db:\"status\"`\n    Count  int    `db:\"count\"`\n}\n\n// Note: This requires a custom query or raw SQL\n// Soy aggregates return single values For grouped aggregates, use the Query builder with custom fields: results, err := users.Query().\n    Fields(\"status\", \"COUNT(*) as count\").\n    GroupBy(\"status\").\n    Exec(ctx, nil)",{"id":612,"title":613,"titles":614,"content":615,"level":19},"/v1.0.8/guides/aggregates#having","HAVING",[566],"Filter aggregate results: results, err := orders.Query().\n    Fields(\"customer_id\", \"SUM(amount) as total\").\n    GroupBy(\"customer_id\").\n    Having(\"total\", \">=\", \"min_total\").\n    Exec(ctx, map[string]any{\"min_total\": 1000})",{"id":617,"title":418,"titles":618,"content":619,"level":40},"/v1.0.8/guides/aggregates#aggregate-having",[566,613],"Use aggregate functions in HAVING: results, err := orders.Query().\n    Fields(\"customer_id\").\n    GroupBy(\"customer_id\").\n    HavingAgg(\"COUNT\", \"*\", \">=\", \"min_orders\").\n    Exec(ctx, map[string]any{\"min_orders\": 5}) SELECT customer_id FROM orders\nGROUP BY customer_id\nHAVING COUNT(*) >= :min_orders",{"id":621,"title":622,"titles":623,"content":624,"level":19},"/v1.0.8/guides/aggregates#combined-where-and-having","Combined WHERE and HAVING",[566],"results, err := orders.Query().\n    Fields(\"customer_id\", \"SUM(amount) as total\").\n    Where(\"created_at\", \">=\", \"start_date\").\n    GroupBy(\"customer_id\").\n    HavingAgg(\"SUM\", \"amount\", \">=\", \"threshold\").\n    OrderBy(\"total\", \"desc\").\n    Limit(10).\n    Exec(ctx, map[string]any{\n        \"start_date\": time.Now().AddDate(0, -1, 0),\n        \"threshold\":  500,\n    })",{"id":626,"title":212,"titles":627,"content":628,"level":19},"/v1.0.8/guides/aggregates#transactions",[566],"Aggregates support transaction execution: tx, err := db.BeginTxx(ctx, nil)\nif err != nil {\n    return err\n}\ndefer tx.Rollback()\n\ncount, err := orders.Count().\n    Where(\"user_id\", \"=\", \"user\").\n    ExecTx(ctx, tx, params)\nif err != nil {\n    return err\n}\n\n// Use count in subsequent operations...",{"id":630,"title":537,"titles":631,"content":632,"level":19},"/v1.0.8/guides/aggregates#inspecting-sql",[566],"result, err := users.Count().\n    Where(\"status\", \"=\", \"status\").\n    Render()\n\nfmt.Println(result.SQL)\n// SELECT COUNT(*) FROM users WHERE status = :status",{"id":634,"title":431,"titles":635,"content":636,"level":19},"/v1.0.8/guides/aggregates#complete-example",[566],"// Dashboard statistics\nfunc GetDashboardStats(ctx context.Context, userID int) (*Stats, error) {\n    params := map[string]any{\"user_id\": userID}\n\n    totalOrders, err := orders.Count().\n        Where(\"user_id\", \"=\", \"user_id\").\n        Exec(ctx, params)\n    if err != nil {\n        return nil, err\n    }\n\n    totalSpent, err := orders.Sum(\"amount\").\n        Where(\"user_id\", \"=\", \"user_id\").\n        Where(\"status\", \"=\", \"status\").\n        Exec(ctx, map[string]any{\n            \"user_id\": userID,\n            \"status\":  \"completed\",\n        })\n    if err != nil {\n        return nil, err\n    }\n\n    avgOrderValue, err := orders.Avg(\"amount\").\n        Where(\"user_id\", \"=\", \"user_id\").\n        Exec(ctx, params)\n    if err != nil {\n        return nil, err\n    }\n\n    return &Stats{\n        TotalOrders:   int(totalOrders),\n        TotalSpent:    totalSpent,\n        AvgOrderValue: avgOrderValue,\n    }, nil\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 .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}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 .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 .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 .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 .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}html pre.shiki code .skxcq, html code.shiki .skxcq{--shiki-default:var(--shiki-builtin)}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)}",{"id":638,"title":207,"titles":639,"content":640,"level":9},"/v1.0.8/guides/specs",[],"JSON-serializable query specifications for external construction",{"id":642,"title":207,"titles":643,"content":644,"level":9},"/v1.0.8/guides/specs#specs",[],"Specs are JSON-serializable query definitions. They enable query construction from external sources: LLM-generated queries, configuration files, or API request bodies. See API Reference for complete type definitions.",{"id":646,"title":647,"titles":648,"content":649,"level":19},"/v1.0.8/guides/specs#use-cases","Use Cases",[207],"LLM Integration - Let language models generate query specifications that your application executes safely. Configuration - Define queries in config files for report generation or scheduled jobs. API Endpoints - Accept query parameters as JSON and convert to type-safe queries. Dynamic Filters - Build complex filter UIs that serialise to specs.",{"id":651,"title":652,"titles":653,"content":34,"level":19},"/v1.0.8/guides/specs#query-specs","Query Specs",[207],{"id":655,"title":656,"titles":657,"content":658,"level":40},"/v1.0.8/guides/specs#queryspec","QuerySpec",[207,652],"Multi-record SELECT queries: spec := soy.QuerySpec{\n    Fields:   []string{\"id\", \"email\", \"name\"},\n    Where:    []soy.ConditionSpec{...},\n    OrderBy:  []soy.OrderBySpec{...},\n    Limit:    intPtr(10),\n    Offset:   intPtr(0),\n    Distinct: true,\n}\n\nquery := users.QueryFromSpec(spec)\nresults, err := query.Exec(ctx, params)",{"id":660,"title":661,"titles":662,"content":663,"level":40},"/v1.0.8/guides/specs#selectspec","SelectSpec",[207,652],"Single-record SELECT: spec := soy.SelectSpec{\n    Fields: []string{\"id\", \"email\"},\n    Where:  []soy.ConditionSpec{\n        {Field: \"id\", Operator: \"=\", Param: \"user_id\"},\n    },\n    ForUpdate: true,\n}\n\nsel := users.SelectFromSpec(spec)\nuser, err := sel.Exec(ctx, map[string]any{\"user_id\": 123})",{"id":665,"title":666,"titles":667,"content":34,"level":19},"/v1.0.8/guides/specs#condition-specs","Condition Specs",[207],{"id":669,"title":143,"titles":670,"content":671,"level":40},"/v1.0.8/guides/specs#simple-conditions",[207,666],"condition := soy.ConditionSpec{\n    Field:    \"age\",\n    Operator: \">=\",\n    Param:    \"min_age\",\n}",{"id":673,"title":163,"titles":674,"content":675,"level":40},"/v1.0.8/guides/specs#null-conditions",[207,666],"// IS NULL\ncondition := soy.ConditionSpec{\n    Field:    \"deleted_at\",\n    Operator: \"IS NULL\",\n    IsNull:   true,\n}\n\n// IS NOT NULL\ncondition := soy.ConditionSpec{\n    Field:    \"email\",\n    Operator: \"IS NOT NULL\",\n    IsNull:   true,\n}",{"id":677,"title":678,"titles":679,"content":680,"level":40},"/v1.0.8/guides/specs#grouped-conditions","Grouped Conditions",[207,666],"AND groups: condition := soy.ConditionSpec{\n    Logic: \"AND\",\n    Group: []soy.ConditionSpec{\n        {Field: \"age\", Operator: \">=\", Param: \"min\"},\n        {Field: \"age\", Operator: \"\u003C=\", Param: \"max\"},\n        {Field: \"status\", Operator: \"=\", Param: \"status\"},\n    },\n} OR groups: condition := soy.ConditionSpec{\n    Logic: \"OR\",\n    Group: []soy.ConditionSpec{\n        {Field: \"role\", Operator: \"=\", Param: \"admin\"},\n        {Field: \"role\", Operator: \"=\", Param: \"mod\"},\n    },\n} Nested groups: // (status = 'active' AND (role = 'admin' OR role = 'mod'))\ncondition := soy.ConditionSpec{\n    Logic: \"AND\",\n    Group: []soy.ConditionSpec{\n        {Field: \"status\", Operator: \"=\", Param: \"status\"},\n        {\n            Logic: \"OR\",\n            Group: []soy.ConditionSpec{\n                {Field: \"role\", Operator: \"=\", Param: \"admin\"},\n                {Field: \"role\", Operator: \"=\", Param: \"mod\"},\n            },\n        },\n    },\n}",{"id":682,"title":683,"titles":684,"content":34,"level":19},"/v1.0.8/guides/specs#orderby-specs","OrderBy Specs",[207],{"id":686,"title":172,"titles":687,"content":688,"level":40},"/v1.0.8/guides/specs#basic-ordering",[207,683],"order := soy.OrderBySpec{\n    Field:     \"created_at\",\n    Direction: \"desc\",\n}",{"id":690,"title":177,"titles":691,"content":692,"level":40},"/v1.0.8/guides/specs#null-handling",[207,683],"order := soy.OrderBySpec{\n    Field:      \"score\",\n    Direction:  \"desc\",\n    NullsFirst: true,\n}\n\norder := soy.OrderBySpec{\n    Field:     \"score\",\n    Direction: \"asc\",\n    NullsLast: true,\n}",{"id":694,"title":182,"titles":695,"content":696,"level":40},"/v1.0.8/guides/specs#expression-ordering",[207,683],"For pgvector or computed values: order := soy.OrderBySpec{\n    Expression: \"embedding \u003C-> :query_vec\",\n    Direction:  \"asc\",\n}",{"id":698,"title":699,"titles":700,"content":34,"level":19},"/v1.0.8/guides/specs#mutation-specs","Mutation Specs",[207],{"id":702,"title":703,"titles":704,"content":705,"level":40},"/v1.0.8/guides/specs#createspec","CreateSpec",[207,699],"INSERT with conflict handling: spec := soy.CreateSpec{\n    OnConflict:     []string{\"email\"},\n    ConflictAction: \"nothing\",\n}\n\n// Or with upsert\nspec := soy.CreateSpec{\n    OnConflict:     []string{\"email\"},\n    ConflictAction: \"update\",\n    ConflictSet:    map[string]string{\"name\": \"name\", \"age\": \"age\"},\n}\n\ncreate := users.InsertFromSpec(spec)\nresult, err := create.Exec(ctx, user)",{"id":707,"title":708,"titles":709,"content":710,"level":40},"/v1.0.8/guides/specs#updatespec","UpdateSpec",[207,699],"spec := soy.UpdateSpec{\n    Set: map[string]string{\n        \"name\":   \"new_name\",\n        \"status\": \"new_status\",\n    },\n    Where: []soy.ConditionSpec{\n        {Field: \"id\", Operator: \"=\", Param: \"user_id\"},\n    },\n}\n\nupdate := users.ModifyFromSpec(spec)\nresult, err := update.Exec(ctx, params)",{"id":712,"title":713,"titles":714,"content":715,"level":40},"/v1.0.8/guides/specs#deletespec","DeleteSpec",[207,699],"spec := soy.DeleteSpec{\n    Where: []soy.ConditionSpec{\n        {Field: \"status\", Operator: \"=\", Param: \"status\"},\n        {Field: \"created_at\", Operator: \"\u003C\", Param: \"cutoff\"},\n    },\n}\n\ndel := users.RemoveFromSpec(spec)\ncount, err := del.Exec(ctx, params)",{"id":717,"title":718,"titles":719,"content":720,"level":19},"/v1.0.8/guides/specs#aggregate-specs","Aggregate Specs",[207],"The aggregate function is determined by which method you call: // COUNT (Field is ignored)\ncountSpec := soy.AggregateSpec{\n    Where: []soy.ConditionSpec{\n        {Field: \"status\", Operator: \"=\", Param: \"status\"},\n    },\n}\ncount, err := users.CountFromSpec(countSpec).Exec(ctx, params)\n\n// SUM/AVG/MIN/MAX (Field is required)\nsumSpec := soy.AggregateSpec{\n    Field: \"amount\",\n    Where: []soy.ConditionSpec{\n        {Field: \"status\", Operator: \"=\", Param: \"paid\"},\n    },\n}\ntotal, err := users.SumFromSpec(sumSpec).Exec(ctx, params)\navg, err := users.AvgFromSpec(sumSpec).Exec(ctx, params)\nmin, err := users.MinFromSpec(sumSpec).Exec(ctx, params)\nmax, err := users.MaxFromSpec(sumSpec).Exec(ctx, params)",{"id":722,"title":723,"titles":724,"content":725,"level":19},"/v1.0.8/guides/specs#compound-query-specs","Compound Query Specs",[207],"Combine queries with set operations: spec := soy.CompoundQuerySpec{\n    Base: soy.QuerySpec{\n        Where: []soy.ConditionSpec{\n            {Field: \"status\", Operator: \"=\", Param: \"active\"},\n        },\n    },\n    Operands: []soy.SetOperandSpec{\n        {\n            Operation: \"union\",\n            Query: soy.QuerySpec{\n                Where: []soy.ConditionSpec{\n                    {Field: \"status\", Operator: \"=\", Param: \"pending\"},\n                },\n            },\n        },\n    },\n    OrderBy: []soy.OrderBySpec{\n        {Field: \"name\", Direction: \"asc\"},\n    },\n    Limit: intPtr(100),\n}\n\ncompound := users.CompoundFromSpec(spec)\nresults, err := compound.Exec(ctx, params)",{"id":727,"title":728,"titles":729,"content":730,"level":19},"/v1.0.8/guides/specs#json-serialisation","JSON Serialisation",[207],"Specs marshal to JSON for storage or transmission: spec := soy.QuerySpec{\n    Fields: []string{\"id\", \"email\"},\n    Where: []soy.ConditionSpec{\n        {Field: \"status\", Operator: \"=\", Param: \"status\"},\n    },\n    OrderBy: []soy.OrderBySpec{\n        {Field: \"name\", Direction: \"asc\"},\n    },\n    Limit: intPtr(10),\n}\n\ndata, _ := json.Marshal(spec) {\n  \"fields\": [\"id\", \"email\"],\n  \"where\": [\n    {\"field\": \"status\", \"operator\": \"=\", \"param\": \"status\"}\n  ],\n  \"order_by\": [\n    {\"field\": \"name\", \"direction\": \"asc\"}\n  ],\n  \"limit\": 10\n}",{"id":732,"title":733,"titles":734,"content":735,"level":19},"/v1.0.8/guides/specs#api-example","API Example",[207],"Accept query specs from HTTP requests: func HandleSearch(w http.ResponseWriter, r *http.Request) {\n    var spec soy.QuerySpec\n    if err := json.NewDecoder(r.Body).Decode(&spec); err != nil {\n        http.Error(w, \"invalid spec\", http.StatusBadRequest)\n        return\n    }\n\n    // Validate and sanitise spec\n    if spec.Limit == nil || *spec.Limit > 100 {\n        limit := 100\n        spec.Limit = &limit\n    }\n\n    // Extract params from request (e.g., query string)\n    params := extractParams(r)\n\n    query := users.QueryFromSpec(spec)\n    results, err := query.Exec(r.Context(), params)\n    if err != nil {\n        http.Error(w, err.Error(), http.StatusInternalServerError)\n        return\n    }\n\n    json.NewEncoder(w).Encode(results)\n}",{"id":737,"title":738,"titles":739,"content":740,"level":19},"/v1.0.8/guides/specs#llm-integration","LLM Integration",[207],"Generate specs from natural language: // Example: User asks \"Find active users over 30, sorted by name\"\n// LLM generates:\nspecJSON := `{\n  \"where\": [\n    {\"field\": \"status\", \"operator\": \"=\", \"param\": \"status\"},\n    {\"field\": \"age\", \"operator\": \">\", \"param\": \"min_age\"}\n  ],\n  \"orderBy\": [{\"field\": \"name\", \"direction\": \"asc\"}]\n}`\n\nvar spec soy.QuerySpec\njson.Unmarshal([]byte(specJSON), &spec)\n\n// Safe execution with validated params\nparams := map[string]any{\n    \"status\":  \"active\",\n    \"min_age\": 30,\n}\n\nresults, err := users.QueryFromSpec(spec).Exec(ctx, params) The spec system ensures that: Only valid field names are used (schema validation)SQL injection is impossible (named parameters)Query structure is predictable (type-safe builders) html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}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 .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 .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}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)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}",{"id":742,"title":743,"titles":744,"content":745,"level":9},"/v1.0.8/guides/compound","Compound Queries",[],"UNION, INTERSECT, and EXCEPT for combining result sets",{"id":747,"title":743,"titles":748,"content":749,"level":9},"/v1.0.8/guides/compound#compound-queries",[],"Compound queries combine multiple SELECT statements using set operations. Soy supports all standard SQL set operations. See API Reference for complete method signatures.",{"id":751,"title":752,"titles":753,"content":754,"level":19},"/v1.0.8/guides/compound#set-operations","Set Operations",[743],"OperationDuplicatesDescriptionUnionRemovedRows from either queryUnionAllKeptAll rows from both queriesIntersectRemovedRows in both queriesIntersectAllKeptRows in both with duplicatesExceptRemovedRows in first but not secondExceptAllKeptSubtract with duplicates",{"id":756,"title":757,"titles":758,"content":759,"level":19},"/v1.0.8/guides/compound#union","UNION",[743],"Combine rows from multiple queries, removing duplicates: results, err := users.Query().\n    Where(\"status\", \"=\", \"active\").\n    Union(\n        users.Query().Where(\"role\", \"=\", \"admin\"),\n    ).\n    Exec(ctx, map[string]any{\n        \"active\": \"active\",\n        \"admin\":  \"admin\",\n    }) SELECT * FROM users WHERE status = :active\nUNION\nSELECT * FROM users WHERE role = :admin",{"id":761,"title":762,"titles":763,"content":764,"level":40},"/v1.0.8/guides/compound#union-all","UNION ALL",[743,757],"Keep duplicates: results, err := users.Query().\n    Where(\"department\", \"=\", \"engineering\").\n    UnionAll(\n        users.Query().Where(\"department\", \"=\", \"design\"),\n    ).\n    Exec(ctx, params)",{"id":766,"title":767,"titles":768,"content":769,"level":19},"/v1.0.8/guides/compound#intersect","INTERSECT",[743],"Rows that appear in both queries: // Users who are both active AND premium\nresults, err := users.Query().\n    Where(\"status\", \"=\", \"active\").\n    Intersect(\n        users.Query().Where(\"tier\", \"=\", \"premium\"),\n    ).\n    Exec(ctx, params)",{"id":771,"title":772,"titles":773,"content":774,"level":40},"/v1.0.8/guides/compound#intersect-all","INTERSECT ALL",[743,767],"Preserve duplicates in intersection: results, err := users.Query().\n    Where(\"region\", \"=\", \"us\").\n    IntersectAll(\n        users.Query().Where(\"verified\", \"=\", \"true\"),\n    ).\n    Exec(ctx, params)",{"id":776,"title":777,"titles":778,"content":779,"level":19},"/v1.0.8/guides/compound#except","EXCEPT",[743],"Rows in the first query but not the second: // Active users who are NOT admins\nresults, err := users.Query().\n    Where(\"status\", \"=\", \"active\").\n    Except(\n        users.Query().Where(\"role\", \"=\", \"admin\"),\n    ).\n    Exec(ctx, params)",{"id":781,"title":782,"titles":783,"content":784,"level":40},"/v1.0.8/guides/compound#except-all","EXCEPT ALL",[743,777],"Subtract with duplicate handling: results, err := users.Query().\n    Where(\"region\", \"=\", \"us\").\n    ExceptAll(\n        users.Query().Where(\"opted_out\", \"=\", \"true\"),\n    ).\n    Exec(ctx, params)",{"id":786,"title":787,"titles":788,"content":789,"level":19},"/v1.0.8/guides/compound#chaining-operations","Chaining Operations",[743],"Combine multiple set operations: results, err := users.Query().\n    Where(\"status\", \"=\", \"active\").\n    Union(\n        users.Query().Where(\"status\", \"=\", \"pending\"),\n    ).\n    Except(\n        users.Query().Where(\"banned\", \"=\", \"true\"),\n    ).\n    Exec(ctx, params) SELECT * FROM users WHERE status = :active\nUNION\nSELECT * FROM users WHERE status = :pending\nEXCEPT\nSELECT * FROM users WHERE banned = :true",{"id":791,"title":792,"titles":793,"content":794,"level":19},"/v1.0.8/guides/compound#order-by-and-limit","ORDER BY and LIMIT",[743],"Apply ordering and pagination to the final result: results, err := users.Query().\n    Where(\"region\", \"=\", \"us\").\n    Union(\n        users.Query().Where(\"region\", \"=\", \"eu\"),\n    ).\n    OrderBy(\"name\", \"asc\").\n    Limit(50).\n    Offset(0).\n    Exec(ctx, params) ORDER BY and LIMIT apply to the combined result, not individual queries.",{"id":796,"title":797,"titles":798,"content":799,"level":19},"/v1.0.8/guides/compound#from-specs","From Specs",[743],"Build compound queries from JSON specifications: spec := soy.CompoundQuerySpec{\n    Base: soy.QuerySpec{\n        Where: []soy.ConditionSpec{\n            {Field: \"status\", Operator: \"=\", Param: \"active\"},\n        },\n    },\n    Operands: []soy.SetOperandSpec{\n        {\n            Operation: \"union\",\n            Query: soy.QuerySpec{\n                Where: []soy.ConditionSpec{\n                    {Field: \"status\", Operator: \"=\", Param: \"pending\"},\n                },\n            },\n        },\n        {\n            Operation: \"except\",\n            Query: soy.QuerySpec{\n                Where: []soy.ConditionSpec{\n                    {Field: \"banned\", Operator: \"=\", Param: \"is_banned\"},\n                },\n            },\n        },\n    },\n    OrderBy: []soy.OrderBySpec{\n        {Field: \"created_at\", Direction: \"desc\"},\n    },\n    Limit: intPtr(100),\n}\n\ncompound := users.CompoundFromSpec(spec)\nresults, err := compound.Exec(ctx, map[string]any{\n    \"active\":    \"active\",\n    \"pending\":   \"pending\",\n    \"is_banned\": true,\n})",{"id":801,"title":802,"titles":803,"content":804,"level":19},"/v1.0.8/guides/compound#column-requirements","Column Requirements",[743],"All queries in a compound must have compatible columns. By default, all queries select all columns. Select specific fields for all queries: results, err := users.Query().\n    Fields(\"id\", \"email\", \"name\").\n    Where(\"status\", \"=\", \"active\").\n    Union(\n        users.Query().\n            Fields(\"id\", \"email\", \"name\").\n            Where(\"status\", \"=\", \"pending\"),\n    ).\n    Exec(ctx, params)",{"id":806,"title":212,"titles":807,"content":808,"level":19},"/v1.0.8/guides/compound#transactions",[743],"Execute compound queries within transactions: tx, err := db.BeginTxx(ctx, nil)\nif err != nil {\n    return err\n}\ndefer tx.Rollback()\n\nresults, err := users.Query().\n    Where(\"status\", \"=\", \"active\").\n    Union(\n        users.Query().Where(\"status\", \"=\", \"pending\"),\n    ).\n    ExecTx(ctx, tx, params)\nif err != nil {\n    return err\n}\n\n// Process results...\n\nreturn tx.Commit()",{"id":810,"title":537,"titles":811,"content":812,"level":19},"/v1.0.8/guides/compound#inspecting-sql",[743],"result, err := users.Query().\n    Where(\"region\", \"=\", \"us\").\n    Union(\n        users.Query().Where(\"region\", \"=\", \"eu\"),\n    ).\n    OrderBy(\"name\", \"asc\").\n    Limit(10).\n    Render()\n\nfmt.Println(result.SQL)",{"id":814,"title":431,"titles":815,"content":816,"level":19},"/v1.0.8/guides/compound#complete-example",[743],"// Find users eligible for a promotion:\n// - Active users with high spending OR\n// - Long-term users (>2 years) OR\n// - VIP tier users\n// EXCEPT users who already received promotion\n\nfunc GetEligibleUsers(ctx context.Context) ([]*User, error) {\n    cutoff := time.Now().AddDate(-2, 0, 0)\n\n    results, err := users.Query().\n        Where(\"status\", \"=\", \"status\").\n        Where(\"total_spent\", \">=\", \"min_spent\").\n        Union(\n            users.Query().\n                Where(\"created_at\", \"\u003C\", \"cutoff\"),\n        ).\n        Union(\n            users.Query().\n                Where(\"tier\", \"=\", \"tier\"),\n        ).\n        Except(\n            users.Query().\n                Where(\"received_promo\", \"=\", \"received\"),\n        ).\n        OrderBy(\"total_spent\", \"desc\").\n        Limit(1000).\n        Exec(ctx, map[string]any{\n            \"status\":    \"active\",\n            \"min_spent\": 500.00,\n            \"cutoff\":    cutoff,\n            \"tier\":      \"vip\",\n            \"received\":  true,\n        })\n\n    return results, err\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 .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 .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)}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 .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 .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}",{"id":818,"title":217,"titles":819,"content":820,"level":9},"/v1.0.8/guides/lifecycle",[],"Intercept records on scan and before write with OnScan and OnRecord",{"id":822,"title":217,"titles":823,"content":824,"level":9},"/v1.0.8/guides/lifecycle#lifecycle-callbacks",[],"Soy supports two optional callbacks on Soy[T] that fire during builder execution paths. They enable cross-cutting concerns — validation, normalisation, enrichment — without wrapping every builder type.",{"id":826,"title":827,"titles":828,"content":829,"level":19},"/v1.0.8/guides/lifecycle#onscan","OnScan",[217],"Fires after a row is scanned into *T. Registered with OnScan(): users.OnScan(func(ctx context.Context, result *User) error {\n    result.Email = strings.ToLower(result.Email)\n    return nil\n})",{"id":831,"title":832,"titles":833,"content":834,"level":40},"/v1.0.8/guides/lifecycle#where-it-fires","Where it fires",[217,827],"BuilderMethodFires?SelectExec, ExecTxOnce (single row)QueryExec, ExecTxPer rowCreateExec, ExecTxOnce (RETURNING row)UpdateExec, ExecTxOnce (RETURNING row)CompoundExec, ExecTxPer rowDeleteExec, ExecTxNo (int64 return)AggregateExec, ExecTxNo (float64 return)Create batchExecBatchNo (no scan)",{"id":836,"title":837,"titles":838,"content":839,"level":40},"/v1.0.8/guides/lifecycle#error-handling","Error handling",[217,827],"Returning an error aborts the operation: users.OnScan(func(ctx context.Context, result *User) error {\n    if result.Status == \"banned\" {\n        return errors.New(\"banned user\")\n    }\n    return nil\n})\n\nuser, err := users.Select().Where(\"id\", \"=\", \"id\").Exec(ctx, params)\n// err wraps \"banned user\" if the scanned record is banned For Query and Compound, an error on any row aborts the entire result.",{"id":841,"title":842,"titles":843,"content":844,"level":19},"/v1.0.8/guides/lifecycle#onrecord","OnRecord",[217],"Fires before a *T is written. Registered with OnRecord(): users.OnRecord(func(ctx context.Context, record *User) error {\n    if record.Email == \"\" {\n        return errors.New(\"email is required\")\n    }\n    record.Email = strings.ToLower(record.Email)\n    return nil\n})",{"id":846,"title":832,"titles":847,"content":848,"level":40},"/v1.0.8/guides/lifecycle#where-it-fires-1",[217,842],"BuilderMethodFires?CreateExec, ExecTxOnce (before INSERT)CreateExecBatchPer record (before INSERT)Create (upsert)Exec, ExecTxOnce (before write) OnRecord does not fire on Update or Delete because those operations take parameter maps, not *T records.",{"id":850,"title":837,"titles":851,"content":852,"level":40},"/v1.0.8/guides/lifecycle#error-handling-1",[217,842],"Returning an error aborts the write: users.OnRecord(func(ctx context.Context, record *User) error {\n    if record.Email == \"\" {\n        return errors.New(\"email is required\")\n    }\n    return nil\n})\n\n_, err := users.Insert().Exec(ctx, &User{Name: \"No Email\"})\n// err wraps \"email is required\" For batch inserts, an error on any record aborts the entire batch.",{"id":854,"title":855,"titles":856,"content":857,"level":19},"/v1.0.8/guides/lifecycle#combining-callbacks","Combining callbacks",[217],"Both callbacks can be registered simultaneously. On an Insert with RETURNING, the order is: OnRecord fires (before INSERT)INSERT executesOnScan fires (after RETURNING row is scanned) users.OnRecord(func(ctx context.Context, record *User) error {\n    record.Email = strings.ToLower(record.Email)\n    return nil\n})\n\nusers.OnScan(func(ctx context.Context, result *User) error {\n    result.FullName = result.FirstName + \" \" + result.LastName\n    return nil\n})",{"id":859,"title":860,"titles":861,"content":862,"level":19},"/v1.0.8/guides/lifecycle#unregistering","Unregistering",[217],"Pass nil to remove a callback: users.OnScan(nil)   // Removes the OnScan callback\nusers.OnRecord(nil) // Removes the OnRecord callback",{"id":864,"title":48,"titles":865,"content":866,"level":19},"/v1.0.8/guides/lifecycle#performance",[217],"Callbacks are nil-checked before invocation. When not registered, there is zero overhead — no function call, no allocation, no interface dispatch.",{"id":868,"title":869,"titles":870,"content":871,"level":19},"/v1.0.8/guides/lifecycle#use-with-grub","Use with grub",[217],"The primary motivation for lifecycle callbacks is integration with grub, which wraps soy with higher-level lifecycle hooks (BeforeSave, AfterLoad, etc.). By registering OnScan and OnRecord on the Soy[T] instance, grub's hooks fire automatically through all builder paths — including when users access builders directly via db.Query(), db.Select(), etc. 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 .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 .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 .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}",{"id":873,"title":187,"titles":874,"content":875,"level":9},"/v1.0.8/cookbook/pagination",[],"Offset-based and cursor-based pagination patterns",{"id":877,"title":187,"titles":878,"content":879,"level":9},"/v1.0.8/cookbook/pagination#pagination",[],"Recipe: Implement efficient pagination for large datasets. Soy supports both offset-based and cursor-based pagination. Choose based on your requirements.",{"id":881,"title":882,"titles":883,"content":884,"level":19},"/v1.0.8/cookbook/pagination#offset-based-pagination","Offset-Based Pagination",[187],"Simple to implement, works with any ordering: func GetUsersPage(ctx context.Context, page, perPage int) ([]*User, error) {\n    offset := (page - 1) * perPage\n\n    return users.Query().\n        OrderBy(\"created_at\", \"desc\").\n        Limit(perPage).\n        Offset(offset).\n        Exec(ctx, nil)\n}",{"id":886,"title":887,"titles":888,"content":889,"level":40},"/v1.0.8/cookbook/pagination#with-total-count","With Total Count",[187,882],"type PagedResult[T any] struct {\n    Items      []*T\n    Total      int\n    Page       int\n    PerPage    int\n    TotalPages int\n}\n\nfunc GetUsersPagedResult(ctx context.Context, page, perPage int) (*PagedResult[User], error) {\n    offset := (page - 1) * perPage\n\n    // Get total count\n    total, err := users.Count().Exec(ctx, nil)\n    if err != nil {\n        return nil, err\n    }\n\n    // Get page items\n    items, err := users.Query().\n        OrderBy(\"created_at\", \"desc\").\n        Limit(perPage).\n        Offset(offset).\n        Exec(ctx, nil)\n    if err != nil {\n        return nil, err\n    }\n\n    totalPages := int(math.Ceil(float64(total) / float64(perPage)))\n\n    return &PagedResult[User]{\n        Items:      items,\n        Total:      int(total),\n        Page:       page,\n        PerPage:    perPage,\n        TotalPages: totalPages,\n    }, nil\n}",{"id":891,"title":892,"titles":893,"content":894,"level":40},"/v1.0.8/cookbook/pagination#limitations","Limitations",[187,882],"Offset pagination has performance issues at scale: Database scans all rows up to offsetPage N requires scanning N * perPage rowsInconsistent results if data changes between pages",{"id":896,"title":897,"titles":898,"content":899,"level":19},"/v1.0.8/cookbook/pagination#cursor-based-pagination","Cursor-Based Pagination",[187],"More efficient for large datasets. Uses the last item's values to fetch the next page.",{"id":901,"title":902,"titles":903,"content":904,"level":40},"/v1.0.8/cookbook/pagination#by-id","By ID",[187,897],"func GetUsersAfter(ctx context.Context, afterID, limit int) ([]*User, error) {\n    if afterID == 0 {\n        // First page\n        return users.Query().\n            OrderBy(\"id\", \"asc\").\n            Limit(limit).\n            Exec(ctx, nil)\n    }\n\n    return users.Query().\n        Where(\"id\", \">\", \"after_id\").\n        OrderBy(\"id\", \"asc\").\n        Limit(limit).\n        Exec(ctx, map[string]any{\"after_id\": afterID})\n}",{"id":906,"title":907,"titles":908,"content":909,"level":40},"/v1.0.8/cookbook/pagination#by-timestamp","By Timestamp",[187,897],"func GetUsersBefore(ctx context.Context, beforeTime time.Time, limit int) ([]*User, error) {\n    if beforeTime.IsZero() {\n        // First page (newest first)\n        return users.Query().\n            OrderBy(\"created_at\", \"desc\").\n            Limit(limit).\n            Exec(ctx, nil)\n    }\n\n    return users.Query().\n        Where(\"created_at\", \"\u003C\", \"before_time\").\n        OrderBy(\"created_at\", \"desc\").\n        Limit(limit).\n        Exec(ctx, map[string]any{\"before_time\": beforeTime})\n}",{"id":911,"title":912,"titles":913,"content":914,"level":40},"/v1.0.8/cookbook/pagination#compound-cursor","Compound Cursor",[187,897],"For non-unique sort columns, use a compound cursor. This requires nested conditions which are best expressed using the spec API: type Cursor struct {\n    Score     int\n    ID        int\n}\n\nfunc GetUsersByScore(ctx context.Context, cursor *Cursor, limit int) ([]*User, error) {\n    if cursor == nil {\n        // First page\n        return users.Query().\n            OrderBy(\"score\", \"desc\").\n            OrderBy(\"id\", \"desc\").\n            Limit(limit).\n            Exec(ctx, nil)\n    }\n\n    // Next page: score \u003C cursor.score OR (score = cursor.score AND id \u003C cursor.id)\n    // Use spec API for nested AND/OR conditions\n    spec := soy.QuerySpec{\n        Where: []soy.ConditionSpec{\n            {\n                Logic: \"OR\",\n                Group: []soy.ConditionSpec{\n                    {Field: \"score\", Operator: \"\u003C\", Param: \"cursor_score\"},\n                    {\n                        Logic: \"AND\",\n                        Group: []soy.ConditionSpec{\n                            {Field: \"score\", Operator: \"=\", Param: \"cursor_score\"},\n                            {Field: \"id\", Operator: \"\u003C\", Param: \"cursor_id\"},\n                        },\n                    },\n                },\n            },\n        },\n        OrderBy: []soy.OrderBySpec{\n            {Field: \"score\", Direction: \"desc\"},\n            {Field: \"id\", Direction: \"desc\"},\n        },\n        Limit: &limit,\n    }\n\n    return users.QueryFromSpec(spec).Exec(ctx, map[string]any{\n        \"cursor_score\": cursor.Score,\n        \"cursor_id\":    cursor.ID,\n    })\n}",{"id":916,"title":917,"titles":918,"content":919,"level":40},"/v1.0.8/cookbook/pagination#opaque-cursors","Opaque Cursors",[187,897],"Encode cursor data for API responses: import \"encoding/base64\"\n\ntype CursorData struct {\n    ID        int       `json:\"id\"`\n    CreatedAt time.Time `json:\"created_at\"`\n}\n\nfunc EncodeCursor(user *User) string {\n    data := CursorData{ID: user.ID, CreatedAt: user.CreatedAt}\n    bytes, _ := json.Marshal(data)\n    return base64.StdEncoding.EncodeToString(bytes)\n}\n\nfunc DecodeCursor(cursor string) (*CursorData, error) {\n    bytes, err := base64.StdEncoding.DecodeString(cursor)\n    if err != nil {\n        return nil, err\n    }\n    var data CursorData\n    if err := json.Unmarshal(bytes, &data); err != nil {\n        return nil, err\n    }\n    return &data, nil\n}",{"id":921,"title":922,"titles":923,"content":924,"level":19},"/v1.0.8/cookbook/pagination#paginated-response","Paginated Response",[187],"Standard response format: type Connection[T any] struct {\n    Edges    []Edge[T] `json:\"edges\"`\n    PageInfo PageInfo  `json:\"pageInfo\"`\n}\n\ntype Edge[T any] struct {\n    Node   *T     `json:\"node\"`\n    Cursor string `json:\"cursor\"`\n}\n\ntype PageInfo struct {\n    HasNextPage     bool   `json:\"hasNextPage\"`\n    HasPreviousPage bool   `json:\"hasPreviousPage\"`\n    StartCursor     string `json:\"startCursor,omitempty\"`\n    EndCursor       string `json:\"endCursor,omitempty\"`\n}\n\nfunc GetUsersConnection(ctx context.Context, after string, first int) (*Connection[User], error) {\n    // Fetch one extra to determine hasNextPage\n    limit := first + 1\n\n    var items []*User\n    var err error\n\n    if after == \"\" {\n        items, err = users.Query().\n            OrderBy(\"id\", \"asc\").\n            Limit(limit).\n            Exec(ctx, nil)\n    } else {\n        cursor, err := DecodeCursor(after)\n        if err != nil {\n            return nil, err\n        }\n        items, err = users.Query().\n            Where(\"id\", \">\", \"after_id\").\n            OrderBy(\"id\", \"asc\").\n            Limit(limit).\n            Exec(ctx, map[string]any{\"after_id\": cursor.ID})\n    }\n    if err != nil {\n        return nil, err\n    }\n\n    hasNextPage := len(items) > first\n    if hasNextPage {\n        items = items[:first] // Remove extra item\n    }\n\n    edges := make([]Edge[User], len(items))\n    for i, item := range items {\n        edges[i] = Edge[User]{\n            Node:   item,\n            Cursor: EncodeCursor(item),\n        }\n    }\n\n    var startCursor, endCursor string\n    if len(edges) > 0 {\n        startCursor = edges[0].Cursor\n        endCursor = edges[len(edges)-1].Cursor\n    }\n\n    return &Connection[User]{\n        Edges: edges,\n        PageInfo: PageInfo{\n            HasNextPage:     hasNextPage,\n            HasPreviousPage: after != \"\",\n            StartCursor:     startCursor,\n            EndCursor:       endCursor,\n        },\n    }, nil\n}",{"id":926,"title":927,"titles":928,"content":929,"level":19},"/v1.0.8/cookbook/pagination#choosing-a-strategy","Choosing a Strategy",[187],"FactorOffsetCursorImplementationSimpleMore complexPerformance at scaleDegradesConsistentJump to page NYesNoConsistent with changesNoYesArbitrary orderingYesRequires indexed column Use offset pagination for: Small datasets (\u003C 10,000 rows)Admin interfaces needing \"jump to page\"Simple implementations Use cursor pagination for: Large datasetsReal-time feedsAPI endpoints with high trafficData that changes frequently html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}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 .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 .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 .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 .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}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)}",{"id":931,"title":932,"titles":933,"content":934,"level":9},"/v1.0.8/cookbook/pgvector","pgvector",[],"Semantic search and similarity queries with pgvector",{"id":936,"title":932,"titles":937,"content":938,"level":9},"/v1.0.8/cookbook/pgvector#pgvector",[],"Recipe: Implement semantic search using PostgreSQL vector similarity. Soy supports pgvector operators for embedding-based similarity search.",{"id":940,"title":941,"titles":942,"content":943,"level":19},"/v1.0.8/cookbook/pgvector#setup","Setup",[932],"Install the pgvector extension: CREATE EXTENSION IF NOT EXISTS vector; Define a model with a vector column: type Document struct {\n    ID        int       `db:\"id\" type:\"serial\" constraints:\"primary key\"`\n    Title     string    `db:\"title\" type:\"text\"`\n    Content   string    `db:\"content\" type:\"text\"`\n    Embedding []float32 `db:\"embedding\" type:\"vector(1536)\"` // OpenAI dimensions\n} Create the soy instance: documents, err := soy.New[Document](db, \"documents\")",{"id":945,"title":946,"titles":947,"content":948,"level":19},"/v1.0.8/cookbook/pgvector#distance-operators","Distance Operators",[932],"pgvector provides four distance measures: OperatorNameUse Case\u003C->L2 (Euclidean)General similarity\u003C#>Inner productNormalized vectors\u003C=>CosineText embeddings\u003C+>L1 (Manhattan)Sparse vectors",{"id":950,"title":951,"titles":952,"content":953,"level":19},"/v1.0.8/cookbook/pgvector#basic-similarity-search","Basic Similarity Search",[932],"Find documents similar to a query embedding: func SearchDocuments(ctx context.Context, queryEmbedding []float32, limit int) ([]*Document, error) {\n    return documents.Query().\n        OrderByExpr(\"embedding\", \"\u003C=>\", \"query_vec\", \"asc\").\n        Limit(limit).\n        Exec(ctx, map[string]any{\n            \"query_vec\": queryEmbedding,\n        })\n} The OrderByExpr method generates: SELECT * FROM documents\nORDER BY embedding \u003C=> :query_vec ASC\nLIMIT :limit",{"id":955,"title":956,"titles":957,"content":958,"level":19},"/v1.0.8/cookbook/pgvector#retrieving-distance-scores","Retrieving Distance Scores",[932],"Use SelectExpr to include computed distance values in results: func SearchWithScores(ctx context.Context, queryEmbedding []float32, limit int) ([]*Document, error) {\n    return documents.Query().\n        SelectExpr(\"embedding\", \"\u003C=>\", \"query_vec\", \"score\").\n        OrderByExpr(\"embedding\", \"\u003C=>\", \"query_vec\", \"asc\").\n        Limit(limit).\n        Exec(ctx, map[string]any{\n            \"query_vec\": queryEmbedding,\n        })\n} This generates: SELECT *, \"embedding\" \u003C=> :query_vec AS \"score\"\nFROM documents\nORDER BY \"embedding\" \u003C=> :query_vec ASC\nLIMIT :limit Note: The score column is added to the result set. To access it, you can either: Add a Score field to your struct with the db:\"score\" tagUse ExecAtom to get results as a map",{"id":960,"title":961,"titles":962,"content":963,"level":19},"/v1.0.8/cookbook/pgvector#filtered-search","Filtered Search",[932],"Combine similarity with conditions: func SearchByCategory(ctx context.Context, embedding []float32, category string, limit int) ([]*Document, error) {\n    return documents.Query().\n        Where(\"category\", \"=\", \"category\").\n        Where(\"published\", \"=\", \"is_published\").\n        OrderByExpr(\"embedding\", \"\u003C=>\", \"query_vec\", \"asc\").\n        Limit(limit).\n        Exec(ctx, map[string]any{\n            \"query_vec\":    embedding,\n            \"category\":     category,\n            \"is_published\": true,\n        })\n}",{"id":965,"title":966,"titles":967,"content":968,"level":19},"/v1.0.8/cookbook/pgvector#distance-threshold","Distance Threshold",[932],"Filter by maximum distance: func SearchWithinDistance(ctx context.Context, embedding []float32, maxDistance float64) ([]*Document, error) {\n    return documents.Query().\n        Where(\"embedding\", \"\u003C=>\", \"query_vec\").  // This adds distance to WHERE\n        OrderByExpr(\"embedding\", \"\u003C=>\", \"query_vec\", \"asc\").\n        Limit(100).\n        Exec(ctx, map[string]any{\n            \"query_vec\":    embedding,\n            \"max_distance\": maxDistance,\n        })\n} Note: For distance thresholds, you may need a raw WHERE clause or post-filtering.",{"id":970,"title":971,"titles":972,"content":973,"level":19},"/v1.0.8/cookbook/pgvector#with-pagination","With Pagination",[932],"Paginate similarity results: type SearchResult struct {\n    Documents []*Document\n    HasMore   bool\n}\n\nfunc SearchPaginated(ctx context.Context, embedding []float32, page, perPage int) (*SearchResult, error) {\n    offset := (page - 1) * perPage\n    limit := perPage + 1 // Fetch one extra\n\n    results, err := documents.Query().\n        OrderByExpr(\"embedding\", \"\u003C=>\", \"query_vec\", \"asc\").\n        Limit(limit).\n        Offset(offset).\n        Exec(ctx, map[string]any{\"query_vec\": embedding})\n    if err != nil {\n        return nil, err\n    }\n\n    hasMore := len(results) > perPage\n    if hasMore {\n        results = results[:perPage]\n    }\n\n    return &SearchResult{\n        Documents: results,\n        HasMore:   hasMore,\n    }, nil\n}",{"id":975,"title":976,"titles":977,"content":978,"level":19},"/v1.0.8/cookbook/pgvector#hybrid-search","Hybrid Search",[932],"Combine keyword and semantic search: func HybridSearch(ctx context.Context, query string, embedding []float32) ([]*Document, error) {\n    // Semantic results\n    semanticResults, err := documents.Query().\n        OrderByExpr(\"embedding\", \"\u003C=>\", \"query_vec\", \"asc\").\n        Limit(50).\n        Exec(ctx, map[string]any{\"query_vec\": embedding})\n    if err != nil {\n        return nil, err\n    }\n\n    // Keyword results\n    keywordResults, err := documents.Query().\n        Where(\"content\", \"ILIKE\", \"search_pattern\").\n        Limit(50).\n        Exec(ctx, map[string]any{\n            \"search_pattern\": \"%\" + query + \"%\",\n        })\n    if err != nil {\n        return nil, err\n    }\n\n    // Merge and deduplicate (implement based on your needs)\n    return mergeResults(semanticResults, keywordResults), nil\n}",{"id":980,"title":981,"titles":982,"content":983,"level":19},"/v1.0.8/cookbook/pgvector#from-spec","From Spec",[932],"Build vector queries from specs: spec := soy.QuerySpec{\n    Fields: []string{\"id\", \"title\", \"content\"},\n    Where: []soy.ConditionSpec{\n        {Field: \"category\", Operator: \"=\", Param: \"category\"},\n    },\n    OrderBy: []soy.OrderBySpec{\n        {Expression: \"embedding \u003C=> :query_vec\", Direction: \"asc\"},\n    },\n    Limit: intPtr(10),\n}\n\nresults, err := documents.QueryFromSpec(spec).Exec(ctx, map[string]any{\n    \"category\":  \"technology\",\n    \"query_vec\": embedding,\n})",{"id":985,"title":986,"titles":987,"content":988,"level":19},"/v1.0.8/cookbook/pgvector#indexing","Indexing",[932],"Create an index for efficient similarity search: -- HNSW index (recommended for most cases)\nCREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);\n\n-- IVFFlat index (faster builds, slightly lower recall)\nCREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100); Choose the index based on distance operator: OperatorIndex Ops Class\u003C->vector_l2_ops\u003C#>vector_ip_ops\u003C=>vector_cosine_ops",{"id":990,"title":991,"titles":992,"content":993,"level":19},"/v1.0.8/cookbook/pgvector#embedding-generation","Embedding Generation",[932],"Generate embeddings before insert: func CreateDocument(ctx context.Context, title, content string) (*Document, error) {\n    // Generate embedding (example with OpenAI)\n    embedding, err := generateEmbedding(content)\n    if err != nil {\n        return nil, err\n    }\n\n    doc := &Document{\n        Title:     title,\n        Content:   content,\n        Embedding: embedding,\n    }\n\n    return documents.Insert().Build().Exec(ctx, doc)\n}\n\nfunc generateEmbedding(text string) ([]float32, error) {\n    // Call your embedding API (OpenAI, Cohere, etc.)\n    // Returns []float32 with appropriate dimensions\n}",{"id":995,"title":431,"titles":996,"content":997,"level":19},"/v1.0.8/cookbook/pgvector#complete-example",[932],"package search\n\nimport (\n    \"context\"\n\n    \"github.com/jmoiron/sqlx\"\n    \"github.com/zoobz-io/soy\"\n)\n\ntype Document struct {\n    ID        int       `db:\"id\" type:\"serial\" constraints:\"primary key\"`\n    Title     string    `db:\"title\" type:\"text\" constraints:\"not null\"`\n    Content   string    `db:\"content\" type:\"text\"`\n    Category  string    `db:\"category\" type:\"text\"`\n    Embedding []float32 `db:\"embedding\" type:\"vector(1536)\"`\n}\n\ntype SearchService struct {\n    documents *soy.Soy[Document]\n    embedder  Embedder\n}\n\ntype Embedder interface {\n    Embed(text string) ([]float32, error)\n}\n\nfunc NewSearchService(db *sqlx.DB, embedder Embedder) (*SearchService, error) {\n    docs, err := soy.New[Document](db, \"documents\")\n    if err != nil {\n        return nil, err\n    }\n    return &SearchService{documents: docs, embedder: embedder}, nil\n}\n\nfunc (s *SearchService) Search(ctx context.Context, query string, opts SearchOptions) ([]*Document, error) {\n    embedding, err := s.embedder.Embed(query)\n    if err != nil {\n        return nil, err\n    }\n\n    q := s.documents.Query()\n\n    if opts.Category != \"\" {\n        q = q.Where(\"category\", \"=\", \"category\")\n    }\n\n    // Use SelectExpr to include distance score in results\n    if opts.IncludeScore {\n        q = q.SelectExpr(\"embedding\", \"\u003C=>\", \"query_vec\", \"score\")\n    }\n\n    return q.\n        OrderByExpr(\"embedding\", \"\u003C=>\", \"query_vec\", \"asc\").\n        Limit(opts.Limit).\n        Exec(ctx, map[string]any{\n            \"query_vec\": embedding,\n            \"category\":  opts.Category,\n        })\n}\n\ntype SearchOptions struct {\n    Category     string\n    Limit        int\n    IncludeScore bool\n} 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 .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 .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 .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}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 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 .skxcq, html code.shiki .skxcq{--shiki-default:var(--shiki-builtin)}",{"id":999,"title":1000,"titles":1001,"content":1002,"level":9},"/v1.0.8/reference/api","API Reference",[],"Complete API reference for the soy package",{"id":1004,"title":1000,"titles":1005,"content":1006,"level":9},"/v1.0.8/reference/api#api-reference",[],"Complete API reference for the github.com/zoobz-io/soy package.",{"id":1008,"title":1009,"titles":1010,"content":1011,"level":19},"/v1.0.8/reference/api#soy","Soy",[1000],"The main coordinator type. Create one per table.",{"id":1013,"title":1014,"titles":1015,"content":1016,"level":40},"/v1.0.8/reference/api#new","New",[1000,1009],"func New[T any](db *sqlx.DB, tableName string, renderer astql.Renderer) (*Soy[T], error) Creates a new soy instance for the given struct type and table name with the specified SQL renderer. Inspects struct tags, builds schema, and caches metadata. Available renderers from github.com/zoobz-io/astql/pkg: postgres.New() - PostgreSQLmariadb.New() - MariaDBsqlite.New() - SQLitemssql.New() - Microsoft SQL Server",{"id":1018,"title":1019,"titles":1020,"content":34,"level":40},"/v1.0.8/reference/api#methods","Methods",[1000,1009],{"id":1022,"title":1023,"titles":1024,"content":1025,"level":1026},"/v1.0.8/reference/api#select","Select",[1000,1009,1019],"func (c *Soy[T]) Select() *Select[T] Returns a builder for single-record SELECT queries.",4,{"id":1028,"title":1029,"titles":1030,"content":1031,"level":1026},"/v1.0.8/reference/api#query","Query",[1000,1009,1019],"func (c *Soy[T]) Query() *Query[T] Returns a builder for multi-record SELECT queries.",{"id":1033,"title":1034,"titles":1035,"content":1036,"level":1026},"/v1.0.8/reference/api#insert","Insert",[1000,1009,1019],"func (c *Soy[T]) Insert() *Create[T] Returns a builder for INSERT operations.",{"id":1038,"title":1039,"titles":1040,"content":1041,"level":1026},"/v1.0.8/reference/api#modify","Modify",[1000,1009,1019],"func (c *Soy[T]) Modify() *Update[T] Returns a builder for UPDATE operations.",{"id":1043,"title":1044,"titles":1045,"content":1046,"level":1026},"/v1.0.8/reference/api#remove","Remove",[1000,1009,1019],"func (c *Soy[T]) Remove() *Delete[T] Returns a builder for DELETE operations.",{"id":1048,"title":1049,"titles":1050,"content":1051,"level":1026},"/v1.0.8/reference/api#count","Count",[1000,1009,1019],"func (c *Soy[T]) Count() *Aggregate[T] Returns a builder for COUNT(*) aggregates.",{"id":1053,"title":1054,"titles":1055,"content":1056,"level":1026},"/v1.0.8/reference/api#sum","Sum",[1000,1009,1019],"func (c *Soy[T]) Sum(field string) *Aggregate[T] Returns a builder for SUM aggregates on the specified field.",{"id":1058,"title":1059,"titles":1060,"content":1061,"level":1026},"/v1.0.8/reference/api#avg","Avg",[1000,1009,1019],"func (c *Soy[T]) Avg(field string) *Aggregate[T] Returns a builder for AVG aggregates on the specified field.",{"id":1063,"title":1064,"titles":1065,"content":1066,"level":1026},"/v1.0.8/reference/api#min","Min",[1000,1009,1019],"func (c *Soy[T]) Min(field string) *Aggregate[T] Returns a builder for MIN aggregates on the specified field.",{"id":1068,"title":1069,"titles":1070,"content":1071,"level":1026},"/v1.0.8/reference/api#max","Max",[1000,1009,1019],"func (c *Soy[T]) Max(field string) *Aggregate[T] Returns a builder for MAX aggregates on the specified field.",{"id":1073,"title":217,"titles":1074,"content":34,"level":40},"/v1.0.8/reference/api#lifecycle-callbacks",[1000,1009],{"id":1076,"title":827,"titles":1077,"content":1078,"level":1026},"/v1.0.8/reference/api#onscan",[1000,1009,217],"func (c *Soy[T]) OnScan(fn func(ctx context.Context, result *T) error) Registers a callback that fires after scanning a row into *T. Called in Select, Query, Update, Create, and Compound execution paths. Pass nil to unregister.",{"id":1080,"title":842,"titles":1081,"content":1082,"level":1026},"/v1.0.8/reference/api#onrecord",[1000,1009,217],"func (c *Soy[T]) OnRecord(fn func(ctx context.Context, record *T) error) Registers a callback that fires before writing a *T. Called in Create execution paths (single insert, batch insert, upsert) before the INSERT is executed. Pass nil to unregister.",{"id":1084,"title":1085,"titles":1086,"content":34,"level":40},"/v1.0.8/reference/api#spec-methods","Spec Methods",[1000,1009],{"id":1088,"title":1089,"titles":1090,"content":1091,"level":1026},"/v1.0.8/reference/api#queryfromspec","QueryFromSpec",[1000,1009,1085],"func (c *Soy[T]) QueryFromSpec(spec QuerySpec) *Query[T] Creates a Query builder from a JSON-serializable spec.",{"id":1093,"title":1094,"titles":1095,"content":1096,"level":1026},"/v1.0.8/reference/api#selectfromspec","SelectFromSpec",[1000,1009,1085],"func (c *Soy[T]) SelectFromSpec(spec SelectSpec) *Select[T] Creates a Select builder from a JSON-serializable spec.",{"id":1098,"title":1099,"titles":1100,"content":1101,"level":1026},"/v1.0.8/reference/api#insertfromspec","InsertFromSpec",[1000,1009,1085],"func (c *Soy[T]) InsertFromSpec(spec CreateSpec) *Create[T] Creates a Create builder from a JSON-serializable spec.",{"id":1103,"title":1104,"titles":1105,"content":1106,"level":1026},"/v1.0.8/reference/api#modifyfromspec","ModifyFromSpec",[1000,1009,1085],"func (c *Soy[T]) ModifyFromSpec(spec UpdateSpec) *Update[T] Creates an Update builder from a JSON-serializable spec.",{"id":1108,"title":1109,"titles":1110,"content":1111,"level":1026},"/v1.0.8/reference/api#removefromspec","RemoveFromSpec",[1000,1009,1085],"func (c *Soy[T]) RemoveFromSpec(spec DeleteSpec) *Delete[T] Creates a Delete builder from a JSON-serializable spec.",{"id":1113,"title":1114,"titles":1115,"content":1116,"level":1026},"/v1.0.8/reference/api#countfromspec","CountFromSpec",[1000,1009,1085],"func (c *Soy[T]) CountFromSpec(spec AggregateSpec) *Aggregate[T] Creates a COUNT aggregate from a spec.",{"id":1118,"title":1119,"titles":1120,"content":1121,"level":1026},"/v1.0.8/reference/api#sumfromspec","SumFromSpec",[1000,1009,1085],"func (c *Soy[T]) SumFromSpec(spec AggregateSpec) *Aggregate[T] Creates a SUM aggregate from a spec. Uses spec.Field as the column to sum.",{"id":1123,"title":1124,"titles":1125,"content":1126,"level":1026},"/v1.0.8/reference/api#avgfromspec","AvgFromSpec",[1000,1009,1085],"func (c *Soy[T]) AvgFromSpec(spec AggregateSpec) *Aggregate[T] Creates an AVG aggregate from a spec. Uses spec.Field as the column to average.",{"id":1128,"title":1129,"titles":1130,"content":1131,"level":1026},"/v1.0.8/reference/api#minfromspec","MinFromSpec",[1000,1009,1085],"func (c *Soy[T]) MinFromSpec(spec AggregateSpec) *Aggregate[T] Creates a MIN aggregate from a spec. Uses spec.Field as the column.",{"id":1133,"title":1134,"titles":1135,"content":1136,"level":1026},"/v1.0.8/reference/api#maxfromspec","MaxFromSpec",[1000,1009,1085],"func (c *Soy[T]) MaxFromSpec(spec AggregateSpec) *Aggregate[T] Creates a MAX aggregate from a spec. Uses spec.Field as the column.",{"id":1138,"title":1139,"titles":1140,"content":1141,"level":1026},"/v1.0.8/reference/api#compoundfromspec","CompoundFromSpec",[1000,1009,1085],"func (c *Soy[T]) CompoundFromSpec(spec CompoundQuerySpec) *Compound[T] Creates a Compound builder from a JSON-serializable spec.",{"id":1143,"title":1144,"titles":1145,"content":1146,"level":19},"/v1.0.8/reference/api#selectt","SelectT",[1000],"Builder for single-record SELECT queries.",{"id":1148,"title":1019,"titles":1149,"content":34,"level":40},"/v1.0.8/reference/api#methods-1",[1000,1144],{"id":1151,"title":1152,"titles":1153,"content":1154,"level":1026},"/v1.0.8/reference/api#fields","Fields",[1000,1144,1019],"func (s *Select[T]) Fields(fields ...string) *Select[T] Selects specific columns. Returns all columns if not called.",{"id":1156,"title":1157,"titles":1158,"content":1159,"level":1026},"/v1.0.8/reference/api#where","Where",[1000,1144,1019],"func (s *Select[T]) Where(field, operator, param string) *Select[T] Adds a WHERE condition. Multiple calls produce AND.",{"id":1161,"title":1162,"titles":1163,"content":1164,"level":1026},"/v1.0.8/reference/api#whereand","WhereAnd",[1000,1144,1019],"func (s *Select[T]) WhereAnd(conditions ...Condition) *Select[T] Adds grouped AND conditions.",{"id":1166,"title":1167,"titles":1168,"content":1169,"level":1026},"/v1.0.8/reference/api#whereor","WhereOr",[1000,1144,1019],"func (s *Select[T]) WhereOr(conditions ...Condition) *Select[T] Adds grouped OR conditions.",{"id":1171,"title":1172,"titles":1173,"content":1174,"level":1026},"/v1.0.8/reference/api#wherenull","WhereNull",[1000,1144,1019],"func (s *Select[T]) WhereNull(field string) *Select[T] Adds IS NULL condition.",{"id":1176,"title":1177,"titles":1178,"content":1179,"level":1026},"/v1.0.8/reference/api#wherenotnull","WhereNotNull",[1000,1144,1019],"func (s *Select[T]) WhereNotNull(field string) *Select[T] Adds IS NOT NULL condition.",{"id":1181,"title":1182,"titles":1183,"content":1184,"level":1026},"/v1.0.8/reference/api#wherebetween","WhereBetween",[1000,1144,1019],"func (s *Select[T]) WhereBetween(field, lowParam, highParam string) *Select[T] Adds a WHERE field BETWEEN low AND high condition.",{"id":1186,"title":1187,"titles":1188,"content":1189,"level":1026},"/v1.0.8/reference/api#wherenotbetween","WhereNotBetween",[1000,1144,1019],"func (s *Select[T]) WhereNotBetween(field, lowParam, highParam string) *Select[T] Adds a WHERE field NOT BETWEEN low AND high condition.",{"id":1191,"title":1192,"titles":1193,"content":1194,"level":1026},"/v1.0.8/reference/api#wherefields","WhereFields",[1000,1144,1019],"func (s *Select[T]) WhereFields(leftField, operator, rightField string) *Select[T] Adds a WHERE condition comparing two fields (e.g., WHERE \"created_at\" \u003C \"updated_at\").",{"id":1196,"title":1197,"titles":1198,"content":1199,"level":1026},"/v1.0.8/reference/api#orderby","OrderBy",[1000,1144,1019],"func (s *Select[T]) OrderBy(field, direction string) *Select[T] Adds ORDER BY clause. Direction: \"asc\" or \"desc\".",{"id":1201,"title":1202,"titles":1203,"content":1204,"level":1026},"/v1.0.8/reference/api#orderbynulls","OrderByNulls",[1000,1144,1019],"func (s *Select[T]) OrderByNulls(field, direction, nulls string) *Select[T] Adds ORDER BY with NULLS FIRST or NULLS LAST. The nulls parameter must be \"first\" or \"last\" (case insensitive).",{"id":1206,"title":1207,"titles":1208,"content":1209,"level":1026},"/v1.0.8/reference/api#orderbyexpr","OrderByExpr",[1000,1144,1019],"func (s *Select[T]) OrderByExpr(field, operator, param, direction string) *Select[T] Adds ORDER BY with expression (e.g., for pgvector).",{"id":1211,"title":1212,"titles":1213,"content":1214,"level":1026},"/v1.0.8/reference/api#distinct","Distinct",[1000,1144,1019],"func (s *Select[T]) Distinct() *Select[T] Adds DISTINCT clause.",{"id":1216,"title":1217,"titles":1218,"content":1219,"level":1026},"/v1.0.8/reference/api#distincton","DistinctOn",[1000,1144,1019],"func (s *Select[T]) DistinctOn(fields ...string) *Select[T] Adds DISTINCT ON clause (PostgreSQL).",{"id":1221,"title":1222,"titles":1223,"content":1224,"level":1026},"/v1.0.8/reference/api#groupby","GroupBy",[1000,1144,1019],"func (s *Select[T]) GroupBy(fields ...string) *Select[T] Adds GROUP BY clause.",{"id":1226,"title":1227,"titles":1228,"content":1229,"level":1026},"/v1.0.8/reference/api#having","Having",[1000,1144,1019],"func (s *Select[T]) Having(field, operator, param string) *Select[T] Adds HAVING condition.",{"id":1231,"title":1232,"titles":1233,"content":1234,"level":1026},"/v1.0.8/reference/api#havingagg","HavingAgg",[1000,1144,1019],"func (s *Select[T]) HavingAgg(function, field, operator, param string) *Select[T] Adds HAVING with aggregate function.",{"id":1236,"title":1237,"titles":1238,"content":1239,"level":1026},"/v1.0.8/reference/api#forupdate","ForUpdate",[1000,1144,1019],"func (s *Select[T]) ForUpdate() *Select[T] Adds FOR UPDATE lock.",{"id":1241,"title":1242,"titles":1243,"content":1244,"level":1026},"/v1.0.8/reference/api#fornokeyupdate","ForNoKeyUpdate",[1000,1144,1019],"func (s *Select[T]) ForNoKeyUpdate() *Select[T] Adds FOR NO KEY UPDATE lock.",{"id":1246,"title":1247,"titles":1248,"content":1249,"level":1026},"/v1.0.8/reference/api#forshare","ForShare",[1000,1144,1019],"func (s *Select[T]) ForShare() *Select[T] Adds FOR SHARE lock.",{"id":1251,"title":1252,"titles":1253,"content":1254,"level":1026},"/v1.0.8/reference/api#forkeyshare","ForKeyShare",[1000,1144,1019],"func (s *Select[T]) ForKeyShare() *Select[T] Adds FOR KEY SHARE lock.",{"id":1256,"title":1257,"titles":1258,"content":1259,"level":1026},"/v1.0.8/reference/api#exec","Exec",[1000,1144,1019],"func (s *Select[T]) Exec(ctx context.Context, params map[string]any) (*T, error) Executes the query and returns a single result. Returns an error if no rows found.",{"id":1261,"title":1262,"titles":1263,"content":1264,"level":1026},"/v1.0.8/reference/api#exectx","ExecTx",[1000,1144,1019],"func (s *Select[T]) ExecTx(ctx context.Context, tx *sqlx.Tx, params map[string]any) (*T, error) Executes within a transaction.",{"id":1266,"title":1267,"titles":1268,"content":1269,"level":1026},"/v1.0.8/reference/api#execatom","ExecAtom",[1000,1144,1019],"func (s *Select[T]) ExecAtom(ctx context.Context, params map[string]any) (*atom.Atom, error) Executes the query and scans the result directly into an *atom.Atom. Returns an error if no rows found.",{"id":1271,"title":1272,"titles":1273,"content":1274,"level":1026},"/v1.0.8/reference/api#exectxatom","ExecTxAtom",[1000,1144,1019],"func (s *Select[T]) ExecTxAtom(ctx context.Context, tx *sqlx.Tx, params map[string]any) (*atom.Atom, error) Executes within a transaction and returns an *atom.Atom.",{"id":1276,"title":1277,"titles":1278,"content":1279,"level":1026},"/v1.0.8/reference/api#render","Render",[1000,1144,1019],"func (s *Select[T]) Render() (*astql.QueryResult, error) Returns the generated SQL as a QueryResult without executing. The QueryResult contains the SQL string and parameter information.",{"id":1281,"title":1282,"titles":1283,"content":1284,"level":1026},"/v1.0.8/reference/api#mustrender","MustRender",[1000,1144,1019],"func (s *Select[T]) MustRender() *astql.QueryResult Like Render but panics on error.",{"id":1286,"title":1287,"titles":1288,"content":1289,"level":19},"/v1.0.8/reference/api#queryt","QueryT",[1000],"Builder for multi-record SELECT queries. Inherits all Select methods plus:",{"id":1291,"title":1292,"titles":1293,"content":34,"level":40},"/v1.0.8/reference/api#additional-methods","Additional Methods",[1000,1287],{"id":1295,"title":1296,"titles":1297,"content":1298,"level":1026},"/v1.0.8/reference/api#limit","Limit",[1000,1287,1292],"func (q *Query[T]) Limit(n int) *Query[T] Sets the maximum number of rows to return (static value).",{"id":1300,"title":1301,"titles":1302,"content":1303,"level":1026},"/v1.0.8/reference/api#limitparam","LimitParam",[1000,1287,1292],"func (q *Query[T]) LimitParam(param string) *Query[T] Sets the LIMIT clause to a parameterized value. Useful for API pagination where limit comes from request parameters.",{"id":1305,"title":1306,"titles":1307,"content":1308,"level":1026},"/v1.0.8/reference/api#offset","Offset",[1000,1287,1292],"func (q *Query[T]) Offset(n int) *Query[T] Sets the number of rows to skip (static value).",{"id":1310,"title":1311,"titles":1312,"content":1313,"level":1026},"/v1.0.8/reference/api#offsetparam","OffsetParam",[1000,1287,1292],"func (q *Query[T]) OffsetParam(param string) *Query[T] Sets the OFFSET clause to a parameterized value. Useful for API pagination where offset comes from request parameters.",{"id":1315,"title":1316,"titles":1317,"content":1318,"level":1026},"/v1.0.8/reference/api#union","Union",[1000,1287,1292],"func (q *Query[T]) Union(other *Query[T]) *Compound[T] Combines with another query using UNION.",{"id":1320,"title":1321,"titles":1322,"content":1323,"level":1026},"/v1.0.8/reference/api#unionall","UnionAll",[1000,1287,1292],"func (q *Query[T]) UnionAll(other *Query[T]) *Compound[T] Combines using UNION ALL.",{"id":1325,"title":1326,"titles":1327,"content":1328,"level":1026},"/v1.0.8/reference/api#intersect","Intersect",[1000,1287,1292],"func (q *Query[T]) Intersect(other *Query[T]) *Compound[T] Combines using INTERSECT.",{"id":1330,"title":1331,"titles":1332,"content":1333,"level":1026},"/v1.0.8/reference/api#intersectall","IntersectAll",[1000,1287,1292],"func (q *Query[T]) IntersectAll(other *Query[T]) *Compound[T] Combines using INTERSECT ALL.",{"id":1335,"title":1336,"titles":1337,"content":1338,"level":1026},"/v1.0.8/reference/api#except","Except",[1000,1287,1292],"func (q *Query[T]) Except(other *Query[T]) *Compound[T] Combines using EXCEPT.",{"id":1340,"title":1341,"titles":1342,"content":1343,"level":1026},"/v1.0.8/reference/api#exceptall","ExceptAll",[1000,1287,1292],"func (q *Query[T]) ExceptAll(other *Query[T]) *Compound[T] Combines using EXCEPT ALL.",{"id":1345,"title":1257,"titles":1346,"content":1347,"level":1026},"/v1.0.8/reference/api#exec-1",[1000,1287,1292],"func (q *Query[T]) Exec(ctx context.Context, params map[string]any) ([]*T, error) Executes and returns all matching rows.",{"id":1349,"title":1262,"titles":1350,"content":1351,"level":1026},"/v1.0.8/reference/api#exectx-1",[1000,1287,1292],"func (q *Query[T]) ExecTx(ctx context.Context, tx *sqlx.Tx, params map[string]any) ([]*T, error) Executes within a transaction.",{"id":1353,"title":1267,"titles":1354,"content":1355,"level":1026},"/v1.0.8/reference/api#execatom-1",[1000,1287,1292],"func (q *Query[T]) ExecAtom(ctx context.Context, params map[string]any) ([]*atom.Atom, error) Executes the query and scans all results directly into []*atom.Atom. Returns nil for empty results.",{"id":1357,"title":1272,"titles":1358,"content":1359,"level":1026},"/v1.0.8/reference/api#exectxatom-1",[1000,1287,1292],"func (q *Query[T]) ExecTxAtom(ctx context.Context, tx *sqlx.Tx, params map[string]any) ([]*atom.Atom, error) Executes within a transaction and returns []*atom.Atom.",{"id":1361,"title":1362,"titles":1363,"content":1364,"level":19},"/v1.0.8/reference/api#compoundt","CompoundT",[1000],"Builder for compound queries with set operations.",{"id":1366,"title":1019,"titles":1367,"content":1368,"level":40},"/v1.0.8/reference/api#methods-2",[1000,1362],"Inherits ordering and pagination methods from Query, plus:",{"id":1370,"title":1371,"titles":1372,"content":1373,"level":1026},"/v1.0.8/reference/api#union-unionall-intersect-intersectall-except-exceptall","Union, UnionAll, Intersect, IntersectAll, Except, ExceptAll",[1000,1362,1019],"func (c *Compound[T]) Union(other *Query[T]) *Compound[T] Chains additional set operations.",{"id":1375,"title":1376,"titles":1377,"content":1378,"level":1026},"/v1.0.8/reference/api#orderby-limit-offset","OrderBy, Limit, Offset",[1000,1362,1019],"Applied to the final combined result.",{"id":1380,"title":1257,"titles":1381,"content":1382,"level":1026},"/v1.0.8/reference/api#exec-2",[1000,1362,1019],"func (c *Compound[T]) Exec(ctx context.Context, params map[string]any) ([]*T, error)",{"id":1384,"title":1385,"titles":1386,"content":1387,"level":19},"/v1.0.8/reference/api#createt","CreateT",[1000],"Builder for INSERT operations.",{"id":1389,"title":1019,"titles":1390,"content":34,"level":40},"/v1.0.8/reference/api#methods-3",[1000,1385],{"id":1392,"title":1393,"titles":1394,"content":1395,"level":1026},"/v1.0.8/reference/api#onconflict","OnConflict",[1000,1385,1019],"func (c *Create[T]) OnConflict(columns ...string) *Create[T] Specifies conflict columns for ON CONFLICT handling.",{"id":1397,"title":1398,"titles":1399,"content":1400,"level":1026},"/v1.0.8/reference/api#donothing","DoNothing",[1000,1385,1019],"func (c *Create[T]) DoNothing() *Create[T] Sets ON CONFLICT DO NOTHING.",{"id":1402,"title":1403,"titles":1404,"content":1405,"level":1026},"/v1.0.8/reference/api#doupdate","DoUpdate",[1000,1385,1019],"func (c *Create[T]) DoUpdate() *Create[T] Sets ON CONFLICT DO UPDATE.",{"id":1407,"title":1408,"titles":1409,"content":1410,"level":1026},"/v1.0.8/reference/api#set","Set",[1000,1385,1019],"func (c *Create[T]) Set(field, param string) *Create[T] Adds a SET clause for DO UPDATE.",{"id":1412,"title":1413,"titles":1414,"content":1415,"level":1026},"/v1.0.8/reference/api#build","Build",[1000,1385,1019],"func (c *Create[T]) Build() *Create[T] Finalises the builder.",{"id":1417,"title":1257,"titles":1418,"content":1419,"level":1026},"/v1.0.8/reference/api#exec-3",[1000,1385,1019],"func (c *Create[T]) Exec(ctx context.Context, record *T) (*T, error) Inserts a single record and returns the result with RETURNING.",{"id":1421,"title":1262,"titles":1422,"content":1423,"level":1026},"/v1.0.8/reference/api#exectx-2",[1000,1385,1019],"func (c *Create[T]) ExecTx(ctx context.Context, tx *sqlx.Tx, record *T) (*T, error) Inserts within a transaction.",{"id":1425,"title":1267,"titles":1426,"content":1427,"level":1026},"/v1.0.8/reference/api#execatom-2",[1000,1385,1019],"func (c *Create[T]) ExecAtom(ctx context.Context, params map[string]any) (*atom.Atom, error) Inserts a record using a parameter map and returns the result as *atom.Atom. Requires all non-primary-key columns to be provided in params.",{"id":1429,"title":1272,"titles":1430,"content":1431,"level":1026},"/v1.0.8/reference/api#exectxatom-2",[1000,1385,1019],"func (c *Create[T]) ExecTxAtom(ctx context.Context, tx *sqlx.Tx, params map[string]any) (*atom.Atom, error) Inserts within a transaction and returns an *atom.Atom.",{"id":1433,"title":1434,"titles":1435,"content":1436,"level":1026},"/v1.0.8/reference/api#execbatch","ExecBatch",[1000,1385,1019],"func (c *Create[T]) ExecBatch(ctx context.Context, records []*T) (int64, error) Inserts multiple records and returns the count of records inserted.",{"id":1438,"title":1439,"titles":1440,"content":1441,"level":19},"/v1.0.8/reference/api#updatet","UpdateT",[1000],"Builder for UPDATE operations.",{"id":1443,"title":1019,"titles":1444,"content":34,"level":40},"/v1.0.8/reference/api#methods-4",[1000,1439],{"id":1446,"title":1408,"titles":1447,"content":1448,"level":1026},"/v1.0.8/reference/api#set-1",[1000,1439,1019],"func (u *Update[T]) Set(field, param string) *Update[T] Adds a column to update.",{"id":1450,"title":1451,"titles":1452,"content":1453,"level":1026},"/v1.0.8/reference/api#setexpr","SetExpr",[1000,1439,1019],"func (u *Update[T]) SetExpr(field, operator, param string) *Update[T] Adds a computed assignment using a binary expression: field = field \u003Cop> param. Use for atomic increments, decrements, and similar operations. Supported operators: +, -, *, /, %.",{"id":1455,"title":1456,"titles":1457,"content":1458,"level":1026},"/v1.0.8/reference/api#where-whereand-whereor-wherenull-wherenotnull","Where, WhereAnd, WhereOr, WhereNull, WhereNotNull",[1000,1439,1019],"Same as Select. At least one WHERE is required.",{"id":1460,"title":1257,"titles":1461,"content":1462,"level":1026},"/v1.0.8/reference/api#exec-4",[1000,1439,1019],"func (u *Update[T]) Exec(ctx context.Context, params map[string]any) (*T, error) Executes and returns the updated record. Returns error if no WHERE clause.",{"id":1464,"title":1262,"titles":1465,"content":1466,"level":1026},"/v1.0.8/reference/api#exectx-3",[1000,1439,1019],"func (u *Update[T]) ExecTx(ctx context.Context, tx *sqlx.Tx, params map[string]any) (*T, error)",{"id":1468,"title":1434,"titles":1469,"content":1470,"level":1026},"/v1.0.8/reference/api#execbatch-1",[1000,1439,1019],"func (u *Update[T]) ExecBatch(ctx context.Context, paramsList []map[string]any) (int64, error) Updates multiple records with different parameter sets.",{"id":1472,"title":1473,"titles":1474,"content":1475,"level":19},"/v1.0.8/reference/api#deletet","DeleteT",[1000],"Builder for DELETE operations.",{"id":1477,"title":1019,"titles":1478,"content":34,"level":40},"/v1.0.8/reference/api#methods-5",[1000,1473],{"id":1480,"title":1456,"titles":1481,"content":1458,"level":1026},"/v1.0.8/reference/api#where-whereand-whereor-wherenull-wherenotnull-1",[1000,1473,1019],{"id":1483,"title":1257,"titles":1484,"content":1485,"level":1026},"/v1.0.8/reference/api#exec-5",[1000,1473,1019],"func (d *Delete[T]) Exec(ctx context.Context, params map[string]any) (int64, error) Executes and returns rows affected. Returns error if no WHERE clause.",{"id":1487,"title":1262,"titles":1488,"content":1489,"level":1026},"/v1.0.8/reference/api#exectx-4",[1000,1473,1019],"func (d *Delete[T]) ExecTx(ctx context.Context, tx *sqlx.Tx, params map[string]any) (int64, error)",{"id":1491,"title":1434,"titles":1492,"content":1493,"level":1026},"/v1.0.8/reference/api#execbatch-2",[1000,1473,1019],"func (d *Delete[T]) ExecBatch(ctx context.Context, paramsList []map[string]any) (int64, error)",{"id":1495,"title":1496,"titles":1497,"content":1498,"level":19},"/v1.0.8/reference/api#aggregatet","AggregateT",[1000],"Builder for aggregate queries.",{"id":1500,"title":1019,"titles":1501,"content":34,"level":40},"/v1.0.8/reference/api#methods-6",[1000,1496],{"id":1503,"title":1456,"titles":1504,"content":1505,"level":1026},"/v1.0.8/reference/api#where-whereand-whereor-wherenull-wherenotnull-2",[1000,1496,1019],"Same as Select.",{"id":1507,"title":1257,"titles":1508,"content":1509,"level":1026},"/v1.0.8/reference/api#exec-6",[1000,1496,1019],"func (a *Aggregate[T]) Exec(ctx context.Context, params map[string]any) (float64, error) Returns the aggregate result. NULL results return 0.0.",{"id":1511,"title":1262,"titles":1512,"content":1513,"level":1026},"/v1.0.8/reference/api#exectx-5",[1000,1496,1019],"func (a *Aggregate[T]) ExecTx(ctx context.Context, tx *sqlx.Tx, params map[string]any) (float64, error)",{"id":1515,"title":153,"titles":1516,"content":34,"level":19},"/v1.0.8/reference/api#condition-helpers",[1000],{"id":1518,"title":1519,"titles":1520,"content":1521,"level":40},"/v1.0.8/reference/api#c","C",[1000,153],"func C(field, operator, param string) Condition Creates a simple condition.",{"id":1523,"title":1524,"titles":1525,"content":1526,"level":40},"/v1.0.8/reference/api#null","Null",[1000,153],"func Null(field string) Condition Creates an IS NULL condition.",{"id":1528,"title":1529,"titles":1530,"content":1531,"level":40},"/v1.0.8/reference/api#notnull","NotNull",[1000,153],"func NotNull(field string) Condition Creates an IS NOT NULL condition.",{"id":1533,"title":207,"titles":1534,"content":1535,"level":19},"/v1.0.8/reference/api#specs",[1000],"JSON-serializable query definitions.",{"id":1537,"title":656,"titles":1538,"content":1539,"level":40},"/v1.0.8/reference/api#queryspec",[1000,207],"type QuerySpec struct {\n    Fields     []string        `json:\"fields,omitempty\"`\n    Where      []ConditionSpec `json:\"where,omitempty\"`\n    OrderBy    []OrderBySpec   `json:\"order_by,omitempty\"`\n    GroupBy    []string        `json:\"group_by,omitempty\"`\n    Having     []ConditionSpec `json:\"having,omitempty\"`\n    HavingAgg  []HavingAggSpec `json:\"having_agg,omitempty\"`\n    Limit      *int            `json:\"limit,omitempty\"`\n    Offset     *int            `json:\"offset,omitempty\"`\n    Distinct   bool            `json:\"distinct,omitempty\"`\n    DistinctOn []string        `json:\"distinct_on,omitempty\"`\n    ForLocking string          `json:\"for_locking,omitempty\"` // \"update\", \"no_key_update\", \"share\", \"key_share\"\n}",{"id":1541,"title":661,"titles":1542,"content":1543,"level":40},"/v1.0.8/reference/api#selectspec",[1000,207],"type SelectSpec struct {\n    Fields     []string        `json:\"fields,omitempty\"`\n    Where      []ConditionSpec `json:\"where,omitempty\"`\n    OrderBy    []OrderBySpec   `json:\"order_by,omitempty\"`\n    GroupBy    []string        `json:\"group_by,omitempty\"`\n    Having     []ConditionSpec `json:\"having,omitempty\"`\n    HavingAgg  []HavingAggSpec `json:\"having_agg,omitempty\"`\n    Limit      *int            `json:\"limit,omitempty\"`\n    Offset     *int            `json:\"offset,omitempty\"`\n    Distinct   bool            `json:\"distinct,omitempty\"`\n    DistinctOn []string        `json:\"distinct_on,omitempty\"`\n    ForLocking string          `json:\"for_locking,omitempty\"` // \"update\", \"no_key_update\", \"share\", \"key_share\"\n}",{"id":1545,"title":1546,"titles":1547,"content":1548,"level":40},"/v1.0.8/reference/api#conditionspec","ConditionSpec",[1000,207],"type ConditionSpec struct {\n    // Simple condition fields\n    Field    string `json:\"field,omitempty\"`\n    Operator string `json:\"operator,omitempty\"`\n    Param    string `json:\"param,omitempty\"`\n    IsNull   bool   `json:\"is_null,omitempty\"`\n\n    // Condition group fields (for AND/OR grouping)\n    Logic string          `json:\"logic,omitempty\"` // \"AND\" or \"OR\"\n    Group []ConditionSpec `json:\"group,omitempty\"` // Nested conditions\n}",{"id":1550,"title":1551,"titles":1552,"content":1553,"level":40},"/v1.0.8/reference/api#orderbyspec","OrderBySpec",[1000,207],"type OrderBySpec struct {\n    Field     string `json:\"field\"`\n    Direction string `json:\"direction\"`           // \"asc\" or \"desc\"\n    Nulls     string `json:\"nulls,omitempty\"`     // \"first\" or \"last\" for NULLS FIRST/LAST\n    Operator  string `json:\"operator,omitempty\"`  // For vector ops: \"\u003C->\", \"\u003C#>\", \"\u003C=>\", \"\u003C+>\"\n    Param     string `json:\"param,omitempty\"`     // Parameter for expression-based ordering\n}",{"id":1555,"title":1556,"titles":1557,"content":1558,"level":40},"/v1.0.8/reference/api#havingaggspec","HavingAggSpec",[1000,207],"type HavingAggSpec struct {\n    Func     string `json:\"func\"`               // \"count\", \"sum\", \"avg\", \"min\", \"max\", \"count_distinct\"\n    Field    string `json:\"field,omitempty\"`    // Field to aggregate (empty for COUNT(*))\n    Operator string `json:\"operator\"`           // Comparison operator\n    Param    string `json:\"param\"`              // Parameter name for comparison value\n}",{"id":1560,"title":703,"titles":1561,"content":1562,"level":40},"/v1.0.8/reference/api#createspec",[1000,207],"type CreateSpec struct {\n    OnConflict     []string          `json:\"on_conflict,omitempty\"`     // Conflict columns\n    ConflictAction string            `json:\"conflict_action,omitempty\"` // \"nothing\" or \"update\"\n    ConflictSet    map[string]string `json:\"conflict_set,omitempty\"`    // Fields to update on conflict\n}",{"id":1564,"title":708,"titles":1565,"content":1566,"level":40},"/v1.0.8/reference/api#updatespec",[1000,207],"type UpdateSpec struct {\n    Set   map[string]string `json:\"set\"`\n    Where []ConditionSpec   `json:\"where\"`\n}",{"id":1568,"title":713,"titles":1569,"content":1570,"level":40},"/v1.0.8/reference/api#deletespec",[1000,207],"type DeleteSpec struct {\n    Where []ConditionSpec `json:\"where\"`\n}",{"id":1572,"title":1573,"titles":1574,"content":1575,"level":40},"/v1.0.8/reference/api#aggregatespec","AggregateSpec",[1000,207],"type AggregateSpec struct {\n    Field string          `json:\"field,omitempty\"` // Required for SUM/AVG/MIN/MAX, not used for COUNT\n    Where []ConditionSpec `json:\"where,omitempty\"`\n} The aggregate function is determined by which method you call (CountFromSpec, SumFromSpec, etc.), not by a field in the spec.",{"id":1577,"title":1578,"titles":1579,"content":1580,"level":40},"/v1.0.8/reference/api#compoundqueryspec","CompoundQuerySpec",[1000,207],"type CompoundQuerySpec struct {\n    Base     QuerySpec        `json:\"base\"`\n    Operands []SetOperandSpec `json:\"operands\"`\n    OrderBy  []OrderBySpec    `json:\"order_by,omitempty\"`\n    Limit    *int             `json:\"limit,omitempty\"`\n    Offset   *int             `json:\"offset,omitempty\"`\n}",{"id":1582,"title":1583,"titles":1584,"content":1585,"level":40},"/v1.0.8/reference/api#setoperandspec","SetOperandSpec",[1000,207],"type SetOperandSpec struct {\n    Operation string    `json:\"operation\"`\n    Query     QuerySpec `json:\"query\"`\n} Operations: \"union\", \"union_all\", \"intersect\", \"intersect_all\", \"except\", \"except_all\"",{"id":1587,"title":96,"titles":1588,"content":1589,"level":19},"/v1.0.8/reference/api#struct-tags",[1000],"TagPurposeExampledbColumn namedb:\"email\"typeSQL column typetype:\"text\", type:\"serial\", type:\"vector(1536)\"constraintsColumn constraintsconstraints:\"primary key\", constraints:\"not null unique\"defaultDefault valuedefault:\"now()\", default:\"0\"checkCheck constraintcheck:\"age >= 0\"indexCreate indexindex:\"true\"referencesForeign keyreferences:\"users(id)\"",{"id":1591,"title":1592,"titles":1593,"content":34,"level":19},"/v1.0.8/reference/api#operators","Operators",[1000],{"id":1595,"title":1596,"titles":1597,"content":1598,"level":40},"/v1.0.8/reference/api#comparison","Comparison",[1000,1592],"OperatorDescription=Equals!=Not equals>Greater than>=Greater than or equal\u003CLess than\u003C=Less than or equal",{"id":1600,"title":1601,"titles":1602,"content":1603,"level":40},"/v1.0.8/reference/api#pattern","Pattern",[1000,1592],"OperatorDescriptionLIKECase-sensitive patternNOT LIKENegated LIKEILIKECase-insensitive patternNOT ILIKENegated ILIKE",{"id":1605,"title":1408,"titles":1606,"content":1607,"level":40},"/v1.0.8/reference/api#set-2",[1000,1592],"OperatorDescriptionINValue in listNOT INValue not in list",{"id":1609,"title":1610,"titles":1611,"content":1612,"level":40},"/v1.0.8/reference/api#regex-postgresql","Regex (PostgreSQL)",[1000,1592],"OperatorDescription~Case-sensitive regex~*Case-insensitive regex!~Negated regex!~*Negated case-insensitive regex",{"id":1614,"title":1615,"titles":1616,"content":1617,"level":40},"/v1.0.8/reference/api#array","Array",[1000,1592],"OperatorDescription@>Contains\u003C@Contained by&&Overlap",{"id":1619,"title":1620,"titles":1621,"content":1622,"level":40},"/v1.0.8/reference/api#vector-pgvector","Vector (pgvector)",[1000,1592],"OperatorDescription\u003C->L2 (Euclidean) distance\u003C#>Inner product distance\u003C=>Cosine distance\u003C+>L1 (Manhattan) distance",{"id":1624,"title":1625,"titles":1626,"content":1627,"level":40},"/v1.0.8/reference/api#arithmetic","Arithmetic",[1000,1592],"OperatorDescription+Addition-Subtraction*Multiplication/Division%Modulo Used with SetExpr for computed UPDATE assignments.",{"id":1629,"title":1630,"titles":1631,"content":1632,"level":19},"/v1.0.8/reference/api#expression-methods","Expression Methods",[1000],"Select and Query builders support expression methods for adding computed columns to SELECT clauses.",{"id":1634,"title":1635,"titles":1636,"content":1637,"level":40},"/v1.0.8/reference/api#string-functions","String Functions",[1000,1630],"MethodSQL OutputSelectUpper(field, alias)UPPER(\"field\") AS \"alias\"SelectLower(field, alias)LOWER(\"field\") AS \"alias\"SelectLength(field, alias)LENGTH(\"field\") AS \"alias\"SelectTrim(field, alias)TRIM(\"field\") AS \"alias\"SelectLTrim(field, alias)LTRIM(\"field\") AS \"alias\"SelectRTrim(field, alias)RTRIM(\"field\") AS \"alias\"SelectSubstring(field, startParam, lengthParam, alias)SUBSTRING(\"field\", :start, :length) AS \"alias\"SelectReplace(field, searchParam, replaceParam, alias)REPLACE(\"field\", :search, :replace) AS \"alias\"SelectConcat(alias, fields...)CONCAT(\"field1\", \"field2\") AS \"alias\"",{"id":1639,"title":1640,"titles":1641,"content":1642,"level":40},"/v1.0.8/reference/api#math-functions","Math Functions",[1000,1630],"MethodSQL OutputSelectAbs(field, alias)ABS(\"field\") AS \"alias\"SelectCeil(field, alias)CEIL(\"field\") AS \"alias\"SelectFloor(field, alias)FLOOR(\"field\") AS \"alias\"SelectRound(field, alias)ROUND(\"field\") AS \"alias\"SelectSqrt(field, alias)SQRT(\"field\") AS \"alias\"SelectPower(field, exponentParam, alias)POWER(\"field\", :exp) AS \"alias\"",{"id":1644,"title":1645,"titles":1646,"content":1647,"level":40},"/v1.0.8/reference/api#datetime-functions","Date/Time Functions",[1000,1630],"MethodSQL OutputSelectNow(alias)NOW() AS \"alias\"SelectCurrentDate(alias)CURRENT_DATE AS \"alias\"SelectCurrentTime(alias)CURRENT_TIME AS \"alias\"SelectCurrentTimestamp(alias)CURRENT_TIMESTAMP AS \"alias\"",{"id":1649,"title":1650,"titles":1651,"content":1652,"level":40},"/v1.0.8/reference/api#null-handling","Null Handling",[1000,1630],"MethodSQL OutputSelectCoalesce(alias, params...)COALESCE(:p1, :p2, ...) AS \"alias\"SelectNullIf(param1, param2, alias)NULLIF(:p1, :p2) AS \"alias\"",{"id":1654,"title":1655,"titles":1656,"content":1657,"level":40},"/v1.0.8/reference/api#type-casting","Type Casting",[1000,1630],"func (s *Select[T]) SelectCast(field, castType, alias string) *Select[T] Cast types: text, integer, bigint, smallint, numeric, real, double precision, boolean, date, time, timestamp, timestamptz, interval, uuid, json, jsonb, bytea.",{"id":1659,"title":1660,"titles":1661,"content":1662,"level":40},"/v1.0.8/reference/api#aggregate-functions","Aggregate Functions",[1000,1630],"MethodSQL OutputSelectSum(field, alias)SUM(\"field\") AS \"alias\"SelectAvg(field, alias)AVG(\"field\") AS \"alias\"SelectMin(field, alias)MIN(\"field\") AS \"alias\"SelectMax(field, alias)MAX(\"field\") AS \"alias\"SelectCount(field, alias)COUNT(\"field\") AS \"alias\"SelectCountStar(alias)COUNT(*) AS \"alias\"SelectCountDistinct(field, alias)COUNT(DISTINCT \"field\") AS \"alias\"",{"id":1664,"title":1665,"titles":1666,"content":1667,"level":40},"/v1.0.8/reference/api#filter-aggregates-postgresql","FILTER Aggregates (PostgreSQL)",[1000,1630],"Aggregate with FILTER clause for conditional aggregation: MethodSQL OutputSelectSumFilter(field, condField, condOp, condParam, alias)SUM(\"field\") FILTER (WHERE \"condField\" op :param) AS \"alias\"SelectAvgFilter(field, condField, condOp, condParam, alias)AVG(\"field\") FILTER (WHERE ...) AS \"alias\"SelectMinFilter(field, condField, condOp, condParam, alias)MIN(\"field\") FILTER (WHERE ...) AS \"alias\"SelectMaxFilter(field, condField, condOp, condParam, alias)MAX(\"field\") FILTER (WHERE ...) AS \"alias\"SelectCountFilter(field, condField, condOp, condParam, alias)COUNT(\"field\") FILTER (WHERE ...) AS \"alias\"SelectCountDistinctFilter(field, condField, condOp, condParam, alias)COUNT(DISTINCT \"field\") FILTER (WHERE ...) AS \"alias\"",{"id":1669,"title":1670,"titles":1671,"content":1672,"level":19},"/v1.0.8/reference/api#case-expressions","CASE Expressions",[1000],"Build SQL CASE expressions with a fluent API: result, err := soy.Select().\n    Fields(\"id\", \"name\").\n    SelectCase().\n        When(soy.C(\"status\", \"=\", \"active\"), \"active_label\").\n        When(soy.C(\"status\", \"=\", \"pending\"), \"pending_label\").\n        Else(\"other_label\").\n        As(\"status_text\").\n        End().\n    Render()\n// SELECT \"id\", \"name\", CASE WHEN \"status\" = :active THEN :active_label ... END AS \"status_text\" FROM \"table\"",{"id":1674,"title":1675,"titles":1676,"content":1677,"level":40},"/v1.0.8/reference/api#selectcase-methods","SelectCase Methods",[1000,1670],"MethodDescriptionSelectCase()Start a CASE expression, returns *SelectCaseBuilderWhen(condition, resultParam)Add a WHEN...THEN clauseElse(resultParam)Set the ELSE clauseAs(alias)Set the result aliasEnd()Complete and return to parent builder",{"id":1679,"title":1680,"titles":1681,"content":1682,"level":19},"/v1.0.8/reference/api#window-functions","Window Functions",[1000],"Build SQL window functions with a fluent API: result, err := soy.Query().\n    Fields(\"id\", \"name\", \"department\").\n    SelectRowNumber().\n        PartitionBy(\"department\").\n        OrderBy(\"salary\", \"DESC\").\n        As(\"rank\").\n        End().\n    Render()\n// SELECT \"id\", \"name\", \"department\", ROW_NUMBER() OVER (PARTITION BY \"department\" ORDER BY \"salary\" DESC) AS \"rank\"",{"id":1684,"title":1685,"titles":1686,"content":1687,"level":40},"/v1.0.8/reference/api#window-function-starters","Window Function Starters",[1000,1680],"MethodSQL FunctionSelectRowNumber()ROW_NUMBER()SelectRank()RANK()SelectDenseRank()DENSE_RANK()SelectNtile(nParam)NTILE(:n)SelectLag(field, offsetParam)LAG(\"field\", :offset)SelectLead(field, offsetParam)LEAD(\"field\", :offset)SelectFirstValue(field)FIRST_VALUE(\"field\")SelectLastValue(field)LAST_VALUE(\"field\")SelectSumOver(field)SUM(\"field\") OVERSelectAvgOver(field)AVG(\"field\") OVERSelectCountOver()COUNT(*) OVERSelectMinOver(field)MIN(\"field\") OVERSelectMaxOver(field)MAX(\"field\") OVER",{"id":1689,"title":1690,"titles":1691,"content":1692,"level":40},"/v1.0.8/reference/api#window-builder-methods","Window Builder Methods",[1000,1680],"MethodDescriptionPartitionBy(fields...)Add PARTITION BY clauseOrderBy(field, direction)Add ORDER BY clauseFrame(start, end)Add ROWS BETWEEN frame clauseAs(alias)Set result aliasEnd()Complete and return to parent builder Frame bounds: \"UNBOUNDED PRECEDING\", \"CURRENT ROW\", \"UNBOUNDED FOLLOWING\"",{"id":1694,"title":153,"titles":1695,"content":34,"level":19},"/v1.0.8/reference/api#condition-helpers-1",[1000],{"id":1697,"title":1698,"titles":1699,"content":1700,"level":40},"/v1.0.8/reference/api#between","Between",[1000,153],"func Between(field, lowParam, highParam string) Condition Creates a BETWEEN condition for use with WhereAnd/WhereOr.",{"id":1702,"title":1703,"titles":1704,"content":1705,"level":40},"/v1.0.8/reference/api#notbetween","NotBetween",[1000,153],"func NotBetween(field, lowParam, highParam string) Condition Creates a NOT BETWEEN condition for use with WhereAnd/WhereOr.",{"id":1707,"title":1708,"titles":1709,"content":1710,"level":19},"/v1.0.8/reference/api#errors","Errors",[1000],"Soy uses sentinel errors that support errors.Is() and errors.As() for precise error handling.",{"id":1712,"title":1713,"titles":1714,"content":1715,"level":40},"/v1.0.8/reference/api#simple-sentinels","Simple Sentinels",[1000,1708],"ErrorDescriptionErrNotFoundQuery expects at least one row but finds noneErrMultipleRowsQuery expects exactly one row but finds multipleErrNoRowsAffectedOperation expects to affect rows but affects noneErrEmptyTableNameTable name is emptyErrNilRendererRenderer is nilErrUnsafeUpdateUPDATE without WHERE clauseErrUnsafeDeleteDELETE without WHERE clause",{"id":1717,"title":1718,"titles":1719,"content":1720,"level":40},"/v1.0.8/reference/api#validation-errors","Validation Errors",[1000,1708],"SentinelMatchesErrInvalidFieldInvalid field nameErrInvalidParamInvalid parameter nameErrInvalidOperatorUnsupported operatorErrInvalidDirectionInvalid sort directionErrInvalidNullsOrderingInvalid NULLS FIRST/LASTErrInvalidTableInvalid table nameErrInvalidConditionInvalid conditionErrInvalidAggregateFuncInvalid aggregate function",{"id":1722,"title":1723,"titles":1724,"content":1725,"level":40},"/v1.0.8/reference/api#query-errors","Query Errors",[1000,1708],"SentinelMatchesErrQueryFailedQuery execution failureErrScanFailedResult scanning failureErrIterationFailedRow iteration failureErrRenderFailedQuery rendering failure",{"id":1727,"title":1728,"titles":1729,"content":1730,"level":40},"/v1.0.8/reference/api#builder-errors","Builder Errors",[1000,1708],"SentinelMatchesErrBuilderHasErrorsBuilder accumulated errors",{"id":1732,"title":1733,"titles":1734,"content":1735,"level":40},"/v1.0.8/reference/api#usage-with-errorsis","Usage with errors.Is",[1000,1708],"record, err := soy.Select().Where(\"id\", \"=\", \"id\").Exec(ctx, params)\nif errors.Is(err, soy.ErrNotFound) {\n    // Handle missing record\n}\nif errors.Is(err, soy.ErrInvalidField) {\n    // Handle invalid field name\n}",{"id":1737,"title":1738,"titles":1739,"content":1740,"level":40},"/v1.0.8/reference/api#usage-with-errorsas","Usage with errors.As",[1000,1708],"Extract details from validation errors: _, err := soy.Select().Where(\"bad_field\", \"=\", \"id\").Exec(ctx, params)\n\nvar valErr *soy.ValidationError\nif errors.As(err, &valErr) {\n    fmt.Printf(\"Invalid %s: %s\\n\", valErr.Kind, valErr.Name)\n} Extract details from query errors: var qErr *soy.QueryError\nif errors.As(err, &qErr) {\n    fmt.Printf(\"Operation %s failed at %s phase\\n\", qErr.Operation, qErr.Phase)\n} html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}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 .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 .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)}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 .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 .suWN2, html code.shiki .suWN2{--shiki-default:var(--shiki-tag)}",[1742],{"title":1743,"path":1744,"stem":1745,"children":1746,"page":1760},"V108","/v1.0.8","v1.0.8",[1747,1749,1761,1778,1787],{"title":6,"path":5,"stem":1748,"description":8},"v1.0.8/1.overview",{"title":1750,"path":1751,"stem":1752,"children":1753,"page":1760},"Learn","/v1.0.8/learn","v1.0.8/2.learn",[1754,1756,1758],{"title":63,"path":62,"stem":1755,"description":65},"v1.0.8/2.learn/1.quickstart",{"title":106,"path":105,"stem":1757,"description":108},"v1.0.8/2.learn/2.concepts",{"title":16,"path":226,"stem":1759,"description":228},"v1.0.8/2.learn/3.architecture",false,{"title":1762,"path":1763,"stem":1764,"children":1765,"page":1760},"Guides","/v1.0.8/guides","v1.0.8/3.guides",[1766,1768,1770,1772,1774,1776],{"title":120,"path":299,"stem":1767,"description":301},"v1.0.8/3.guides/1.queries",{"title":436,"path":435,"stem":1769,"description":438},"v1.0.8/3.guides/2.mutations",{"title":566,"path":565,"stem":1771,"description":568},"v1.0.8/3.guides/3.aggregates",{"title":207,"path":638,"stem":1773,"description":640},"v1.0.8/3.guides/4.specs",{"title":743,"path":742,"stem":1775,"description":745},"v1.0.8/3.guides/5.compound",{"title":217,"path":818,"stem":1777,"description":820},"v1.0.8/3.guides/6.lifecycle",{"title":1779,"path":1780,"stem":1781,"children":1782,"page":1760},"Cookbook","/v1.0.8/cookbook","v1.0.8/4.cookbook",[1783,1785],{"title":187,"path":873,"stem":1784,"description":875},"v1.0.8/4.cookbook/1.pagination",{"title":932,"path":931,"stem":1786,"description":934},"v1.0.8/4.cookbook/2.pgvector",{"title":1788,"path":1789,"stem":1790,"children":1791,"page":1760},"Reference","/v1.0.8/reference","v1.0.8/5.reference",[1792],{"title":1000,"path":999,"stem":1793,"description":1002},"v1.0.8/5.reference/1.api",[1795],{"title":1743,"path":1744,"stem":1745,"children":1796,"page":1760},[1797,1798,1803,1811,1815],{"title":6,"path":5,"stem":1748},{"title":1750,"path":1751,"stem":1752,"children":1799,"page":1760},[1800,1801,1802],{"title":63,"path":62,"stem":1755},{"title":106,"path":105,"stem":1757},{"title":16,"path":226,"stem":1759},{"title":1762,"path":1763,"stem":1764,"children":1804,"page":1760},[1805,1806,1807,1808,1809,1810],{"title":120,"path":299,"stem":1767},{"title":436,"path":435,"stem":1769},{"title":566,"path":565,"stem":1771},{"title":207,"path":638,"stem":1773},{"title":743,"path":742,"stem":1775},{"title":217,"path":818,"stem":1777},{"title":1779,"path":1780,"stem":1781,"children":1812,"page":1760},[1813,1814],{"title":187,"path":873,"stem":1784},{"title":932,"path":931,"stem":1786},{"title":1788,"path":1789,"stem":1790,"children":1816,"page":1760},[1817],{"title":1000,"path":999,"stem":1793},[1819,3643,4173],{"id":1820,"title":1821,"body":1822,"description":34,"extension":3636,"icon":3637,"meta":3638,"navigation":1982,"path":3639,"seo":3640,"stem":3641,"__hash__":3642},"resources/readme.md","README",{"type":1823,"value":1824,"toc":3625},"minimark",[1825,1829,1897,1900,1903,1908,2151,2154,2158,2175,2178,2182,3112,3115,3256,3260,3311,3315,3321,3333,3507,3510,3514,3600,3604,3612,3615,3621],[1826,1827,1828],"h1",{"id":1828},"soy",[1830,1831,1832,1843,1851,1859,1867,1875,1882,1889],"p",{},[1833,1834,1838],"a",{"href":1835,"rel":1836},"https://github.com/zoobz-io/soy/actions/workflows/ci.yml",[1837],"nofollow",[1839,1840],"img",{"alt":1841,"src":1842},"CI Status","https://github.com/zoobz-io/soy/workflows/CI/badge.svg",[1833,1844,1847],{"href":1845,"rel":1846},"https://codecov.io/gh/zoobz-io/soy",[1837],[1839,1848],{"alt":1849,"src":1850},"codecov","https://codecov.io/gh/zoobz-io/soy/graph/badge.svg?branch=main",[1833,1852,1855],{"href":1853,"rel":1854},"https://goreportcard.com/report/github.com/zoobz-io/soy",[1837],[1839,1856],{"alt":1857,"src":1858},"Go Report Card","https://goreportcard.com/badge/github.com/zoobz-io/soy",[1833,1860,1863],{"href":1861,"rel":1862},"https://github.com/zoobz-io/soy/security/code-scanning",[1837],[1839,1864],{"alt":1865,"src":1866},"CodeQL","https://github.com/zoobz-io/soy/workflows/CodeQL/badge.svg",[1833,1868,1871],{"href":1869,"rel":1870},"https://pkg.go.dev/github.com/zoobz-io/soy",[1837],[1839,1872],{"alt":1873,"src":1874},"Go Reference","https://pkg.go.dev/badge/github.com/zoobz-io/soy.svg",[1833,1876,1878],{"href":1877},"LICENSE",[1839,1879],{"alt":1880,"src":1881},"License","https://img.shields.io/github/license/zoobz-io/soy",[1833,1883,1885],{"href":1884},"go.mod",[1839,1886],{"alt":1887,"src":1888},"Go Version","https://img.shields.io/github/go-mod/go-version/zoobz-io/soy",[1833,1890,1893],{"href":1891,"rel":1892},"https://github.com/zoobz-io/soy/releases",[1837],[1839,1894],{"alt":1895,"src":1896},"Release","https://img.shields.io/github/v/release/zoobz-io/soy",[1830,1898,1899],{},"Type-safe SQL query builder for Go with schema validation and multi-database support.",[1830,1901,1902],{},"Extract schema from struct tags, validate queries at initialization, execute with zero reflection.",[1904,1905,1907],"h2",{"id":1906},"schema-once-query-forever","Schema Once, Query Forever",[1909,1910,1914],"pre",{"className":1911,"code":1912,"language":1913,"meta":34,"style":34},"language-go shiki shiki-themes","type User struct {\n    ID    int64  `db:\"id\" type:\"bigserial primary key\"`\n    Email string `db:\"email\" type:\"text unique not null\"`\n    Name  string `db:\"name\" type:\"text\"`\n}\n\n// Schema extracted and validated here — once\nusers, _ := soy.New[User](db, \"users\", postgres.New())\n\n// Every query after: type-safe, zero reflection, validated fields\nuser, _ := users.Select().\n    Where(\"email\", \"=\", \"email_param\").\n    Exec(ctx, map[string]any{\"email_param\": \"alice@example.com\"})\n// Returns *User, not interface{}\n","go",[1915,1916,1917,1936,1949,1960,1971,1977,1984,1991,2045,2050,2056,2078,2103,2145],"code",{"__ignoreMap":34},[1918,1919,1921,1925,1929,1932],"span",{"class":1920,"line":9},"line",[1918,1922,1924],{"class":1923},"sUt3r","type",[1918,1926,1928],{"class":1927},"sYBwO"," User",[1918,1930,1931],{"class":1923}," struct",[1918,1933,1935],{"class":1934},"sq5bi"," {\n",[1918,1937,1938,1942,1945],{"class":1920,"line":19},[1918,1939,1941],{"class":1940},"sBGCq","    ID",[1918,1943,1944],{"class":1927},"    int64",[1918,1946,1948],{"class":1947},"sxAnc","  `db:\"id\" type:\"bigserial primary key\"`\n",[1918,1950,1951,1954,1957],{"class":1920,"line":40},[1918,1952,1953],{"class":1940},"    Email",[1918,1955,1956],{"class":1927}," string",[1918,1958,1959],{"class":1947}," `db:\"email\" type:\"text unique not null\"`\n",[1918,1961,1962,1965,1968],{"class":1920,"line":1026},[1918,1963,1964],{"class":1940},"    Name",[1918,1966,1967],{"class":1927},"  string",[1918,1969,1970],{"class":1947}," `db:\"name\" type:\"text\"`\n",[1918,1972,1974],{"class":1920,"line":1973},5,[1918,1975,1976],{"class":1934},"}\n",[1918,1978,1980],{"class":1920,"line":1979},6,[1918,1981,1983],{"emptyLinePlaceholder":1982},true,"\n",[1918,1985,1987],{"class":1920,"line":1986},7,[1918,1988,1990],{"class":1989},"sLkEo","// Schema extracted and validated here — once\n",[1918,1992,1994,1998,2001,2004,2007,2010,2013,2016,2019,2022,2025,2028,2030,2033,2035,2038,2040,2042],{"class":1920,"line":1993},8,[1918,1995,1997],{"class":1996},"sh8_p","users",[1918,1999,2000],{"class":1934},",",[1918,2002,2003],{"class":1996}," _",[1918,2005,2006],{"class":1996}," :=",[1918,2008,2009],{"class":1996}," soy",[1918,2011,2012],{"class":1934},".",[1918,2014,1014],{"class":2015},"s5klm",[1918,2017,2018],{"class":1934},"[",[1918,2020,2021],{"class":1927},"User",[1918,2023,2024],{"class":1934},"](",[1918,2026,2027],{"class":1996},"db",[1918,2029,2000],{"class":1934},[1918,2031,2032],{"class":1947}," \"users\"",[1918,2034,2000],{"class":1934},[1918,2036,2037],{"class":1996}," postgres",[1918,2039,2012],{"class":1934},[1918,2041,1014],{"class":2015},[1918,2043,2044],{"class":1934},"())\n",[1918,2046,2048],{"class":1920,"line":2047},9,[1918,2049,1983],{"emptyLinePlaceholder":1982},[1918,2051,2053],{"class":1920,"line":2052},10,[1918,2054,2055],{"class":1989},"// Every query after: type-safe, zero reflection, validated fields\n",[1918,2057,2059,2062,2064,2066,2068,2071,2073,2075],{"class":1920,"line":2058},11,[1918,2060,2061],{"class":1996},"user",[1918,2063,2000],{"class":1934},[1918,2065,2003],{"class":1996},[1918,2067,2006],{"class":1996},[1918,2069,2070],{"class":1996}," users",[1918,2072,2012],{"class":1934},[1918,2074,1023],{"class":2015},[1918,2076,2077],{"class":1934},"().\n",[1918,2079,2081,2084,2087,2090,2092,2095,2097,2100],{"class":1920,"line":2080},12,[1918,2082,2083],{"class":2015},"    Where",[1918,2085,2086],{"class":1934},"(",[1918,2088,2089],{"class":1947},"\"email\"",[1918,2091,2000],{"class":1934},[1918,2093,2094],{"class":1947}," \"=\"",[1918,2096,2000],{"class":1934},[1918,2098,2099],{"class":1947}," \"email_param\"",[1918,2101,2102],{"class":1934},").\n",[1918,2104,2106,2109,2111,2114,2116,2119,2121,2124,2127,2130,2133,2136,2139,2142],{"class":1920,"line":2105},13,[1918,2107,2108],{"class":2015},"    Exec",[1918,2110,2086],{"class":1934},[1918,2112,2113],{"class":1996},"ctx",[1918,2115,2000],{"class":1934},[1918,2117,2118],{"class":1923}," map",[1918,2120,2018],{"class":1934},[1918,2122,2123],{"class":1927},"string",[1918,2125,2126],{"class":1934},"]",[1918,2128,2129],{"class":1927},"any",[1918,2131,2132],{"class":1934},"{",[1918,2134,2135],{"class":1947},"\"email_param\"",[1918,2137,2138],{"class":1934},":",[1918,2140,2141],{"class":1947}," \"alice@example.com\"",[1918,2143,2144],{"class":1934},"})\n",[1918,2146,2148],{"class":1920,"line":2147},14,[1918,2149,2150],{"class":1989},"// Returns *User, not interface{}\n",[1830,2152,2153],{},"Field names validated against struct tags. Type-safe results. No reflection on the hot path.",[1904,2155,2157],{"id":2156},"install","Install",[1909,2159,2163],{"className":2160,"code":2161,"language":2162,"meta":34,"style":34},"language-bash shiki shiki-themes","go get github.com/zoobz-io/soy\n","bash",[1915,2164,2165],{"__ignoreMap":34},[1918,2166,2167,2169,2172],{"class":1920,"line":9},[1918,2168,1913],{"class":2015},[1918,2170,2171],{"class":1947}," get",[1918,2173,2174],{"class":1947}," github.com/zoobz-io/soy\n",[1830,2176,2177],{},"Requires Go 1.24+.",[1904,2179,2181],{"id":2180},"quick-start","Quick Start",[1909,2183,2185],{"className":1911,"code":2184,"language":1913,"meta":34,"style":34},"package main\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"log\"\n\n    \"github.com/jmoiron/sqlx\"\n    _ \"github.com/lib/pq\"\n    \"github.com/zoobz-io/astql/pkg/postgres\"\n    \"github.com/zoobz-io/soy\"\n)\n\ntype User struct {\n    ID    int64  `db:\"id\" type:\"bigserial primary key\"`\n    Email string `db:\"email\" type:\"text unique not null\"`\n    Name  string `db:\"name\" type:\"text\"`\n    Age   int    `db:\"age\" type:\"int\"`\n}\n\nfunc main() {\n    db, _ := sqlx.Connect(\"postgres\", \"postgres://localhost/mydb?sslmode=disable\")\n    defer db.Close()\n\n    // Create instance — schema validated here\n    users, _ := soy.New[User](db, \"users\", postgres.New())\n    ctx := context.Background()\n\n    // Insert\n    created, _ := users.Insert().Exec(ctx, &User{\n        Email: \"alice@example.com\",\n        Name:  \"Alice\",\n        Age:   30,\n    })\n    fmt.Printf(\"Created: %d\\n\", created.ID)\n\n    // Select one\n    user, _ := users.Select().\n        Where(\"email\", \"=\", \"email_param\").\n        Exec(ctx, map[string]any{\"email_param\": \"alice@example.com\"})\n    fmt.Printf(\"Found: %s\\n\", user.Name)\n\n    // Query many\n    all, _ := users.Query().\n        Where(\"age\", \">=\", \"min_age\").\n        OrderBy(\"name\", \"asc\").\n        Limit(10).\n        Exec(ctx, map[string]any{\"min_age\": 18})\n    fmt.Printf(\"Users: %d\\n\", len(all))\n\n    // Update\n    updated, _ := users.Modify().\n        Set(\"age\", \"new_age\").\n        Where(\"id\", \"=\", \"user_id\").\n        Exec(ctx, map[string]any{\"new_age\": 31, \"user_id\": created.ID})\n    fmt.Printf(\"Updated age: %d\\n\", updated.Age)\n\n    // Aggregate\n    count, _ := users.Count().\n        Where(\"age\", \">=\", \"min_age\").\n        Exec(ctx, map[string]any{\"min_age\": 18})\n    fmt.Printf(\"Count: %.0f\\n\", count)\n}\n",[1915,2186,2187,2195,2199,2208,2213,2218,2223,2227,2232,2241,2246,2251,2256,2260,2270,2279,2288,2297,2309,2314,2319,2333,2365,2383,2388,2394,2434,2452,2457,2463,2500,2513,2526,2540,2546,2585,2590,2596,2616,2636,2668,2701,2706,2712,2732,2754,2772,2785,2818,2852,2857,2863,2883,2900,2921,2966,2998,3003,3009,3029,3048,3079,3107],{"__ignoreMap":34},[1918,2188,2189,2192],{"class":1920,"line":9},[1918,2190,2191],{"class":1923},"package",[1918,2193,2194],{"class":1927}," main\n",[1918,2196,2197],{"class":1920,"line":19},[1918,2198,1983],{"emptyLinePlaceholder":1982},[1918,2200,2201,2204],{"class":1920,"line":40},[1918,2202,2203],{"class":1923},"import",[1918,2205,2207],{"class":2206},"soy-K"," (\n",[1918,2209,2210],{"class":1920,"line":1026},[1918,2211,2212],{"class":1947},"    \"context\"\n",[1918,2214,2215],{"class":1920,"line":1973},[1918,2216,2217],{"class":1947},"    \"fmt\"\n",[1918,2219,2220],{"class":1920,"line":1979},[1918,2221,2222],{"class":1947},"    \"log\"\n",[1918,2224,2225],{"class":1920,"line":1986},[1918,2226,1983],{"emptyLinePlaceholder":1982},[1918,2228,2229],{"class":1920,"line":1993},[1918,2230,2231],{"class":1947},"    \"github.com/jmoiron/sqlx\"\n",[1918,2233,2234,2238],{"class":1920,"line":2047},[1918,2235,2237],{"class":2236},"sSYET","    _",[1918,2239,2240],{"class":1947}," \"github.com/lib/pq\"\n",[1918,2242,2243],{"class":1920,"line":2052},[1918,2244,2245],{"class":1947},"    \"github.com/zoobz-io/astql/pkg/postgres\"\n",[1918,2247,2248],{"class":1920,"line":2058},[1918,2249,2250],{"class":1947},"    \"github.com/zoobz-io/soy\"\n",[1918,2252,2253],{"class":1920,"line":2080},[1918,2254,2255],{"class":2206},")\n",[1918,2257,2258],{"class":1920,"line":2105},[1918,2259,1983],{"emptyLinePlaceholder":1982},[1918,2261,2262,2264,2266,2268],{"class":1920,"line":2147},[1918,2263,1924],{"class":1923},[1918,2265,1928],{"class":1927},[1918,2267,1931],{"class":1923},[1918,2269,1935],{"class":1934},[1918,2271,2273,2275,2277],{"class":1920,"line":2272},15,[1918,2274,1941],{"class":1940},[1918,2276,1944],{"class":1927},[1918,2278,1948],{"class":1947},[1918,2280,2282,2284,2286],{"class":1920,"line":2281},16,[1918,2283,1953],{"class":1940},[1918,2285,1956],{"class":1927},[1918,2287,1959],{"class":1947},[1918,2289,2291,2293,2295],{"class":1920,"line":2290},17,[1918,2292,1964],{"class":1940},[1918,2294,1967],{"class":1927},[1918,2296,1970],{"class":1947},[1918,2298,2300,2303,2306],{"class":1920,"line":2299},18,[1918,2301,2302],{"class":1940},"    Age",[1918,2304,2305],{"class":1927},"   int",[1918,2307,2308],{"class":1947},"    `db:\"age\" type:\"int\"`\n",[1918,2310,2312],{"class":1920,"line":2311},19,[1918,2313,1976],{"class":1934},[1918,2315,2317],{"class":1920,"line":2316},20,[1918,2318,1983],{"emptyLinePlaceholder":1982},[1918,2320,2322,2325,2328,2331],{"class":1920,"line":2321},21,[1918,2323,2324],{"class":1923},"func",[1918,2326,2327],{"class":2015}," main",[1918,2329,2330],{"class":1934},"()",[1918,2332,1935],{"class":1934},[1918,2334,2336,2339,2341,2343,2345,2348,2350,2353,2355,2358,2360,2363],{"class":1920,"line":2335},22,[1918,2337,2338],{"class":1996},"    db",[1918,2340,2000],{"class":1934},[1918,2342,2003],{"class":1996},[1918,2344,2006],{"class":1996},[1918,2346,2347],{"class":1996}," sqlx",[1918,2349,2012],{"class":1934},[1918,2351,2352],{"class":2015},"Connect",[1918,2354,2086],{"class":1934},[1918,2356,2357],{"class":1947},"\"postgres\"",[1918,2359,2000],{"class":1934},[1918,2361,2362],{"class":1947}," \"postgres://localhost/mydb?sslmode=disable\"",[1918,2364,2255],{"class":1934},[1918,2366,2368,2372,2375,2377,2380],{"class":1920,"line":2367},23,[1918,2369,2371],{"class":2370},"sW3Qg","    defer",[1918,2373,2374],{"class":1996}," db",[1918,2376,2012],{"class":1934},[1918,2378,2379],{"class":2015},"Close",[1918,2381,2382],{"class":1934},"()\n",[1918,2384,2386],{"class":1920,"line":2385},24,[1918,2387,1983],{"emptyLinePlaceholder":1982},[1918,2389,2391],{"class":1920,"line":2390},25,[1918,2392,2393],{"class":1989},"    // Create instance — schema validated here\n",[1918,2395,2397,2400,2402,2404,2406,2408,2410,2412,2414,2416,2418,2420,2422,2424,2426,2428,2430,2432],{"class":1920,"line":2396},26,[1918,2398,2399],{"class":1996},"    users",[1918,2401,2000],{"class":1934},[1918,2403,2003],{"class":1996},[1918,2405,2006],{"class":1996},[1918,2407,2009],{"class":1996},[1918,2409,2012],{"class":1934},[1918,2411,1014],{"class":2015},[1918,2413,2018],{"class":1934},[1918,2415,2021],{"class":1927},[1918,2417,2024],{"class":1934},[1918,2419,2027],{"class":1996},[1918,2421,2000],{"class":1934},[1918,2423,2032],{"class":1947},[1918,2425,2000],{"class":1934},[1918,2427,2037],{"class":1996},[1918,2429,2012],{"class":1934},[1918,2431,1014],{"class":2015},[1918,2433,2044],{"class":1934},[1918,2435,2437,2440,2442,2445,2447,2450],{"class":1920,"line":2436},27,[1918,2438,2439],{"class":1996},"    ctx",[1918,2441,2006],{"class":1996},[1918,2443,2444],{"class":1996}," context",[1918,2446,2012],{"class":1934},[1918,2448,2449],{"class":2015},"Background",[1918,2451,2382],{"class":1934},[1918,2453,2455],{"class":1920,"line":2454},28,[1918,2456,1983],{"emptyLinePlaceholder":1982},[1918,2458,2460],{"class":1920,"line":2459},29,[1918,2461,2462],{"class":1989},"    // Insert\n",[1918,2464,2466,2469,2471,2473,2475,2477,2479,2481,2484,2486,2488,2490,2492,2495,2497],{"class":1920,"line":2465},30,[1918,2467,2468],{"class":1996},"    created",[1918,2470,2000],{"class":1934},[1918,2472,2003],{"class":1996},[1918,2474,2006],{"class":1996},[1918,2476,2070],{"class":1996},[1918,2478,2012],{"class":1934},[1918,2480,1034],{"class":2015},[1918,2482,2483],{"class":1934},"().",[1918,2485,1257],{"class":2015},[1918,2487,2086],{"class":1934},[1918,2489,2113],{"class":1996},[1918,2491,2000],{"class":1934},[1918,2493,2494],{"class":2370}," &",[1918,2496,2021],{"class":1927},[1918,2498,2499],{"class":1934},"{\n",[1918,2501,2503,2506,2508,2510],{"class":1920,"line":2502},31,[1918,2504,2505],{"class":1940},"        Email",[1918,2507,2138],{"class":1934},[1918,2509,2141],{"class":1947},[1918,2511,2512],{"class":1934},",\n",[1918,2514,2516,2519,2521,2524],{"class":1920,"line":2515},32,[1918,2517,2518],{"class":1940},"        Name",[1918,2520,2138],{"class":1934},[1918,2522,2523],{"class":1947},"  \"Alice\"",[1918,2525,2512],{"class":1934},[1918,2527,2529,2532,2534,2538],{"class":1920,"line":2528},33,[1918,2530,2531],{"class":1940},"        Age",[1918,2533,2138],{"class":1934},[1918,2535,2537],{"class":2536},"sMAmT","   30",[1918,2539,2512],{"class":1934},[1918,2541,2543],{"class":1920,"line":2542},34,[1918,2544,2545],{"class":1934},"    })\n",[1918,2547,2549,2552,2554,2557,2559,2562,2566,2570,2573,2575,2578,2580,2583],{"class":1920,"line":2548},35,[1918,2550,2551],{"class":1996},"    fmt",[1918,2553,2012],{"class":1934},[1918,2555,2556],{"class":2015},"Printf",[1918,2558,2086],{"class":1934},[1918,2560,2561],{"class":1947},"\"Created: ",[1918,2563,2565],{"class":2564},"scyPU","%d",[1918,2567,2569],{"class":2568},"suWN2","\\n",[1918,2571,2572],{"class":1947},"\"",[1918,2574,2000],{"class":1934},[1918,2576,2577],{"class":1996}," created",[1918,2579,2012],{"class":1934},[1918,2581,2582],{"class":1996},"ID",[1918,2584,2255],{"class":1934},[1918,2586,2588],{"class":1920,"line":2587},36,[1918,2589,1983],{"emptyLinePlaceholder":1982},[1918,2591,2593],{"class":1920,"line":2592},37,[1918,2594,2595],{"class":1989},"    // Select one\n",[1918,2597,2599,2602,2604,2606,2608,2610,2612,2614],{"class":1920,"line":2598},38,[1918,2600,2601],{"class":1996},"    user",[1918,2603,2000],{"class":1934},[1918,2605,2003],{"class":1996},[1918,2607,2006],{"class":1996},[1918,2609,2070],{"class":1996},[1918,2611,2012],{"class":1934},[1918,2613,1023],{"class":2015},[1918,2615,2077],{"class":1934},[1918,2617,2619,2622,2624,2626,2628,2630,2632,2634],{"class":1920,"line":2618},39,[1918,2620,2621],{"class":2015},"        Where",[1918,2623,2086],{"class":1934},[1918,2625,2089],{"class":1947},[1918,2627,2000],{"class":1934},[1918,2629,2094],{"class":1947},[1918,2631,2000],{"class":1934},[1918,2633,2099],{"class":1947},[1918,2635,2102],{"class":1934},[1918,2637,2639,2642,2644,2646,2648,2650,2652,2654,2656,2658,2660,2662,2664,2666],{"class":1920,"line":2638},40,[1918,2640,2641],{"class":2015},"        Exec",[1918,2643,2086],{"class":1934},[1918,2645,2113],{"class":1996},[1918,2647,2000],{"class":1934},[1918,2649,2118],{"class":1923},[1918,2651,2018],{"class":1934},[1918,2653,2123],{"class":1927},[1918,2655,2126],{"class":1934},[1918,2657,2129],{"class":1927},[1918,2659,2132],{"class":1934},[1918,2661,2135],{"class":1947},[1918,2663,2138],{"class":1934},[1918,2665,2141],{"class":1947},[1918,2667,2144],{"class":1934},[1918,2669,2671,2673,2675,2677,2679,2682,2685,2687,2689,2691,2694,2696,2699],{"class":1920,"line":2670},41,[1918,2672,2551],{"class":1996},[1918,2674,2012],{"class":1934},[1918,2676,2556],{"class":2015},[1918,2678,2086],{"class":1934},[1918,2680,2681],{"class":1947},"\"Found: ",[1918,2683,2684],{"class":2564},"%s",[1918,2686,2569],{"class":2568},[1918,2688,2572],{"class":1947},[1918,2690,2000],{"class":1934},[1918,2692,2693],{"class":1996}," user",[1918,2695,2012],{"class":1934},[1918,2697,2698],{"class":1996},"Name",[1918,2700,2255],{"class":1934},[1918,2702,2704],{"class":1920,"line":2703},42,[1918,2705,1983],{"emptyLinePlaceholder":1982},[1918,2707,2709],{"class":1920,"line":2708},43,[1918,2710,2711],{"class":1989},"    // Query many\n",[1918,2713,2715,2718,2720,2722,2724,2726,2728,2730],{"class":1920,"line":2714},44,[1918,2716,2717],{"class":1996},"    all",[1918,2719,2000],{"class":1934},[1918,2721,2003],{"class":1996},[1918,2723,2006],{"class":1996},[1918,2725,2070],{"class":1996},[1918,2727,2012],{"class":1934},[1918,2729,1029],{"class":2015},[1918,2731,2077],{"class":1934},[1918,2733,2735,2737,2739,2742,2744,2747,2749,2752],{"class":1920,"line":2734},45,[1918,2736,2621],{"class":2015},[1918,2738,2086],{"class":1934},[1918,2740,2741],{"class":1947},"\"age\"",[1918,2743,2000],{"class":1934},[1918,2745,2746],{"class":1947}," \">=\"",[1918,2748,2000],{"class":1934},[1918,2750,2751],{"class":1947}," \"min_age\"",[1918,2753,2102],{"class":1934},[1918,2755,2757,2760,2762,2765,2767,2770],{"class":1920,"line":2756},46,[1918,2758,2759],{"class":2015},"        OrderBy",[1918,2761,2086],{"class":1934},[1918,2763,2764],{"class":1947},"\"name\"",[1918,2766,2000],{"class":1934},[1918,2768,2769],{"class":1947}," \"asc\"",[1918,2771,2102],{"class":1934},[1918,2773,2775,2778,2780,2783],{"class":1920,"line":2774},47,[1918,2776,2777],{"class":2015},"        Limit",[1918,2779,2086],{"class":1934},[1918,2781,2782],{"class":2536},"10",[1918,2784,2102],{"class":1934},[1918,2786,2788,2790,2792,2794,2796,2798,2800,2802,2804,2806,2808,2811,2813,2816],{"class":1920,"line":2787},48,[1918,2789,2641],{"class":2015},[1918,2791,2086],{"class":1934},[1918,2793,2113],{"class":1996},[1918,2795,2000],{"class":1934},[1918,2797,2118],{"class":1923},[1918,2799,2018],{"class":1934},[1918,2801,2123],{"class":1927},[1918,2803,2126],{"class":1934},[1918,2805,2129],{"class":1927},[1918,2807,2132],{"class":1934},[1918,2809,2810],{"class":1947},"\"min_age\"",[1918,2812,2138],{"class":1934},[1918,2814,2815],{"class":2536}," 18",[1918,2817,2144],{"class":1934},[1918,2819,2821,2823,2825,2827,2829,2832,2834,2836,2838,2840,2844,2846,2849],{"class":1920,"line":2820},49,[1918,2822,2551],{"class":1996},[1918,2824,2012],{"class":1934},[1918,2826,2556],{"class":2015},[1918,2828,2086],{"class":1934},[1918,2830,2831],{"class":1947},"\"Users: ",[1918,2833,2565],{"class":2564},[1918,2835,2569],{"class":2568},[1918,2837,2572],{"class":1947},[1918,2839,2000],{"class":1934},[1918,2841,2843],{"class":2842},"skxcq"," len",[1918,2845,2086],{"class":1934},[1918,2847,2848],{"class":1996},"all",[1918,2850,2851],{"class":1934},"))\n",[1918,2853,2855],{"class":1920,"line":2854},50,[1918,2856,1983],{"emptyLinePlaceholder":1982},[1918,2858,2860],{"class":1920,"line":2859},51,[1918,2861,2862],{"class":1989},"    // Update\n",[1918,2864,2866,2869,2871,2873,2875,2877,2879,2881],{"class":1920,"line":2865},52,[1918,2867,2868],{"class":1996},"    updated",[1918,2870,2000],{"class":1934},[1918,2872,2003],{"class":1996},[1918,2874,2006],{"class":1996},[1918,2876,2070],{"class":1996},[1918,2878,2012],{"class":1934},[1918,2880,1039],{"class":2015},[1918,2882,2077],{"class":1934},[1918,2884,2886,2889,2891,2893,2895,2898],{"class":1920,"line":2885},53,[1918,2887,2888],{"class":2015},"        Set",[1918,2890,2086],{"class":1934},[1918,2892,2741],{"class":1947},[1918,2894,2000],{"class":1934},[1918,2896,2897],{"class":1947}," \"new_age\"",[1918,2899,2102],{"class":1934},[1918,2901,2903,2905,2907,2910,2912,2914,2916,2919],{"class":1920,"line":2902},54,[1918,2904,2621],{"class":2015},[1918,2906,2086],{"class":1934},[1918,2908,2909],{"class":1947},"\"id\"",[1918,2911,2000],{"class":1934},[1918,2913,2094],{"class":1947},[1918,2915,2000],{"class":1934},[1918,2917,2918],{"class":1947}," \"user_id\"",[1918,2920,2102],{"class":1934},[1918,2922,2924,2926,2928,2930,2932,2934,2936,2938,2940,2942,2944,2947,2949,2952,2954,2956,2958,2960,2962,2964],{"class":1920,"line":2923},55,[1918,2925,2641],{"class":2015},[1918,2927,2086],{"class":1934},[1918,2929,2113],{"class":1996},[1918,2931,2000],{"class":1934},[1918,2933,2118],{"class":1923},[1918,2935,2018],{"class":1934},[1918,2937,2123],{"class":1927},[1918,2939,2126],{"class":1934},[1918,2941,2129],{"class":1927},[1918,2943,2132],{"class":1934},[1918,2945,2946],{"class":1947},"\"new_age\"",[1918,2948,2138],{"class":1934},[1918,2950,2951],{"class":2536}," 31",[1918,2953,2000],{"class":1934},[1918,2955,2918],{"class":1947},[1918,2957,2138],{"class":1934},[1918,2959,2577],{"class":1996},[1918,2961,2012],{"class":1934},[1918,2963,2582],{"class":1996},[1918,2965,2144],{"class":1934},[1918,2967,2969,2971,2973,2975,2977,2980,2982,2984,2986,2988,2991,2993,2996],{"class":1920,"line":2968},56,[1918,2970,2551],{"class":1996},[1918,2972,2012],{"class":1934},[1918,2974,2556],{"class":2015},[1918,2976,2086],{"class":1934},[1918,2978,2979],{"class":1947},"\"Updated age: ",[1918,2981,2565],{"class":2564},[1918,2983,2569],{"class":2568},[1918,2985,2572],{"class":1947},[1918,2987,2000],{"class":1934},[1918,2989,2990],{"class":1996}," updated",[1918,2992,2012],{"class":1934},[1918,2994,2995],{"class":1996},"Age",[1918,2997,2255],{"class":1934},[1918,2999,3001],{"class":1920,"line":3000},57,[1918,3002,1983],{"emptyLinePlaceholder":1982},[1918,3004,3006],{"class":1920,"line":3005},58,[1918,3007,3008],{"class":1989},"    // Aggregate\n",[1918,3010,3012,3015,3017,3019,3021,3023,3025,3027],{"class":1920,"line":3011},59,[1918,3013,3014],{"class":1996},"    count",[1918,3016,2000],{"class":1934},[1918,3018,2003],{"class":1996},[1918,3020,2006],{"class":1996},[1918,3022,2070],{"class":1996},[1918,3024,2012],{"class":1934},[1918,3026,1049],{"class":2015},[1918,3028,2077],{"class":1934},[1918,3030,3032,3034,3036,3038,3040,3042,3044,3046],{"class":1920,"line":3031},60,[1918,3033,2621],{"class":2015},[1918,3035,2086],{"class":1934},[1918,3037,2741],{"class":1947},[1918,3039,2000],{"class":1934},[1918,3041,2746],{"class":1947},[1918,3043,2000],{"class":1934},[1918,3045,2751],{"class":1947},[1918,3047,2102],{"class":1934},[1918,3049,3051,3053,3055,3057,3059,3061,3063,3065,3067,3069,3071,3073,3075,3077],{"class":1920,"line":3050},61,[1918,3052,2641],{"class":2015},[1918,3054,2086],{"class":1934},[1918,3056,2113],{"class":1996},[1918,3058,2000],{"class":1934},[1918,3060,2118],{"class":1923},[1918,3062,2018],{"class":1934},[1918,3064,2123],{"class":1927},[1918,3066,2126],{"class":1934},[1918,3068,2129],{"class":1927},[1918,3070,2132],{"class":1934},[1918,3072,2810],{"class":1947},[1918,3074,2138],{"class":1934},[1918,3076,2815],{"class":2536},[1918,3078,2144],{"class":1934},[1918,3080,3082,3084,3086,3088,3090,3093,3096,3098,3100,3102,3105],{"class":1920,"line":3081},62,[1918,3083,2551],{"class":1996},[1918,3085,2012],{"class":1934},[1918,3087,2556],{"class":2015},[1918,3089,2086],{"class":1934},[1918,3091,3092],{"class":1947},"\"Count: ",[1918,3094,3095],{"class":2564},"%.0f",[1918,3097,2569],{"class":2568},[1918,3099,2572],{"class":1947},[1918,3101,2000],{"class":1934},[1918,3103,3104],{"class":1996}," count",[1918,3106,2255],{"class":1934},[1918,3108,3110],{"class":1920,"line":3109},63,[1918,3111,1976],{"class":1934},[1904,3113,27],{"id":3114},"capabilities",[3116,3117,3118,3134],"table",{},[3119,3120,3121],"thead",{},[3122,3123,3124,3128,3131],"tr",{},[3125,3126,3127],"th",{},"Feature",[3125,3129,3130],{},"Description",[3125,3132,3133],{},"Docs",[3135,3136,3137,3162,3176,3193,3206,3218,3231,3244],"tbody",{},[3122,3138,3139,3143,3157],{},[3140,3141,3142],"td",{},"Type-Safe Queries",[3140,3144,3145,3146,3149,3150,3153,3154],{},"Generics return ",[1915,3147,3148],{},"*T"," or ",[1915,3151,3152],{},"[]*T",", not ",[1915,3155,3156],{},"interface{}",[3140,3158,3159],{},[1833,3160,120],{"href":3161},"docs/guides/queries",[3122,3163,3164,3167,3170],{},[3140,3165,3166],{},"Schema Validation",[3140,3168,3169],{},"Field names checked against struct tags at init",[3140,3171,3172],{},[1833,3173,3175],{"href":3174},"docs/learn/concepts","Concepts",[3122,3177,3178,3181,3188],{},[3140,3179,3180],{},"Multi-Database",[3140,3182,3183,3184],{},"PostgreSQL, MariaDB, SQLite, SQL Server via ",[1833,3185,249],{"href":3186,"rel":3187},"https://github.com/zoobz-io/astql",[1837],[3140,3189,3190],{},[1833,3191,63],{"href":3192},"docs/learn/quickstart",[3122,3194,3195,3198,3201],{},[3140,3196,3197],{},"Fluent Builders",[3140,3199,3200],{},"Chainable API for SELECT, INSERT, UPDATE, DELETE",[3140,3202,3203],{},[1833,3204,436],{"href":3205},"docs/guides/mutations",[3122,3207,3208,3210,3213],{},[3140,3209,566],{},[3140,3211,3212],{},"COUNT, SUM, AVG, MIN, MAX with FILTER clauses",[3140,3214,3215],{},[1833,3216,566],{"href":3217},"docs/guides/aggregates",[3122,3219,3220,3222,3225],{},[3140,3221,1680],{},[3140,3223,3224],{},"ROW_NUMBER, RANK, LAG, LEAD, and more",[3140,3226,3227],{},[1833,3228,3230],{"href":3229},"docs/reference/api","API",[3122,3232,3233,3235,3238],{},[3140,3234,743],{},[3140,3236,3237],{},"UNION, INTERSECT, EXCEPT",[3140,3239,3240],{},[1833,3241,3243],{"href":3242},"docs/guides/compound","Compound",[3122,3245,3246,3249,3252],{},[3140,3247,3248],{},"Safety Guards",[3140,3250,3251],{},"DELETE/UPDATE require WHERE; prevents accidents",[3140,3253,3254],{},[1833,3255,3175],{"href":3174},[1904,3257,3259],{"id":3258},"why-soy","Why soy?",[3261,3262,3263,3274,3287,3293,3299,3305],"ul",{},[3264,3265,3266,3270,3271],"li",{},[3267,3268,3269],"strong",{},"Zero reflection on hot path"," — all introspection happens once at ",[1915,3272,3273],{},"New()",[3264,3275,3276,3279,3280,3149,3282,3284,3285],{},[3267,3277,3278],{},"Type-safe results"," — queries return ",[1915,3281,3148],{},[1915,3283,3152],{},", never ",[1915,3286,3156],{},[3264,3288,3289,3292],{},[3267,3290,3291],{},"Schema validation at init"," — field name typos caught immediately, not at runtime",[3264,3294,3295,3298],{},[3267,3296,3297],{},"Multi-database parity"," — same API across PostgreSQL, MariaDB, SQLite, SQL Server",[3264,3300,3301,3304],{},[3267,3302,3303],{},"Safety by default"," — DELETE/UPDATE require WHERE; prevents accidental full-table operations",[3264,3306,3307,3310],{},[3267,3308,3309],{},"Minimal dependencies"," — sqlx plus purpose-built libraries (astql, sentinel, atom)",[1904,3312,3314],{"id":3313},"type-safe-database-layer","Type-Safe Database Layer",[1830,3316,3317,3318,2012],{},"Soy enables a pattern: ",[3267,3319,3320],{},"define types once, query safely everywhere",[1830,3322,3323,3324,3328,3329,3332],{},"Your struct definitions become the contract. ",[1833,3325,244],{"href":3326,"rel":3327},"https://github.com/zoobz-io/sentinel",[1837]," extracts metadata from struct tags. ",[1833,3330,249],{"href":3186,"rel":3331},[1837]," validates queries against that schema. Soy wraps it all in a fluent API.",[1909,3334,3336],{"className":1911,"code":3335,"language":1913,"meta":34,"style":34},"// Your domain type — the single source of truth\ntype Order struct {\n    ID        int64     `db:\"id\" type:\"bigserial primary key\"`\n    UserID    int64     `db:\"user_id\" type:\"bigint not null\" references:\"users(id)\"`\n    Total     float64   `db:\"total\" type:\"numeric(10,2) not null\"`\n    Status    string    `db:\"status\" type:\"text\" check:\"status IN ('pending','paid','shipped')\"`\n    CreatedAt time.Time `db:\"created_at\" type:\"timestamptz default now()\"`\n}\n\n// Soy validates against the schema\norders, _ := soy.New[Order](db, \"orders\", postgres.New())\n\n// Invalid field? Caught at init, not runtime\norders.Select().Where(\"totla\", \"=\", \"x\")  // Error: field \"totla\" not found\n",[1915,3337,3338,3343,3354,3364,3374,3385,3396,3412,3416,3420,3425,3466,3470,3475],{"__ignoreMap":34},[1918,3339,3340],{"class":1920,"line":9},[1918,3341,3342],{"class":1989},"// Your domain type — the single source of truth\n",[1918,3344,3345,3347,3350,3352],{"class":1920,"line":19},[1918,3346,1924],{"class":1923},[1918,3348,3349],{"class":1927}," Order",[1918,3351,1931],{"class":1923},[1918,3353,1935],{"class":1934},[1918,3355,3356,3358,3361],{"class":1920,"line":40},[1918,3357,1941],{"class":1940},[1918,3359,3360],{"class":1927},"        int64",[1918,3362,3363],{"class":1947},"     `db:\"id\" type:\"bigserial primary key\"`\n",[1918,3365,3366,3369,3371],{"class":1920,"line":1026},[1918,3367,3368],{"class":1940},"    UserID",[1918,3370,1944],{"class":1927},[1918,3372,3373],{"class":1947},"     `db:\"user_id\" type:\"bigint not null\" references:\"users(id)\"`\n",[1918,3375,3376,3379,3382],{"class":1920,"line":1973},[1918,3377,3378],{"class":1940},"    Total",[1918,3380,3381],{"class":1927},"     float64",[1918,3383,3384],{"class":1947},"   `db:\"total\" type:\"numeric(10,2) not null\"`\n",[1918,3386,3387,3390,3393],{"class":1920,"line":1979},[1918,3388,3389],{"class":1940},"    Status",[1918,3391,3392],{"class":1927},"    string",[1918,3394,3395],{"class":1947},"    `db:\"status\" type:\"text\" check:\"status IN ('pending','paid','shipped')\"`\n",[1918,3397,3398,3401,3404,3406,3409],{"class":1920,"line":1986},[1918,3399,3400],{"class":1940},"    CreatedAt",[1918,3402,3403],{"class":1927}," time",[1918,3405,2012],{"class":1934},[1918,3407,3408],{"class":1927},"Time",[1918,3410,3411],{"class":1947}," `db:\"created_at\" type:\"timestamptz default now()\"`\n",[1918,3413,3414],{"class":1920,"line":1993},[1918,3415,1976],{"class":1934},[1918,3417,3418],{"class":1920,"line":2047},[1918,3419,1983],{"emptyLinePlaceholder":1982},[1918,3421,3422],{"class":1920,"line":2052},[1918,3423,3424],{"class":1989},"// Soy validates against the schema\n",[1918,3426,3427,3430,3432,3434,3436,3438,3440,3442,3444,3447,3449,3451,3453,3456,3458,3460,3462,3464],{"class":1920,"line":2058},[1918,3428,3429],{"class":1996},"orders",[1918,3431,2000],{"class":1934},[1918,3433,2003],{"class":1996},[1918,3435,2006],{"class":1996},[1918,3437,2009],{"class":1996},[1918,3439,2012],{"class":1934},[1918,3441,1014],{"class":2015},[1918,3443,2018],{"class":1934},[1918,3445,3446],{"class":1927},"Order",[1918,3448,2024],{"class":1934},[1918,3450,2027],{"class":1996},[1918,3452,2000],{"class":1934},[1918,3454,3455],{"class":1947}," \"orders\"",[1918,3457,2000],{"class":1934},[1918,3459,2037],{"class":1996},[1918,3461,2012],{"class":1934},[1918,3463,1014],{"class":2015},[1918,3465,2044],{"class":1934},[1918,3467,3468],{"class":1920,"line":2080},[1918,3469,1983],{"emptyLinePlaceholder":1982},[1918,3471,3472],{"class":1920,"line":2105},[1918,3473,3474],{"class":1989},"// Invalid field? Caught at init, not runtime\n",[1918,3476,3477,3479,3481,3483,3485,3487,3489,3492,3494,3496,3498,3501,3504],{"class":1920,"line":2147},[1918,3478,3429],{"class":1996},[1918,3480,2012],{"class":1934},[1918,3482,1023],{"class":2015},[1918,3484,2483],{"class":1934},[1918,3486,1157],{"class":2015},[1918,3488,2086],{"class":1934},[1918,3490,3491],{"class":1947},"\"totla\"",[1918,3493,2000],{"class":1934},[1918,3495,2094],{"class":1947},[1918,3497,2000],{"class":1934},[1918,3499,3500],{"class":1947}," \"x\"",[1918,3502,3503],{"class":1934},")",[1918,3505,3506],{"class":1989},"  // Error: field \"totla\" not found\n",[1830,3508,3509],{},"Three packages, one type definition, complete safety from struct tags to SQL execution.",[1904,3511,3513],{"id":3512},"documentation","Documentation",[3261,3515,3516,3522,3538,3570,3589],{},[3264,3517,3518,3521],{},[1833,3519,6],{"href":3520},"docs/overview"," — what soy does and why",[3264,3523,3524,3526],{},[3267,3525,1750],{},[3261,3527,3528,3533],{},[3264,3529,3530,3532],{},[1833,3531,63],{"href":3192}," — get started in minutes",[3264,3534,3535,3537],{},[1833,3536,3175],{"href":3174}," — queries, conditions, builders",[3264,3539,3540,3542],{},[3267,3541,1762],{},[3261,3543,3544,3549,3554,3559,3565],{},[3264,3545,3546,3548],{},[1833,3547,120],{"href":3161}," — SELECT with filtering and ordering",[3264,3550,3551,3553],{},[1833,3552,436],{"href":3205}," — INSERT, UPDATE, DELETE",[3264,3555,3556,3558],{},[1833,3557,566],{"href":3217}," — COUNT, SUM, AVG, MIN, MAX",[3264,3560,3561,3564],{},[1833,3562,207],{"href":3563},"docs/guides/specs"," — JSON-serializable query definitions",[3264,3566,3567,3569],{},[1833,3568,743],{"href":3242}," — UNION, INTERSECT, EXCEPT",[3264,3571,3572,3574],{},[3267,3573,1779],{},[3261,3575,3576,3582],{},[3264,3577,3578,3581],{},[1833,3579,187],{"href":3580},"docs/cookbook/pagination"," — LIMIT/OFFSET patterns",[3264,3583,3584,3588],{},[1833,3585,3587],{"href":3586},"docs/cookbook/pgvector","Vector Search"," — pgvector similarity queries",[3264,3590,3591,3593],{},[3267,3592,1788],{},[3261,3594,3595],{},[3264,3596,3597,3599],{},[1833,3598,3230],{"href":3229}," — complete function documentation",[1904,3601,3603],{"id":3602},"contributing","Contributing",[1830,3605,3606,3607,3611],{},"See ",[1833,3608,3610],{"href":3609},"CONTRIBUTING","CONTRIBUTING.md"," for guidelines.",[1904,3613,1880],{"id":3614},"license",[1830,3616,3617,3618,3620],{},"MIT License — see ",[1833,3619,1877],{"href":1877}," for details.",[3622,3623,3624],"style",{},"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 .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}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 .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 .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 .suWN2, html code.shiki .suWN2{--shiki-default:var(--shiki-tag)}html pre.shiki code .skxcq, html code.shiki .skxcq{--shiki-default:var(--shiki-builtin)}",{"title":34,"searchDepth":19,"depth":19,"links":3626},[3627,3628,3629,3630,3631,3632,3633,3634,3635],{"id":1906,"depth":19,"text":1907},{"id":2156,"depth":19,"text":2157},{"id":2180,"depth":19,"text":2181},{"id":3114,"depth":19,"text":27},{"id":3258,"depth":19,"text":3259},{"id":3313,"depth":19,"text":3314},{"id":3512,"depth":19,"text":3513},{"id":3602,"depth":19,"text":3603},{"id":3614,"depth":19,"text":1880},"md","book-open",{},"/readme",{"title":1821,"description":34},"readme","aQM34moy6E-AVmTG2YLNw3v3bkCsktva2w3y7TRvXXQ",{"id":3644,"title":3645,"body":3646,"description":34,"extension":3636,"icon":4167,"meta":4168,"navigation":1982,"path":4169,"seo":4170,"stem":4171,"__hash__":4172},"resources/security.md","Security",{"type":1823,"value":3647,"toc":4148},[3648,3652,3656,3659,3698,3702,3705,3710,3715,3718,3757,3761,3764,3813,3817,3843,3847,3850,3854,3857,3974,3978,3981,4018,4022,4025,4050,4054,4058,4061,4072,4076,4079,4090,4094,4105,4109,4123,4127,4130,4136,4139,4145],[1826,3649,3651],{"id":3650},"security-policy","Security Policy",[1904,3653,3655],{"id":3654},"supported-versions","Supported Versions",[1830,3657,3658],{},"We release patches for security vulnerabilities. Which versions are eligible for receiving such patches depends on the CVSS v3.0 Rating:",[3116,3660,3661,3674],{},[3119,3662,3663],{},[3122,3664,3665,3668,3671],{},[3125,3666,3667],{},"Version",[3125,3669,3670],{},"Supported",[3125,3672,3673],{},"Status",[3135,3675,3676,3687],{},[3122,3677,3678,3681,3684],{},[3140,3679,3680],{},"latest",[3140,3682,3683],{},"✅",[3140,3685,3686],{},"Active development",[3122,3688,3689,3692,3695],{},[3140,3690,3691],{},"\u003C latest",[3140,3693,3694],{},"❌",[3140,3696,3697],{},"Security fixes only for critical issues",[1904,3699,3701],{"id":3700},"reporting-a-vulnerability","Reporting a Vulnerability",[1830,3703,3704],{},"We take the security of soy seriously. If you have discovered a security vulnerability in this project, please report it responsibly.",[3706,3707,3709],"h3",{"id":3708},"how-to-report","How to Report",[1830,3711,3712],{},[3267,3713,3714],{},"Please DO NOT report security vulnerabilities through public GitHub issues.",[1830,3716,3717],{},"Instead, please report them via one of the following methods:",[3719,3720,3721,3744],"ol",{},[3264,3722,3723,3726,3727],{},[3267,3724,3725],{},"GitHub Security Advisories"," (Preferred)",[3261,3728,3729,3738,3741],{},[3264,3730,3731,3732,3737],{},"Go to the ",[1833,3733,3736],{"href":3734,"rel":3735},"https://github.com/zoobz-io/soy/security",[1837],"Security tab"," of this repository",[3264,3739,3740],{},"Click \"Report a vulnerability\"",[3264,3742,3743],{},"Fill out the form with details about the vulnerability",[3264,3745,3746,3749],{},[3267,3747,3748],{},"Email",[3261,3750,3751,3754],{},[3264,3752,3753],{},"Send details to the repository maintainer through GitHub profile contact information",[3264,3755,3756],{},"Use PGP encryption if possible for sensitive details",[3706,3758,3760],{"id":3759},"what-to-include","What to Include",[1830,3762,3763],{},"Please include the following information (as much as you can provide) to help us better understand the nature and scope of the possible issue:",[3261,3765,3766,3772,3778,3784,3790,3795,3801,3807],{},[3264,3767,3768,3771],{},[3267,3769,3770],{},"Type of issue"," (e.g., SQL injection, buffer overflow, access control bypass, etc.)",[3264,3773,3774,3777],{},[3267,3775,3776],{},"Full paths of source file(s)"," related to the manifestation of the issue",[3264,3779,3780,3783],{},[3267,3781,3782],{},"The location of the affected source code"," (tag/branch/commit or direct URL)",[3264,3785,3786,3789],{},[3267,3787,3788],{},"Any special configuration required"," to reproduce the issue",[3264,3791,3792,3789],{},[3267,3793,3794],{},"Step-by-step instructions",[3264,3796,3797,3800],{},[3267,3798,3799],{},"Proof-of-concept or exploit code"," (if possible)",[3264,3802,3803,3806],{},[3267,3804,3805],{},"Impact of the issue",", including how an attacker might exploit the issue",[3264,3808,3809,3812],{},[3267,3810,3811],{},"Your name and affiliation"," (optional)",[3706,3814,3816],{"id":3815},"what-to-expect","What to Expect",[3261,3818,3819,3825,3831,3837],{},[3264,3820,3821,3824],{},[3267,3822,3823],{},"Acknowledgment",": We will acknowledge receipt of your vulnerability report within 48 hours",[3264,3826,3827,3830],{},[3267,3828,3829],{},"Initial Assessment",": Within 7 days, we will provide an initial assessment of the report",[3264,3832,3833,3836],{},[3267,3834,3835],{},"Resolution Timeline",": We aim to resolve critical issues within 30 days",[3264,3838,3839,3842],{},[3267,3840,3841],{},"Disclosure",": We will coordinate with you on the disclosure timeline",[3706,3844,3846],{"id":3845},"preferred-languages","Preferred Languages",[1830,3848,3849],{},"We prefer all communications to be in English.",[1904,3851,3853],{"id":3852},"security-best-practices","Security Best Practices",[1830,3855,3856],{},"When using soy in your applications, we recommend:",[3719,3858,3859,3880,3893,3909,3924,3943,3959],{},[3264,3860,3861,3864],{},[3267,3862,3863],{},"Keep Dependencies Updated",[1909,3865,3867],{"className":2160,"code":3866,"language":2162,"meta":34,"style":34},"go get -u github.com/zoobz-io/soy\n",[1915,3868,3869],{"__ignoreMap":34},[1918,3870,3871,3873,3875,3878],{"class":1920,"line":9},[1918,3872,1913],{"class":2015},[1918,3874,2171],{"class":1947},[1918,3876,3877],{"class":1923}," -u",[1918,3879,2174],{"class":1947},[3264,3881,3882,3885],{},[3267,3883,3884],{},"Use Context Properly",[3261,3886,3887,3890],{},[3264,3888,3889],{},"Always pass contexts with appropriate timeouts",[3264,3891,3892],{},"Handle context cancellation in your queries",[3264,3894,3895,3898],{},[3267,3896,3897],{},"Input Validation",[3261,3899,3900,3903,3906],{},[3264,3901,3902],{},"Validate all user inputs before passing to queries",[3264,3904,3905],{},"Use parameterized queries (soy handles this automatically)",[3264,3907,3908],{},"Never construct raw SQL from user input",[3264,3910,3911,3913],{},[3267,3912,202],{},[3261,3914,3915,3918,3921],{},[3264,3916,3917],{},"Never expose internal error details to users",[3264,3919,3920],{},"Log errors securely without leaking sensitive data",[3264,3922,3923],{},"Implement proper fallback mechanisms",[3264,3925,3926,3929],{},[3267,3927,3928],{},"Database Security",[3261,3930,3931,3934,3937,3940],{},[3264,3932,3933],{},"Use least-privilege database accounts",[3264,3935,3936],{},"Enable SSL/TLS for database connections",[3264,3938,3939],{},"Rotate database credentials regularly",[3264,3941,3942],{},"Never commit credentials to version control",[3264,3944,3945,3948],{},[3267,3946,3947],{},"SQL Injection Protection",[3261,3949,3950,3953,3956],{},[3264,3951,3952],{},"soy uses parameterized queries via sqlx",[3264,3954,3955],{},"All user inputs are properly escaped",[3264,3957,3958],{},"ASTQL validates query structure at initialization",[3264,3960,3961,3963],{},[3267,3962,3166],{},[3261,3964,3965,3968,3971],{},[3264,3966,3967],{},"Schema validation happens at initialization via ASTQL",[3264,3969,3970],{},"Invalid queries fail fast before reaching the database",[3264,3972,3973],{},"Column and table names are validated against schema",[1904,3975,3977],{"id":3976},"security-features","Security Features",[1830,3979,3980],{},"soy includes several built-in security features:",[3261,3982,3983,3988,3994,4000,4006,4012],{},[3264,3984,3985,3987],{},[3267,3986,37],{},": Generic types prevent type confusion attacks",[3264,3989,3990,3993],{},[3267,3991,3992],{},"SQL Validation",": ASTQL validates all queries against schema",[3264,3995,3996,3999],{},[3267,3997,3998],{},"Parameterized Queries",": All values use SQL parameters, not string concatenation",[3264,4001,4002,4005],{},[3267,4003,4004],{},"Context Support",": Built-in timeout and cancellation support",[3264,4007,4008,4011],{},[3267,4009,4010],{},"Error Isolation",": Errors are properly wrapped without leaking sensitive data",[3264,4013,4014,4017],{},[3267,4015,4016],{},"Zero SQL Injection Risk",": Query structure is validated; values are parameterized",[1904,4019,4021],{"id":4020},"automated-security-scanning","Automated Security Scanning",[1830,4023,4024],{},"This project uses:",[3261,4026,4027,4032,4038,4044],{},[3264,4028,4029,4031],{},[3267,4030,1865],{},": GitHub's semantic code analysis for security vulnerabilities",[3264,4033,4034,4037],{},[3267,4035,4036],{},"Dependabot",": Automated dependency updates",[3264,4039,4040,4043],{},[3267,4041,4042],{},"golangci-lint",": Static analysis including security linters (gosec)",[3264,4045,4046,4049],{},[3267,4047,4048],{},"Codecov",": Coverage tracking to ensure security-critical code is tested",[1904,4051,4053],{"id":4052},"known-security-considerations","Known Security Considerations",[3706,4055,4057],{"id":4056},"sql-injection","SQL Injection",[1830,4059,4060],{},"soy is designed to prevent SQL injection by:",[3719,4062,4063,4066,4069],{},[3264,4064,4065],{},"Validating query structure at initialization",[3264,4067,4068],{},"Using parameterized queries for all values",[3264,4070,4071],{},"Never concatenating user input into SQL strings",[3706,4073,4075],{"id":4074},"access-control","Access Control",[1830,4077,4078],{},"soy does not implement access control - this is the responsibility of:",[3719,4080,4081,4084,4087],{},[3264,4082,4083],{},"Your application layer",[3264,4085,4086],{},"Database-level permissions",[3264,4088,4089],{},"Row-level security policies",[3706,4091,4093],{"id":4092},"data-exposure","Data Exposure",[3261,4095,4096,4099,4102],{},[3264,4097,4098],{},"Be careful with error messages in production",[3264,4100,4101],{},"Don't log sensitive query parameters",[3264,4103,4104],{},"Implement proper audit logging at application level",[1904,4106,4108],{"id":4107},"vulnerability-disclosure-policy","Vulnerability Disclosure Policy",[3261,4110,4111,4114,4117,4120],{},[3264,4112,4113],{},"Security vulnerabilities will be disclosed via GitHub Security Advisories",[3264,4115,4116],{},"We follow a 90-day disclosure timeline for non-critical issues",[3264,4118,4119],{},"Critical vulnerabilities may be disclosed sooner after patches are available",[3264,4121,4122],{},"We will credit reporters who follow responsible disclosure practices",[1904,4124,4126],{"id":4125},"credits","Credits",[1830,4128,4129],{},"We thank the following individuals for responsibly disclosing security issues:",[1830,4131,4132],{},[4133,4134,4135],"em",{},"This list is currently empty. Be the first to help improve our security!",[4137,4138],"hr",{},[1830,4140,4141,4144],{},[3267,4142,4143],{},"Last Updated",": 2025-11-04",[3622,4146,4147],{},"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 .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}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);}",{"title":34,"searchDepth":19,"depth":19,"links":4149},[4150,4151,4157,4158,4159,4160,4165,4166],{"id":3654,"depth":19,"text":3655},{"id":3700,"depth":19,"text":3701,"children":4152},[4153,4154,4155,4156],{"id":3708,"depth":40,"text":3709},{"id":3759,"depth":40,"text":3760},{"id":3815,"depth":40,"text":3816},{"id":3845,"depth":40,"text":3846},{"id":3852,"depth":19,"text":3853},{"id":3976,"depth":19,"text":3977},{"id":4020,"depth":19,"text":4021},{"id":4052,"depth":19,"text":4053,"children":4161},[4162,4163,4164],{"id":4056,"depth":40,"text":4057},{"id":4074,"depth":40,"text":4075},{"id":4092,"depth":40,"text":4093},{"id":4107,"depth":19,"text":4108},{"id":4125,"depth":19,"text":4126},"shield",{},"/security",{"title":3645,"description":34},"security","n0A6TLNNEylWzRhkd5llNzs_G-Jrv66kJZnNPSBtTMA",{"id":4174,"title":3603,"body":4175,"description":4183,"extension":3636,"icon":1915,"meta":4815,"navigation":1982,"path":4816,"seo":4817,"stem":3602,"__hash__":4818},"resources/contributing.md",{"type":1823,"value":4176,"toc":4788},[4177,4181,4184,4188,4191,4195,4233,4237,4241,4259,4263,4282,4284,4295,4299,4303,4317,4321,4332,4336,4341,4344,4358,4362,4365,4379,4383,4386,4400,4404,4432,4435,4438,4453,4456,4472,4475,4491,4494,4507,4511,4519,4523,4526,4570,4574,4578,4581,4592,4595,4609,4613,4616,4644,4648,4674,4678,4705,4711,4715,4718,4729,4732,4735,4764,4767,4771,4782,4785],[1826,4178,4180],{"id":4179},"contributing-to-soy","Contributing to soy",[1830,4182,4183],{},"Thank you for your interest in contributing to soy! This guide will help you get started.",[1904,4185,4187],{"id":4186},"code-of-conduct","Code of Conduct",[1830,4189,4190],{},"By participating in this project, you agree to maintain a respectful and inclusive environment for all contributors.",[1904,4192,4194],{"id":4193},"getting-started","Getting Started",[3719,4196,4197,4200,4206,4212,4215,4221,4224,4230],{},[3264,4198,4199],{},"Fork the repository",[3264,4201,4202,4203],{},"Clone your fork: ",[1915,4204,4205],{},"git clone https://github.com/yourusername/soy.git",[3264,4207,4208,4209],{},"Create a feature branch: ",[1915,4210,4211],{},"git checkout -b feature/your-feature-name",[3264,4213,4214],{},"Make your changes",[3264,4216,4217,4218],{},"Run tests: ",[1915,4219,4220],{},"go test ./...",[3264,4222,4223],{},"Commit your changes with a descriptive message",[3264,4225,4226,4227],{},"Push to your fork: ",[1915,4228,4229],{},"git push origin feature/your-feature-name",[3264,4231,4232],{},"Create a Pull Request",[1904,4234,4236],{"id":4235},"development-guidelines","Development Guidelines",[3706,4238,4240],{"id":4239},"code-style","Code Style",[3261,4242,4243,4246,4253,4256],{},[3264,4244,4245],{},"Follow standard Go conventions",[3264,4247,4248,4249,4252],{},"Run ",[1915,4250,4251],{},"go fmt"," before committing",[3264,4254,4255],{},"Add comments for exported functions and types",[3264,4257,4258],{},"Keep functions small and focused",[3706,4260,4262],{"id":4261},"testing","Testing",[3261,4264,4265,4268,4273,4276,4279],{},[3264,4266,4267],{},"Write tests for new functionality",[3264,4269,4270,4271],{},"Ensure all tests pass: ",[1915,4272,4220],{},[3264,4274,4275],{},"Include benchmarks for performance-critical code",[3264,4277,4278],{},"Aim for good test coverage",[3264,4280,4281],{},"Test with PostgreSQL for database operations",[3706,4283,3513],{"id":3512},[3261,4285,4286,4289,4292],{},[3264,4287,4288],{},"Update documentation for API changes",[3264,4290,4291],{},"Add examples for new features",[3264,4293,4294],{},"Keep doc comments clear and concise",[1904,4296,4298],{"id":4297},"types-of-contributions","Types of Contributions",[3706,4300,4302],{"id":4301},"bug-reports","Bug Reports",[3261,4304,4305,4308,4311,4314],{},[3264,4306,4307],{},"Use GitHub Issues",[3264,4309,4310],{},"Include minimal reproduction code",[3264,4312,4313],{},"Describe expected vs actual behavior",[3264,4315,4316],{},"Include Go version, database version, and OS",[3706,4318,4320],{"id":4319},"feature-requests","Feature Requests",[3261,4322,4323,4326,4329],{},[3264,4324,4325],{},"Open an issue for discussion first",[3264,4327,4328],{},"Explain the use case",[3264,4330,4331],{},"Consider backwards compatibility",[3706,4333,4335],{"id":4334},"code-contributions","Code Contributions",[4337,4338,4340],"h4",{"id":4339},"adding-query-builders","Adding Query Builders",[1830,4342,4343],{},"New query builders should:",[3261,4345,4346,4349,4352,4355],{},[3264,4347,4348],{},"Follow the existing pattern (Select, Create, Update, Delete)",[3264,4350,4351],{},"Include comprehensive tests",[3264,4353,4354],{},"Add documentation with examples",[3264,4356,4357],{},"Validate queries using ASTQL",[4337,4359,4361],{"id":4360},"adding-schema-features","Adding Schema Features",[1830,4363,4364],{},"New schema features should:",[3261,4366,4367,4370,4373,4376],{},[3264,4368,4369],{},"Support struct tag syntax",[3264,4371,4372],{},"Generate valid DBML",[3264,4374,4375],{},"Work with ASTQL validation",[3264,4377,4378],{},"Include tests for edge cases",[4337,4380,4382],{"id":4381},"database-support","Database Support",[1830,4384,4385],{},"Adding support for new databases:",[3261,4387,4388,4391,4394,4397],{},[3264,4389,4390],{},"Implement dialect-specific SQL generation",[3264,4392,4393],{},"Handle type mapping correctly",[3264,4395,4396],{},"Test against actual database instance",[3264,4398,4399],{},"Document driver requirements",[1904,4401,4403],{"id":4402},"pull-request-process","Pull Request Process",[3719,4405,4406,4412,4417,4422,4427],{},[3264,4407,4408,4411],{},[3267,4409,4410],{},"Keep PRs focused"," - One feature/fix per PR",[3264,4413,4414],{},[3267,4415,4416],{},"Write descriptive commit messages",[3264,4418,4419],{},[3267,4420,4421],{},"Update tests and documentation",[3264,4423,4424],{},[3267,4425,4426],{},"Ensure CI passes",[3264,4428,4429],{},[3267,4430,4431],{},"Respond to review feedback",[1904,4433,4262],{"id":4434},"testing-1",[1830,4436,4437],{},"Run the full test suite:",[1909,4439,4441],{"className":2160,"code":4440,"language":2162,"meta":34,"style":34},"go test ./...\n",[1915,4442,4443],{"__ignoreMap":34},[1918,4444,4445,4447,4450],{"class":1920,"line":9},[1918,4446,1913],{"class":2015},[1918,4448,4449],{"class":1947}," test",[1918,4451,4452],{"class":1947}," ./...\n",[1830,4454,4455],{},"Run with race detection:",[1909,4457,4459],{"className":2160,"code":4458,"language":2162,"meta":34,"style":34},"go test -race ./...\n",[1915,4460,4461],{"__ignoreMap":34},[1918,4462,4463,4465,4467,4470],{"class":1920,"line":9},[1918,4464,1913],{"class":2015},[1918,4466,4449],{"class":1947},[1918,4468,4469],{"class":1923}," -race",[1918,4471,4452],{"class":1947},[1830,4473,4474],{},"Run benchmarks:",[1909,4476,4478],{"className":2160,"code":4477,"language":2162,"meta":34,"style":34},"go test -bench=. ./...\n",[1915,4479,4480],{"__ignoreMap":34},[1918,4481,4482,4484,4486,4489],{"class":1920,"line":9},[1918,4483,1913],{"class":2015},[1918,4485,4449],{"class":1947},[1918,4487,4488],{"class":1923}," -bench=.",[1918,4490,4452],{"class":1947},[1830,4492,4493],{},"Generate coverage:",[1909,4495,4497],{"className":2160,"code":4496,"language":2162,"meta":34,"style":34},"make coverage\n",[1915,4498,4499],{"__ignoreMap":34},[1918,4500,4501,4504],{"class":1920,"line":9},[1918,4502,4503],{"class":2015},"make",[1918,4505,4506],{"class":1947}," coverage\n",[1904,4508,4510],{"id":4509},"project-structure","Project Structure",[1909,4512,4517],{"className":4513,"code":4515,"language":4516},[4514],"language-text","soy/\n├── *.go              # Core library files\n├── *_test.go         # Tests\n├── go.mod            # Module dependencies\n└── README.md         # Documentation\n","text",[1915,4518,4515],{"__ignoreMap":34},[1904,4520,4522],{"id":4521},"commit-messages","Commit Messages",[1830,4524,4525],{},"Follow conventional commits:",[3261,4527,4528,4534,4540,4546,4552,4558,4564],{},[3264,4529,4530,4533],{},[1915,4531,4532],{},"feat:"," New feature",[3264,4535,4536,4539],{},[1915,4537,4538],{},"fix:"," Bug fix",[3264,4541,4542,4545],{},[1915,4543,4544],{},"docs:"," Documentation changes",[3264,4547,4548,4551],{},[1915,4549,4550],{},"test:"," Test additions/changes",[3264,4553,4554,4557],{},[1915,4555,4556],{},"refactor:"," Code refactoring",[3264,4559,4560,4563],{},[1915,4561,4562],{},"perf:"," Performance improvements",[3264,4565,4566,4569],{},[1915,4567,4568],{},"chore:"," Maintenance tasks",[1904,4571,4573],{"id":4572},"release-process","Release Process",[3706,4575,4577],{"id":4576},"automated-releases","Automated Releases",[1830,4579,4580],{},"This project uses automated release versioning. To create a release:",[3719,4582,4583,4586,4589],{},[3264,4584,4585],{},"Go to Actions → Release → Run workflow",[3264,4587,4588],{},"Leave \"Version override\" empty for automatic version inference",[3264,4590,4591],{},"Click \"Run workflow\"",[1830,4593,4594],{},"The system will:",[3261,4596,4597,4600,4603,4606],{},[3264,4598,4599],{},"Automatically determine the next version from conventional commits",[3264,4601,4602],{},"Create a git tag",[3264,4604,4605],{},"Generate release notes via GoReleaser",[3264,4607,4608],{},"Publish the release to GitHub",[3706,4610,4612],{"id":4611},"manual-release-legacy","Manual Release (Legacy)",[1830,4614,4615],{},"You can still create releases manually:",[1909,4617,4619],{"className":2160,"code":4618,"language":2162,"meta":34,"style":34},"git tag v1.2.3\ngit push origin v1.2.3\n",[1915,4620,4621,4632],{"__ignoreMap":34},[1918,4622,4623,4626,4629],{"class":1920,"line":9},[1918,4624,4625],{"class":2015},"git",[1918,4627,4628],{"class":1947}," tag",[1918,4630,4631],{"class":1947}," v1.2.3\n",[1918,4633,4634,4636,4639,4642],{"class":1920,"line":19},[1918,4635,4625],{"class":2015},[1918,4637,4638],{"class":1947}," push",[1918,4640,4641],{"class":1947}," origin",[1918,4643,4631],{"class":1947},[3706,4645,4647],{"id":4646},"known-limitations","Known Limitations",[3261,4649,4650,4656,4662],{},[3264,4651,4652,4655],{},[3267,4653,4654],{},"Protected branches",": The automated release cannot bypass branch protection rules. This is by design for security.",[3264,4657,4658,4661],{},[3267,4659,4660],{},"Concurrent releases",": Rapid successive releases may fail. Simply retry after a moment.",[3264,4663,4664,4667,4668,4670,4671,4673],{},[3267,4665,4666],{},"Conventional commits required",": Version inference requires conventional commit format (",[1915,4669,4532],{},", ",[1915,4672,4538],{},", etc.)",[3706,4675,4677],{"id":4676},"commit-conventions-for-versioning","Commit Conventions for Versioning",[3261,4679,4680,4685,4690,4696],{},[3264,4681,4682,4684],{},[1915,4683,4532],{}," new features (minor version: 1.2.0 → 1.3.0)",[3264,4686,4687,4689],{},[1915,4688,4538],{}," bug fixes (patch version: 1.2.0 → 1.2.1)",[3264,4691,4692,4695],{},[1915,4693,4694],{},"feat!:"," breaking changes (major version: 1.2.0 → 2.0.0)",[3264,4697,4698,4670,4700,4670,4702,4704],{},[1915,4699,4544],{},[1915,4701,4550],{},[1915,4703,4568],{}," no version change",[1830,4706,4707,4708],{},"Example: ",[1915,4709,4710],{},"feat(query): add support for JOIN operations",[3706,4712,4714],{"id":4713},"version-preview-on-pull-requests","Version Preview on Pull Requests",[1830,4716,4717],{},"Every PR automatically shows the next version that will be created:",[3261,4719,4720,4723,4726],{},[3264,4721,4722],{},"Check PR comments for \"Version Preview\"",[3264,4724,4725],{},"Updates automatically as you add commits",[3264,4727,4728],{},"Helps verify your commits have the intended effect",[1904,4730,240],{"id":4731},"dependencies",[1830,4733,4734],{},"Soy builds on:",[3261,4736,4737,4744,4750,4756],{},[3264,4738,4739,4743],{},[1833,4740,254],{"href":4741,"rel":4742},"https://github.com/jmoiron/sqlx",[1837]," - Database operations",[3264,4745,4746,4749],{},[1833,4747,244],{"href":3326,"rel":4748},[1837]," - Type metadata extraction",[3264,4751,4752,4755],{},[1833,4753,249],{"href":3186,"rel":4754},[1837]," - SQL validation",[3264,4757,4758,4763],{},[1833,4759,4762],{"href":4760,"rel":4761},"https://github.com/zoobz-io/dbml",[1837],"DBML"," - Schema representation",[1830,4765,4766],{},"When contributing, respect the boundaries between these layers.",[1904,4768,4770],{"id":4769},"questions","Questions?",[3261,4772,4773,4776,4779],{},[3264,4774,4775],{},"Open an issue for questions",[3264,4777,4778],{},"Check existing issues first",[3264,4780,4781],{},"Be patient and respectful",[1830,4783,4784],{},"Thank you for contributing to soy!",[3622,4786,4787],{},"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 .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)}",{"title":34,"searchDepth":19,"depth":19,"links":4789},[4790,4791,4792,4797,4802,4803,4804,4805,4806,4813,4814],{"id":4186,"depth":19,"text":4187},{"id":4193,"depth":19,"text":4194},{"id":4235,"depth":19,"text":4236,"children":4793},[4794,4795,4796],{"id":4239,"depth":40,"text":4240},{"id":4261,"depth":40,"text":4262},{"id":3512,"depth":40,"text":3513},{"id":4297,"depth":19,"text":4298,"children":4798},[4799,4800,4801],{"id":4301,"depth":40,"text":4302},{"id":4319,"depth":40,"text":4320},{"id":4334,"depth":40,"text":4335},{"id":4402,"depth":19,"text":4403},{"id":4434,"depth":19,"text":4262},{"id":4509,"depth":19,"text":4510},{"id":4521,"depth":19,"text":4522},{"id":4572,"depth":19,"text":4573,"children":4807},[4808,4809,4810,4811,4812],{"id":4576,"depth":40,"text":4577},{"id":4611,"depth":40,"text":4612},{"id":4646,"depth":40,"text":4647},{"id":4676,"depth":40,"text":4677},{"id":4713,"depth":40,"text":4714},{"id":4731,"depth":19,"text":240},{"id":4769,"depth":19,"text":4770},{},"/contributing",{"title":3603,"description":4183},"mRnt18yUvH39xHJ4QTZRGFu-0uWVJlYbHobYFBfE1Fg",{"id":4820,"title":217,"author":4821,"body":4822,"description":820,"extension":3636,"meta":5845,"navigation":1982,"path":818,"published":5846,"readtime":5847,"seo":5848,"stem":1777,"tags":5849,"updated":5846,"__hash__":5853},"soy/v1.0.8/3.guides/6.lifecycle.md","zoobzio",{"type":1823,"value":4823,"toc":5831},[4824,4827,4834,4837,4846,4934,4937,5065,5068,5071,5224,5227,5230,5239,5357,5360,5412,5418,5421,5424,5562,5565,5568,5571,5586,5746,5749,5756,5795,5798,5800,5803,5828],[1826,4825,217],{"id":4826},"lifecycle-callbacks",[1830,4828,4829,4830,4833],{},"Soy supports two optional callbacks on ",[1915,4831,4832],{},"Soy[T]"," that fire during builder execution paths. They enable cross-cutting concerns — validation, normalisation, enrichment — without wrapping every builder type.",[1904,4835,827],{"id":4836},"onscan",[1830,4838,4839,4840,4842,4843,2138],{},"Fires after a row is scanned into ",[1915,4841,3148],{},". Registered with ",[1915,4844,4845],{},"OnScan()",[1909,4847,4849],{"className":1911,"code":4848,"language":1913,"meta":34,"style":34},"users.OnScan(func(ctx context.Context, result *User) error {\n    result.Email = strings.ToLower(result.Email)\n    return nil\n})\n",[1915,4850,4851,4891,4922,4930],{"__ignoreMap":34},[1918,4852,4853,4855,4857,4859,4861,4863,4865,4867,4869,4871,4874,4876,4879,4882,4884,4886,4889],{"class":1920,"line":9},[1918,4854,1997],{"class":1996},[1918,4856,2012],{"class":1934},[1918,4858,827],{"class":2015},[1918,4860,2086],{"class":1934},[1918,4862,2324],{"class":1923},[1918,4864,2086],{"class":1934},[1918,4866,2113],{"class":2236},[1918,4868,2444],{"class":1927},[1918,4870,2012],{"class":1934},[1918,4872,4873],{"class":1927},"Context",[1918,4875,2000],{"class":1934},[1918,4877,4878],{"class":2236}," result",[1918,4880,4881],{"class":2370}," *",[1918,4883,2021],{"class":1927},[1918,4885,3503],{"class":1934},[1918,4887,4888],{"class":1927}," error",[1918,4890,1935],{"class":1934},[1918,4892,4893,4896,4898,4900,4903,4906,4908,4911,4913,4916,4918,4920],{"class":1920,"line":19},[1918,4894,4895],{"class":1996},"    result",[1918,4897,2012],{"class":1934},[1918,4899,3748],{"class":1996},[1918,4901,4902],{"class":1996}," =",[1918,4904,4905],{"class":1996}," strings",[1918,4907,2012],{"class":1934},[1918,4909,4910],{"class":2015},"ToLower",[1918,4912,2086],{"class":1934},[1918,4914,4915],{"class":1996},"result",[1918,4917,2012],{"class":1934},[1918,4919,3748],{"class":1996},[1918,4921,2255],{"class":1934},[1918,4923,4924,4927],{"class":1920,"line":40},[1918,4925,4926],{"class":2370},"    return",[1918,4928,4929],{"class":1923}," nil\n",[1918,4931,4932],{"class":1920,"line":1026},[1918,4933,2144],{"class":1934},[3706,4935,832],{"id":4936},"where-it-fires",[3116,4938,4939,4952],{},[3119,4940,4941],{},[3122,4942,4943,4946,4949],{},[3125,4944,4945],{},"Builder",[3125,4947,4948],{},"Method",[3125,4950,4951],{},"Fires?",[3135,4953,4954,4967,4980,4994,5007,5019,5037,5053],{},[3122,4955,4956,4958,4964],{},[3140,4957,1023],{},[3140,4959,4960,4670,4962],{},[1915,4961,1257],{},[1915,4963,1262],{},[3140,4965,4966],{},"Once (single row)",[3122,4968,4969,4971,4977],{},[3140,4970,1029],{},[3140,4972,4973,4670,4975],{},[1915,4974,1257],{},[1915,4976,1262],{},[3140,4978,4979],{},"Per row",[3122,4981,4982,4985,4991],{},[3140,4983,4984],{},"Create",[3140,4986,4987,4670,4989],{},[1915,4988,1257],{},[1915,4990,1262],{},[3140,4992,4993],{},"Once (RETURNING row)",[3122,4995,4996,4999,5005],{},[3140,4997,4998],{},"Update",[3140,5000,5001,4670,5003],{},[1915,5002,1257],{},[1915,5004,1262],{},[3140,5006,4993],{},[3122,5008,5009,5011,5017],{},[3140,5010,3243],{},[3140,5012,5013,4670,5015],{},[1915,5014,1257],{},[1915,5016,1262],{},[3140,5018,4979],{},[3122,5020,5021,5024,5030],{},[3140,5022,5023],{},"Delete",[3140,5025,5026,4670,5028],{},[1915,5027,1257],{},[1915,5029,1262],{},[3140,5031,5032,5033,5036],{},"No (",[1915,5034,5035],{},"int64"," return)",[3122,5038,5039,5042,5048],{},[3140,5040,5041],{},"Aggregate",[3140,5043,5044,4670,5046],{},[1915,5045,1257],{},[1915,5047,1262],{},[3140,5049,5032,5050,5036],{},[1915,5051,5052],{},"float64",[3122,5054,5055,5058,5062],{},[3140,5056,5057],{},"Create batch",[3140,5059,5060],{},[1915,5061,1434],{},[3140,5063,5064],{},"No (no scan)",[3706,5066,837],{"id":5067},"error-handling",[1830,5069,5070],{},"Returning an error aborts the operation:",[1909,5072,5074],{"className":1911,"code":5073,"language":1913,"meta":34,"style":34},"users.OnScan(func(ctx context.Context, result *User) error {\n    if result.Status == \"banned\" {\n        return errors.New(\"banned user\")\n    }\n    return nil\n})\n\nuser, err := users.Select().Where(\"id\", \"=\", \"id\").Exec(ctx, params)\n// err wraps \"banned user\" if the scanned record is banned\n",[1915,5075,5076,5112,5131,5150,5155,5161,5165,5169,5219],{"__ignoreMap":34},[1918,5077,5078,5080,5082,5084,5086,5088,5090,5092,5094,5096,5098,5100,5102,5104,5106,5108,5110],{"class":1920,"line":9},[1918,5079,1997],{"class":1996},[1918,5081,2012],{"class":1934},[1918,5083,827],{"class":2015},[1918,5085,2086],{"class":1934},[1918,5087,2324],{"class":1923},[1918,5089,2086],{"class":1934},[1918,5091,2113],{"class":2236},[1918,5093,2444],{"class":1927},[1918,5095,2012],{"class":1934},[1918,5097,4873],{"class":1927},[1918,5099,2000],{"class":1934},[1918,5101,4878],{"class":2236},[1918,5103,4881],{"class":2370},[1918,5105,2021],{"class":1927},[1918,5107,3503],{"class":1934},[1918,5109,4888],{"class":1927},[1918,5111,1935],{"class":1934},[1918,5113,5114,5117,5119,5121,5123,5126,5129],{"class":1920,"line":19},[1918,5115,5116],{"class":2370},"    if",[1918,5118,4878],{"class":1996},[1918,5120,2012],{"class":1934},[1918,5122,3673],{"class":1996},[1918,5124,5125],{"class":2370}," ==",[1918,5127,5128],{"class":1947}," \"banned\"",[1918,5130,1935],{"class":1934},[1918,5132,5133,5136,5139,5141,5143,5145,5148],{"class":1920,"line":40},[1918,5134,5135],{"class":2370},"        return",[1918,5137,5138],{"class":1996}," errors",[1918,5140,2012],{"class":1934},[1918,5142,1014],{"class":2015},[1918,5144,2086],{"class":1934},[1918,5146,5147],{"class":1947},"\"banned user\"",[1918,5149,2255],{"class":1934},[1918,5151,5152],{"class":1920,"line":1026},[1918,5153,5154],{"class":1934},"    }\n",[1918,5156,5157,5159],{"class":1920,"line":1973},[1918,5158,4926],{"class":2370},[1918,5160,4929],{"class":1923},[1918,5162,5163],{"class":1920,"line":1979},[1918,5164,2144],{"class":1934},[1918,5166,5167],{"class":1920,"line":1986},[1918,5168,1983],{"emptyLinePlaceholder":1982},[1918,5170,5171,5173,5175,5178,5180,5182,5184,5186,5188,5190,5192,5194,5196,5198,5200,5203,5206,5208,5210,5212,5214,5217],{"class":1920,"line":1993},[1918,5172,2061],{"class":1996},[1918,5174,2000],{"class":1934},[1918,5176,5177],{"class":1996}," err",[1918,5179,2006],{"class":1996},[1918,5181,2070],{"class":1996},[1918,5183,2012],{"class":1934},[1918,5185,1023],{"class":2015},[1918,5187,2483],{"class":1934},[1918,5189,1157],{"class":2015},[1918,5191,2086],{"class":1934},[1918,5193,2909],{"class":1947},[1918,5195,2000],{"class":1934},[1918,5197,2094],{"class":1947},[1918,5199,2000],{"class":1934},[1918,5201,5202],{"class":1947}," \"id\"",[1918,5204,5205],{"class":1934},").",[1918,5207,1257],{"class":2015},[1918,5209,2086],{"class":1934},[1918,5211,2113],{"class":1996},[1918,5213,2000],{"class":1934},[1918,5215,5216],{"class":1996}," params",[1918,5218,2255],{"class":1934},[1918,5220,5221],{"class":1920,"line":2047},[1918,5222,5223],{"class":1989},"// err wraps \"banned user\" if the scanned record is banned\n",[1830,5225,5226],{},"For Query and Compound, an error on any row aborts the entire result.",[1904,5228,842],{"id":5229},"onrecord",[1830,5231,5232,5233,5235,5236,2138],{},"Fires before a ",[1915,5234,3148],{}," is written. Registered with ",[1915,5237,5238],{},"OnRecord()",[1909,5240,5242],{"className":1911,"code":5241,"language":1913,"meta":34,"style":34},"users.OnRecord(func(ctx context.Context, record *User) error {\n    if record.Email == \"\" {\n        return errors.New(\"email is required\")\n    }\n    record.Email = strings.ToLower(record.Email)\n    return nil\n})\n",[1915,5243,5244,5281,5298,5315,5319,5347,5353],{"__ignoreMap":34},[1918,5245,5246,5248,5250,5252,5254,5256,5258,5260,5262,5264,5266,5268,5271,5273,5275,5277,5279],{"class":1920,"line":9},[1918,5247,1997],{"class":1996},[1918,5249,2012],{"class":1934},[1918,5251,842],{"class":2015},[1918,5253,2086],{"class":1934},[1918,5255,2324],{"class":1923},[1918,5257,2086],{"class":1934},[1918,5259,2113],{"class":2236},[1918,5261,2444],{"class":1927},[1918,5263,2012],{"class":1934},[1918,5265,4873],{"class":1927},[1918,5267,2000],{"class":1934},[1918,5269,5270],{"class":2236}," record",[1918,5272,4881],{"class":2370},[1918,5274,2021],{"class":1927},[1918,5276,3503],{"class":1934},[1918,5278,4888],{"class":1927},[1918,5280,1935],{"class":1934},[1918,5282,5283,5285,5287,5289,5291,5293,5296],{"class":1920,"line":19},[1918,5284,5116],{"class":2370},[1918,5286,5270],{"class":1996},[1918,5288,2012],{"class":1934},[1918,5290,3748],{"class":1996},[1918,5292,5125],{"class":2370},[1918,5294,5295],{"class":1947}," \"\"",[1918,5297,1935],{"class":1934},[1918,5299,5300,5302,5304,5306,5308,5310,5313],{"class":1920,"line":40},[1918,5301,5135],{"class":2370},[1918,5303,5138],{"class":1996},[1918,5305,2012],{"class":1934},[1918,5307,1014],{"class":2015},[1918,5309,2086],{"class":1934},[1918,5311,5312],{"class":1947},"\"email is required\"",[1918,5314,2255],{"class":1934},[1918,5316,5317],{"class":1920,"line":1026},[1918,5318,5154],{"class":1934},[1918,5320,5321,5324,5326,5328,5330,5332,5334,5336,5338,5341,5343,5345],{"class":1920,"line":1973},[1918,5322,5323],{"class":1996},"    record",[1918,5325,2012],{"class":1934},[1918,5327,3748],{"class":1996},[1918,5329,4902],{"class":1996},[1918,5331,4905],{"class":1996},[1918,5333,2012],{"class":1934},[1918,5335,4910],{"class":2015},[1918,5337,2086],{"class":1934},[1918,5339,5340],{"class":1996},"record",[1918,5342,2012],{"class":1934},[1918,5344,3748],{"class":1996},[1918,5346,2255],{"class":1934},[1918,5348,5349,5351],{"class":1920,"line":1979},[1918,5350,4926],{"class":2370},[1918,5352,4929],{"class":1923},[1918,5354,5355],{"class":1920,"line":1986},[1918,5356,2144],{"class":1934},[3706,5358,832],{"id":5359},"where-it-fires-1",[3116,5361,5362,5372],{},[3119,5363,5364],{},[3122,5365,5366,5368,5370],{},[3125,5367,4945],{},[3125,5369,4948],{},[3125,5371,4951],{},[3135,5373,5374,5387,5398],{},[3122,5375,5376,5378,5384],{},[3140,5377,4984],{},[3140,5379,5380,4670,5382],{},[1915,5381,1257],{},[1915,5383,1262],{},[3140,5385,5386],{},"Once (before INSERT)",[3122,5388,5389,5391,5395],{},[3140,5390,4984],{},[3140,5392,5393],{},[1915,5394,1434],{},[3140,5396,5397],{},"Per record (before INSERT)",[3122,5399,5400,5403,5409],{},[3140,5401,5402],{},"Create (upsert)",[3140,5404,5405,4670,5407],{},[1915,5406,1257],{},[1915,5408,1262],{},[3140,5410,5411],{},"Once (before write)",[1830,5413,5414,5415,5417],{},"OnRecord does not fire on Update or Delete because those operations take parameter maps, not ",[1915,5416,3148],{}," records.",[3706,5419,837],{"id":5420},"error-handling-1",[1830,5422,5423],{},"Returning an error aborts the write:",[1909,5425,5427],{"className":1911,"code":5426,"language":1913,"meta":34,"style":34},"users.OnRecord(func(ctx context.Context, record *User) error {\n    if record.Email == \"\" {\n        return errors.New(\"email is required\")\n    }\n    return nil\n})\n\n_, err := users.Insert().Exec(ctx, &User{Name: \"No Email\"})\n// err wraps \"email is required\"\n",[1915,5428,5429,5465,5481,5497,5501,5507,5511,5515,5557],{"__ignoreMap":34},[1918,5430,5431,5433,5435,5437,5439,5441,5443,5445,5447,5449,5451,5453,5455,5457,5459,5461,5463],{"class":1920,"line":9},[1918,5432,1997],{"class":1996},[1918,5434,2012],{"class":1934},[1918,5436,842],{"class":2015},[1918,5438,2086],{"class":1934},[1918,5440,2324],{"class":1923},[1918,5442,2086],{"class":1934},[1918,5444,2113],{"class":2236},[1918,5446,2444],{"class":1927},[1918,5448,2012],{"class":1934},[1918,5450,4873],{"class":1927},[1918,5452,2000],{"class":1934},[1918,5454,5270],{"class":2236},[1918,5456,4881],{"class":2370},[1918,5458,2021],{"class":1927},[1918,5460,3503],{"class":1934},[1918,5462,4888],{"class":1927},[1918,5464,1935],{"class":1934},[1918,5466,5467,5469,5471,5473,5475,5477,5479],{"class":1920,"line":19},[1918,5468,5116],{"class":2370},[1918,5470,5270],{"class":1996},[1918,5472,2012],{"class":1934},[1918,5474,3748],{"class":1996},[1918,5476,5125],{"class":2370},[1918,5478,5295],{"class":1947},[1918,5480,1935],{"class":1934},[1918,5482,5483,5485,5487,5489,5491,5493,5495],{"class":1920,"line":40},[1918,5484,5135],{"class":2370},[1918,5486,5138],{"class":1996},[1918,5488,2012],{"class":1934},[1918,5490,1014],{"class":2015},[1918,5492,2086],{"class":1934},[1918,5494,5312],{"class":1947},[1918,5496,2255],{"class":1934},[1918,5498,5499],{"class":1920,"line":1026},[1918,5500,5154],{"class":1934},[1918,5502,5503,5505],{"class":1920,"line":1973},[1918,5504,4926],{"class":2370},[1918,5506,4929],{"class":1923},[1918,5508,5509],{"class":1920,"line":1979},[1918,5510,2144],{"class":1934},[1918,5512,5513],{"class":1920,"line":1986},[1918,5514,1983],{"emptyLinePlaceholder":1982},[1918,5516,5517,5520,5522,5524,5526,5528,5530,5532,5534,5536,5538,5540,5542,5544,5546,5548,5550,5552,5555],{"class":1920,"line":1993},[1918,5518,5519],{"class":1996},"_",[1918,5521,2000],{"class":1934},[1918,5523,5177],{"class":1996},[1918,5525,2006],{"class":1996},[1918,5527,2070],{"class":1996},[1918,5529,2012],{"class":1934},[1918,5531,1034],{"class":2015},[1918,5533,2483],{"class":1934},[1918,5535,1257],{"class":2015},[1918,5537,2086],{"class":1934},[1918,5539,2113],{"class":1996},[1918,5541,2000],{"class":1934},[1918,5543,2494],{"class":2370},[1918,5545,2021],{"class":1927},[1918,5547,2132],{"class":1934},[1918,5549,2698],{"class":1940},[1918,5551,2138],{"class":1934},[1918,5553,5554],{"class":1947}," \"No Email\"",[1918,5556,2144],{"class":1934},[1918,5558,5559],{"class":1920,"line":2047},[1918,5560,5561],{"class":1989},"// err wraps \"email is required\"\n",[1830,5563,5564],{},"For batch inserts, an error on any record aborts the entire batch.",[1904,5566,855],{"id":5567},"combining-callbacks",[1830,5569,5570],{},"Both callbacks can be registered simultaneously. On an Insert with RETURNING, the order is:",[3719,5572,5573,5578,5581],{},[3264,5574,5575,5577],{},[1915,5576,842],{}," fires (before INSERT)",[3264,5579,5580],{},"INSERT executes",[3264,5582,5583,5585],{},[1915,5584,827],{}," fires (after RETURNING row is scanned)",[1909,5587,5589],{"className":1911,"code":5588,"language":1913,"meta":34,"style":34},"users.OnRecord(func(ctx context.Context, record *User) error {\n    record.Email = strings.ToLower(record.Email)\n    return nil\n})\n\nusers.OnScan(func(ctx context.Context, result *User) error {\n    result.FullName = result.FirstName + \" \" + result.LastName\n    return nil\n})\n",[1915,5590,5591,5627,5653,5659,5663,5667,5703,5736,5742],{"__ignoreMap":34},[1918,5592,5593,5595,5597,5599,5601,5603,5605,5607,5609,5611,5613,5615,5617,5619,5621,5623,5625],{"class":1920,"line":9},[1918,5594,1997],{"class":1996},[1918,5596,2012],{"class":1934},[1918,5598,842],{"class":2015},[1918,5600,2086],{"class":1934},[1918,5602,2324],{"class":1923},[1918,5604,2086],{"class":1934},[1918,5606,2113],{"class":2236},[1918,5608,2444],{"class":1927},[1918,5610,2012],{"class":1934},[1918,5612,4873],{"class":1927},[1918,5614,2000],{"class":1934},[1918,5616,5270],{"class":2236},[1918,5618,4881],{"class":2370},[1918,5620,2021],{"class":1927},[1918,5622,3503],{"class":1934},[1918,5624,4888],{"class":1927},[1918,5626,1935],{"class":1934},[1918,5628,5629,5631,5633,5635,5637,5639,5641,5643,5645,5647,5649,5651],{"class":1920,"line":19},[1918,5630,5323],{"class":1996},[1918,5632,2012],{"class":1934},[1918,5634,3748],{"class":1996},[1918,5636,4902],{"class":1996},[1918,5638,4905],{"class":1996},[1918,5640,2012],{"class":1934},[1918,5642,4910],{"class":2015},[1918,5644,2086],{"class":1934},[1918,5646,5340],{"class":1996},[1918,5648,2012],{"class":1934},[1918,5650,3748],{"class":1996},[1918,5652,2255],{"class":1934},[1918,5654,5655,5657],{"class":1920,"line":40},[1918,5656,4926],{"class":2370},[1918,5658,4929],{"class":1923},[1918,5660,5661],{"class":1920,"line":1026},[1918,5662,2144],{"class":1934},[1918,5664,5665],{"class":1920,"line":1973},[1918,5666,1983],{"emptyLinePlaceholder":1982},[1918,5668,5669,5671,5673,5675,5677,5679,5681,5683,5685,5687,5689,5691,5693,5695,5697,5699,5701],{"class":1920,"line":1979},[1918,5670,1997],{"class":1996},[1918,5672,2012],{"class":1934},[1918,5674,827],{"class":2015},[1918,5676,2086],{"class":1934},[1918,5678,2324],{"class":1923},[1918,5680,2086],{"class":1934},[1918,5682,2113],{"class":2236},[1918,5684,2444],{"class":1927},[1918,5686,2012],{"class":1934},[1918,5688,4873],{"class":1927},[1918,5690,2000],{"class":1934},[1918,5692,4878],{"class":2236},[1918,5694,4881],{"class":2370},[1918,5696,2021],{"class":1927},[1918,5698,3503],{"class":1934},[1918,5700,4888],{"class":1927},[1918,5702,1935],{"class":1934},[1918,5704,5705,5707,5709,5712,5714,5716,5718,5721,5724,5727,5729,5731,5733],{"class":1920,"line":1986},[1918,5706,4895],{"class":1996},[1918,5708,2012],{"class":1934},[1918,5710,5711],{"class":1996},"FullName",[1918,5713,4902],{"class":1996},[1918,5715,4878],{"class":1996},[1918,5717,2012],{"class":1934},[1918,5719,5720],{"class":1996},"FirstName",[1918,5722,5723],{"class":1996}," +",[1918,5725,5726],{"class":1947}," \" \"",[1918,5728,5723],{"class":1996},[1918,5730,4878],{"class":1996},[1918,5732,2012],{"class":1934},[1918,5734,5735],{"class":1996},"LastName\n",[1918,5737,5738,5740],{"class":1920,"line":1993},[1918,5739,4926],{"class":2370},[1918,5741,4929],{"class":1923},[1918,5743,5744],{"class":1920,"line":2047},[1918,5745,2144],{"class":1934},[1904,5747,860],{"id":5748},"unregistering",[1830,5750,5751,5752,5755],{},"Pass ",[1915,5753,5754],{},"nil"," to remove a callback:",[1909,5757,5759],{"className":1911,"code":5758,"language":1913,"meta":34,"style":34},"users.OnScan(nil)   // Removes the OnScan callback\nusers.OnRecord(nil) // Removes the OnRecord callback\n",[1915,5760,5761,5778],{"__ignoreMap":34},[1918,5762,5763,5765,5767,5769,5771,5773,5775],{"class":1920,"line":9},[1918,5764,1997],{"class":1996},[1918,5766,2012],{"class":1934},[1918,5768,827],{"class":2015},[1918,5770,2086],{"class":1934},[1918,5772,5754],{"class":1923},[1918,5774,3503],{"class":1934},[1918,5776,5777],{"class":1989},"   // Removes the OnScan callback\n",[1918,5779,5780,5782,5784,5786,5788,5790,5792],{"class":1920,"line":19},[1918,5781,1997],{"class":1996},[1918,5783,2012],{"class":1934},[1918,5785,842],{"class":2015},[1918,5787,2086],{"class":1934},[1918,5789,5754],{"class":1923},[1918,5791,3503],{"class":1934},[1918,5793,5794],{"class":1989}," // Removes the OnRecord callback\n",[1904,5796,48],{"id":5797},"performance",[1830,5799,866],{},[1904,5801,869],{"id":5802},"use-with-grub",[1830,5804,5805,5806,5811,5812,5814,5815,5817,5818,5820,5821,4670,5824,5827],{},"The primary motivation for lifecycle callbacks is integration with ",[1833,5807,5810],{"href":5808,"rel":5809},"https://github.com/zoobz-io/grub",[1837],"grub",", which wraps soy with higher-level lifecycle hooks (BeforeSave, AfterLoad, etc.). By registering ",[1915,5813,827],{}," and ",[1915,5816,842],{}," on the ",[1915,5819,4832],{}," instance, grub's hooks fire automatically through all builder paths — including when users access builders directly via ",[1915,5822,5823],{},"db.Query()",[1915,5825,5826],{},"db.Select()",", etc.",[3622,5829,5830],{},"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 .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 .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 .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}",{"title":34,"searchDepth":19,"depth":19,"links":5832},[5833,5837,5841,5842,5843,5844],{"id":4836,"depth":19,"text":827,"children":5834},[5835,5836],{"id":4936,"depth":40,"text":832},{"id":5067,"depth":40,"text":837},{"id":5229,"depth":19,"text":842,"children":5838},[5839,5840],{"id":5359,"depth":40,"text":832},{"id":5420,"depth":40,"text":837},{"id":5567,"depth":19,"text":855},{"id":5748,"depth":19,"text":860},{"id":5797,"depth":19,"text":48},{"id":5802,"depth":19,"text":869},{},"2025-12-20T00:00:00.000Z",null,{"title":217,"description":820},[5850,5851,5852,827,842],"Lifecycle","Callbacks","Hooks","leh_9ePDyoJ6GutugeluRma7oCnaKSbUcUNcJfeDJkU",[5855,5856],{"title":743,"path":742,"stem":1775,"description":745,"children":-1},{"title":187,"path":873,"stem":1784,"description":875,"children":-1},1776186994150]