[{"data":1,"prerenderedAt":3723},["ShallowReactive",2],{"highlight:import \"github.com/zoobz-io/edamame\"\n\n// Define statements once at package level\nvar (\n    ByStatus = edamame.NewQueryStatement(\n        \"by-status\", \"Query users by status\",\n        edamame.QuerySpec{\n            Where: []edamame.ConditionSpec{\n                {Field: \"status\", Operator: \"=\", Param: \"status\"},\n            },\n            OrderBy: []edamame.OrderBySpec{\n                {Field: \"created_at\", Direction: \"desc\"},\n            },\n            Limit: ptr(50),\n        },\n    )\n\n    SelectByID = edamame.NewSelectStatement(\n        \"select-by-id\", \"Select user by ID\",\n        edamame.SelectSpec{\n            Where: []edamame.ConditionSpec{\n                {Field: \"id\", Operator: \"=\", Param: \"id\"},\n            },\n        },\n    )\n)\n\n// Execute with type safety — wrong statement type won't compile\nusers, _ := exec.ExecQuery(ctx, ByStatus, map[string]any{\"status\": \"active\"})\nuser, _ := exec.ExecSelect(ctx, SelectByID, map[string]any{\"id\": 123})":3,"footer-resources":4,"search-sections-edamame":2784,"nav-edamame":3645,"content-tree-edamame":3685,"content-table-nav-edamame":3704},"\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-keyword)\">import\u003C/span>\u003Cspan style=\"color:var(--shiki-string)\"> \"github.com/zoobz-io/edamame\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-comment)\">// Define statements once at package level\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-keyword)\">var\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\"> (\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-text)\">    ByStatus\u003C/span>\u003Cspan style=\"color:var(--shiki-text)\"> =\u003C/span>\u003Cspan style=\"color:var(--shiki-text)\"> edamame\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">.\u003C/span>\u003Cspan style=\"color:var(--shiki-function)\">NewQueryStatement\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-string)\">        \"by-status\"\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">,\u003C/span>\u003Cspan style=\"color:var(--shiki-string)\"> \"Query users by status\"\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-type)\">        edamame\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">.\u003C/span>\u003Cspan style=\"color:var(--shiki-type)\">QuerySpec\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">{\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-property)\">            Where\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">:\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\"> []\u003C/span>\u003Cspan style=\"color:var(--shiki-type)\">edamame\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">.\u003C/span>\u003Cspan style=\"color:var(--shiki-type)\">ConditionSpec\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">{\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-punctuation)\">                {\u003C/span>\u003Cspan style=\"color:var(--shiki-property)\">Field\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">:\u003C/span>\u003Cspan style=\"color:var(--shiki-string)\"> \"status\"\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">,\u003C/span>\u003Cspan style=\"color:var(--shiki-property)\"> Operator\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">:\u003C/span>\u003Cspan style=\"color:var(--shiki-string)\"> \"=\"\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">,\u003C/span>\u003Cspan style=\"color:var(--shiki-property)\"> Param\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">:\u003C/span>\u003Cspan style=\"color:var(--shiki-string)\"> \"status\"\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">},\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-punctuation)\">            },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-property)\">            OrderBy\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">:\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\"> []\u003C/span>\u003Cspan style=\"color:var(--shiki-type)\">edamame\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">.\u003C/span>\u003Cspan style=\"color:var(--shiki-type)\">OrderBySpec\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">{\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-punctuation)\">                {\u003C/span>\u003Cspan style=\"color:var(--shiki-property)\">Field\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">:\u003C/span>\u003Cspan style=\"color:var(--shiki-string)\"> \"created_at\"\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">,\u003C/span>\u003Cspan style=\"color:var(--shiki-property)\"> Direction\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">:\u003C/span>\u003Cspan style=\"color:var(--shiki-string)\"> \"desc\"\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">},\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-punctuation)\">            },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-property)\">            Limit\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">:\u003C/span>\u003Cspan style=\"color:var(--shiki-function)\"> ptr\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">(\u003C/span>\u003Cspan style=\"color:var(--shiki-number)\">50\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-punctuation)\">        },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-punctuation)\">    )\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-text)\">    SelectByID\u003C/span>\u003Cspan style=\"color:var(--shiki-text)\"> =\u003C/span>\u003Cspan style=\"color:var(--shiki-text)\"> edamame\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">.\u003C/span>\u003Cspan style=\"color:var(--shiki-function)\">NewSelectStatement\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-string)\">        \"select-by-id\"\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">,\u003C/span>\u003Cspan style=\"color:var(--shiki-string)\"> \"Select user by ID\"\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-type)\">        edamame\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">.\u003C/span>\u003Cspan style=\"color:var(--shiki-type)\">SelectSpec\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">{\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-property)\">            Where\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">:\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\"> []\u003C/span>\u003Cspan style=\"color:var(--shiki-type)\">edamame\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">.\u003C/span>\u003Cspan style=\"color:var(--shiki-type)\">ConditionSpec\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">{\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-punctuation)\">                {\u003C/span>\u003Cspan style=\"color:var(--shiki-property)\">Field\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">:\u003C/span>\u003Cspan style=\"color:var(--shiki-string)\"> \"id\"\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">,\u003C/span>\u003Cspan style=\"color:var(--shiki-property)\"> Operator\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">:\u003C/span>\u003Cspan style=\"color:var(--shiki-string)\"> \"=\"\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">,\u003C/span>\u003Cspan style=\"color:var(--shiki-property)\"> Param\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">:\u003C/span>\u003Cspan style=\"color:var(--shiki-string)\"> \"id\"\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">},\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-punctuation)\">            },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-punctuation)\">        },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-punctuation)\">    )\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-punctuation)\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-comment)\">// Execute with type safety — wrong statement type won't compile\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-text)\">users\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">,\u003C/span>\u003Cspan style=\"color:var(--shiki-text)\"> _\u003C/span>\u003Cspan style=\"color:var(--shiki-text)\"> :=\u003C/span>\u003Cspan style=\"color:var(--shiki-text)\"> exec\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">.\u003C/span>\u003Cspan style=\"color:var(--shiki-function)\">ExecQuery\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">(\u003C/span>\u003Cspan style=\"color:var(--shiki-text)\">ctx\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">,\u003C/span>\u003Cspan style=\"color:var(--shiki-text)\"> ByStatus\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">,\u003C/span>\u003Cspan style=\"color:var(--shiki-keyword)\"> map\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">[\u003C/span>\u003Cspan style=\"color:var(--shiki-type)\">string\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">]\u003C/span>\u003Cspan style=\"color:var(--shiki-type)\">any\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">{\u003C/span>\u003Cspan style=\"color:var(--shiki-string)\">\"status\"\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">:\u003C/span>\u003Cspan style=\"color:var(--shiki-string)\"> \"active\"\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">})\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:var(--shiki-text)\">user\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">,\u003C/span>\u003Cspan style=\"color:var(--shiki-text)\"> _\u003C/span>\u003Cspan style=\"color:var(--shiki-text)\"> :=\u003C/span>\u003Cspan style=\"color:var(--shiki-text)\"> exec\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">.\u003C/span>\u003Cspan style=\"color:var(--shiki-function)\">ExecSelect\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">(\u003C/span>\u003Cspan style=\"color:var(--shiki-text)\">ctx\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">,\u003C/span>\u003Cspan style=\"color:var(--shiki-text)\"> SelectByID\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">,\u003C/span>\u003Cspan style=\"color:var(--shiki-keyword)\"> map\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">[\u003C/span>\u003Cspan style=\"color:var(--shiki-type)\">string\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">]\u003C/span>\u003Cspan style=\"color:var(--shiki-type)\">any\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">{\u003C/span>\u003Cspan style=\"color:var(--shiki-string)\">\"id\"\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">:\u003C/span>\u003Cspan style=\"color:var(--shiki-number)\"> 123\u003C/span>\u003Cspan style=\"color:var(--shiki-punctuation)\">})\u003C/span>\u003C/span>",[5,1832,2275],{"id":6,"title":7,"body":8,"description":103,"extension":1825,"icon":1826,"meta":1827,"navigation":178,"path":1828,"seo":1829,"stem":1830,"__hash__":1831},"resources/readme.md","README",{"type":9,"value":10,"toc":1810},"minimark",[11,15,83,86,89,94,97,553,556,560,577,585,589,1412,1416,1516,1520,1563,1567,1573,1576,1722,1725,1729,1738,1743,1758,1762,1774,1778,1785,1789,1797,1800,1806],[12,13,14],"h1",{"id":14},"edamame",[16,17,18,29,37,45,53,61,68,75],"p",{},[19,20,24],"a",{"href":21,"rel":22},"https://github.com/zoobz-io/edamame/actions/workflows/ci.yml",[23],"nofollow",[25,26],"img",{"alt":27,"src":28},"CI Status","https://github.com/zoobz-io/edamame/workflows/CI/badge.svg",[19,30,33],{"href":31,"rel":32},"https://codecov.io/gh/zoobz-io/edamame",[23],[25,34],{"alt":35,"src":36},"codecov","https://codecov.io/gh/zoobz-io/edamame/graph/badge.svg?branch=main",[19,38,41],{"href":39,"rel":40},"https://goreportcard.com/report/github.com/zoobz-io/edamame",[23],[25,42],{"alt":43,"src":44},"Go Report Card","https://goreportcard.com/badge/github.com/zoobz-io/edamame",[19,46,49],{"href":47,"rel":48},"https://github.com/zoobz-io/edamame/security/code-scanning",[23],[25,50],{"alt":51,"src":52},"CodeQL","https://github.com/zoobz-io/edamame/workflows/CodeQL/badge.svg",[19,54,57],{"href":55,"rel":56},"https://pkg.go.dev/github.com/zoobz-io/edamame",[23],[25,58],{"alt":59,"src":60},"Go Reference","https://pkg.go.dev/badge/github.com/zoobz-io/edamame.svg",[19,62,64],{"href":63},"LICENSE",[25,65],{"alt":66,"src":67},"License","https://img.shields.io/github/license/zoobz-io/edamame",[19,69,71],{"href":70},"go.mod",[25,72],{"alt":73,"src":74},"Go Version","https://img.shields.io/github/go-mod/go-version/zoobz-io/edamame",[19,76,79],{"href":77,"rel":78},"https://github.com/zoobz-io/edamame/releases",[23],[25,80],{"alt":81,"src":82},"Release","https://img.shields.io/github/v/release/zoobz-io/edamame",[16,84,85],{},"Statement-driven query exec for Go.",[16,87,88],{},"Define database queries as typed statements, execute them without magic strings.",[90,91,93],"h2",{"id":92},"queries-as-data","Queries as Data",[16,95,96],{},"Edamame treats queries as specs—pure data structures wrapped in typed statements.",[98,99,104],"pre",{"className":100,"code":101,"language":102,"meta":103,"style":103},"language-go shiki shiki-themes","// Define statements as package-level variables\nvar (\n    QueryAll = edamame.NewQueryStatement(\"query-all\", \"Query all users\", edamame.QuerySpec{})\n\n    ByStatus = edamame.NewQueryStatement(\"by-status\", \"Query users by status\", edamame.QuerySpec{\n        Where:   []edamame.ConditionSpec{{Field: \"status\", Operator: \"=\", Param: \"status\"}},\n        OrderBy: []edamame.OrderBySpec{{Field: \"created_at\", Direction: \"desc\"}},\n        Limit:   ptr(50),\n    })\n\n    SelectByID = edamame.NewSelectStatement(\"select-by-id\", \"Select user by ID\", edamame.SelectSpec{\n        Where: []edamame.ConditionSpec{{Field: \"id\", Operator: \"=\", Param: \"id\"}},\n    })\n)\n\n// Execute with type safety\nusers, _ := exec.ExecQuery(ctx, ByStatus, map[string]any{\"status\": \"active\"})\nuser, _ := exec.ExecSelect(ctx, SelectByID, map[string]any{\"id\": 123})\n","go","",[105,106,107,116,127,173,180,215,268,307,327,333,338,374,416,421,427,432,438,501],"code",{"__ignoreMap":103},[108,109,112],"span",{"class":110,"line":111},"line",1,[108,113,115],{"class":114},"sLkEo","// Define statements as package-level variables\n",[108,117,119,123],{"class":110,"line":118},2,[108,120,122],{"class":121},"sUt3r","var",[108,124,126],{"class":125},"sq5bi"," (\n",[108,128,130,134,137,140,143,147,150,154,157,160,162,165,167,170],{"class":110,"line":129},3,[108,131,133],{"class":132},"sh8_p","    QueryAll",[108,135,136],{"class":132}," =",[108,138,139],{"class":132}," edamame",[108,141,142],{"class":125},".",[108,144,146],{"class":145},"s5klm","NewQueryStatement",[108,148,149],{"class":125},"(",[108,151,153],{"class":152},"sxAnc","\"query-all\"",[108,155,156],{"class":125},",",[108,158,159],{"class":152}," \"Query all users\"",[108,161,156],{"class":125},[108,163,139],{"class":164},"sYBwO",[108,166,142],{"class":125},[108,168,169],{"class":164},"QuerySpec",[108,171,172],{"class":125},"{})\n",[108,174,176],{"class":110,"line":175},4,[108,177,179],{"emptyLinePlaceholder":178},true,"\n",[108,181,183,186,188,190,192,194,196,199,201,204,206,208,210,212],{"class":110,"line":182},5,[108,184,185],{"class":132},"    ByStatus",[108,187,136],{"class":132},[108,189,139],{"class":132},[108,191,142],{"class":125},[108,193,146],{"class":145},[108,195,149],{"class":125},[108,197,198],{"class":152},"\"by-status\"",[108,200,156],{"class":125},[108,202,203],{"class":152}," \"Query users by status\"",[108,205,156],{"class":125},[108,207,139],{"class":164},[108,209,142],{"class":125},[108,211,169],{"class":164},[108,213,214],{"class":125},"{\n",[108,216,218,222,225,228,230,232,235,238,241,243,246,248,251,253,256,258,261,263,265],{"class":110,"line":217},6,[108,219,221],{"class":220},"sBGCq","        Where",[108,223,224],{"class":125},":",[108,226,227],{"class":125},"   []",[108,229,14],{"class":164},[108,231,142],{"class":125},[108,233,234],{"class":164},"ConditionSpec",[108,236,237],{"class":125},"{{",[108,239,240],{"class":220},"Field",[108,242,224],{"class":125},[108,244,245],{"class":152}," \"status\"",[108,247,156],{"class":125},[108,249,250],{"class":220}," Operator",[108,252,224],{"class":125},[108,254,255],{"class":152}," \"=\"",[108,257,156],{"class":125},[108,259,260],{"class":220}," Param",[108,262,224],{"class":125},[108,264,245],{"class":152},[108,266,267],{"class":125},"}},\n",[108,269,271,274,276,279,281,283,286,288,290,292,295,297,300,302,305],{"class":110,"line":270},7,[108,272,273],{"class":220},"        OrderBy",[108,275,224],{"class":125},[108,277,278],{"class":125}," []",[108,280,14],{"class":164},[108,282,142],{"class":125},[108,284,285],{"class":164},"OrderBySpec",[108,287,237],{"class":125},[108,289,240],{"class":220},[108,291,224],{"class":125},[108,293,294],{"class":152}," \"created_at\"",[108,296,156],{"class":125},[108,298,299],{"class":220}," Direction",[108,301,224],{"class":125},[108,303,304],{"class":152}," \"desc\"",[108,306,267],{"class":125},[108,308,310,313,315,318,320,324],{"class":110,"line":309},8,[108,311,312],{"class":220},"        Limit",[108,314,224],{"class":125},[108,316,317],{"class":145},"   ptr",[108,319,149],{"class":125},[108,321,323],{"class":322},"sMAmT","50",[108,325,326],{"class":125},"),\n",[108,328,330],{"class":110,"line":329},9,[108,331,332],{"class":125},"    })\n",[108,334,336],{"class":110,"line":335},10,[108,337,179],{"emptyLinePlaceholder":178},[108,339,341,344,346,348,350,353,355,358,360,363,365,367,369,372],{"class":110,"line":340},11,[108,342,343],{"class":132},"    SelectByID",[108,345,136],{"class":132},[108,347,139],{"class":132},[108,349,142],{"class":125},[108,351,352],{"class":145},"NewSelectStatement",[108,354,149],{"class":125},[108,356,357],{"class":152},"\"select-by-id\"",[108,359,156],{"class":125},[108,361,362],{"class":152}," \"Select user by ID\"",[108,364,156],{"class":125},[108,366,139],{"class":164},[108,368,142],{"class":125},[108,370,371],{"class":164},"SelectSpec",[108,373,214],{"class":125},[108,375,377,379,381,383,385,387,389,391,393,395,398,400,402,404,406,408,410,412,414],{"class":110,"line":376},12,[108,378,221],{"class":220},[108,380,224],{"class":125},[108,382,278],{"class":125},[108,384,14],{"class":164},[108,386,142],{"class":125},[108,388,234],{"class":164},[108,390,237],{"class":125},[108,392,240],{"class":220},[108,394,224],{"class":125},[108,396,397],{"class":152}," \"id\"",[108,399,156],{"class":125},[108,401,250],{"class":220},[108,403,224],{"class":125},[108,405,255],{"class":152},[108,407,156],{"class":125},[108,409,260],{"class":220},[108,411,224],{"class":125},[108,413,397],{"class":152},[108,415,267],{"class":125},[108,417,419],{"class":110,"line":418},13,[108,420,332],{"class":125},[108,422,424],{"class":110,"line":423},14,[108,425,426],{"class":125},")\n",[108,428,430],{"class":110,"line":429},15,[108,431,179],{"emptyLinePlaceholder":178},[108,433,435],{"class":110,"line":434},16,[108,436,437],{"class":114},"// Execute with type safety\n",[108,439,441,444,446,449,452,455,457,460,462,465,467,470,472,475,478,481,484,487,490,493,495,498],{"class":110,"line":440},17,[108,442,443],{"class":132},"users",[108,445,156],{"class":125},[108,447,448],{"class":132}," _",[108,450,451],{"class":132}," :=",[108,453,454],{"class":132}," exec",[108,456,142],{"class":125},[108,458,459],{"class":145},"ExecQuery",[108,461,149],{"class":125},[108,463,464],{"class":132},"ctx",[108,466,156],{"class":125},[108,468,469],{"class":132}," ByStatus",[108,471,156],{"class":125},[108,473,474],{"class":121}," map",[108,476,477],{"class":125},"[",[108,479,480],{"class":164},"string",[108,482,483],{"class":125},"]",[108,485,486],{"class":164},"any",[108,488,489],{"class":125},"{",[108,491,492],{"class":152},"\"status\"",[108,494,224],{"class":125},[108,496,497],{"class":152}," \"active\"",[108,499,500],{"class":125},"})\n",[108,502,504,507,509,511,513,515,517,520,522,524,526,529,531,533,535,537,539,541,543,546,548,551],{"class":110,"line":503},18,[108,505,506],{"class":132},"user",[108,508,156],{"class":125},[108,510,448],{"class":132},[108,512,451],{"class":132},[108,514,454],{"class":132},[108,516,142],{"class":125},[108,518,519],{"class":145},"ExecSelect",[108,521,149],{"class":125},[108,523,464],{"class":132},[108,525,156],{"class":125},[108,527,528],{"class":132}," SelectByID",[108,530,156],{"class":125},[108,532,474],{"class":121},[108,534,477],{"class":125},[108,536,480],{"class":164},[108,538,483],{"class":125},[108,540,486],{"class":164},[108,542,489],{"class":125},[108,544,545],{"class":152},"\"id\"",[108,547,224],{"class":125},[108,549,550],{"class":322}," 123",[108,552,500],{"class":125},[16,554,555],{},"Type-safe. No magic strings. Compile-time guarantees.",[90,557,559],{"id":558},"install","Install",[98,561,565],{"className":562,"code":563,"language":564,"meta":103,"style":103},"language-bash shiki shiki-themes","go get github.com/zoobz-io/edamame\n","bash",[105,566,567],{"__ignoreMap":103},[108,568,569,571,574],{"class":110,"line":111},[108,570,102],{"class":145},[108,572,573],{"class":152}," get",[108,575,576],{"class":152}," github.com/zoobz-io/edamame\n",[16,578,579,580,142],{},"Requires Go 1.24+. Supports PostgreSQL, MariaDB, SQLite, and SQL Server via ",[19,581,584],{"href":582,"rel":583},"https://github.com/zoobz-io/astql",[23],"astql",[90,586,588],{"id":587},"quick-start","Quick Start",[98,590,592],{"className":100,"code":591,"language":102,"meta":103,"style":103},"package main\n\nimport (\n    \"context\"\n    \"fmt\"\n\n    \"github.com/jmoiron/sqlx\"\n    _ \"github.com/lib/pq\" // or mariadb, sqlite3, mssql driver\n    \"github.com/zoobz-io/astql/pkg/postgres\" // or mariadb, sqlite, mssql\n    \"github.com/zoobz-io/edamame\"\n)\n\ntype User struct {\n    ID     int    `db:\"id\" type:\"integer\" constraints:\"primarykey\"`\n    Email  string `db:\"email\" type:\"text\" constraints:\"notnull,unique\"`\n    Name   string `db:\"name\" type:\"text\"`\n    Status string `db:\"status\" type:\"text\"`\n}\n\n// Define statements\nvar (\n    QueryAll = edamame.NewQueryStatement(\"query-all\", \"Query all users\", edamame.QuerySpec{})\n\n    SelectByID = edamame.NewSelectStatement(\"select-by-id\", \"Select user by ID\", edamame.SelectSpec{\n        Where: []edamame.ConditionSpec{{Field: \"id\", Operator: \"=\", Param: \"id\"}},\n    })\n\n    CountAll = edamame.NewAggregateStatement(\"count-all\", \"Count all users\", edamame.AggCount, edamame.AggregateSpec{})\n\n    ActiveUsers = edamame.NewQueryStatement(\"active\", \"Query active users\", edamame.QuerySpec{\n        Where: []edamame.ConditionSpec{\n            {Field: \"status\", Operator: \"=\", Param: \"status\"},\n        },\n    })\n)\n\nfunc main() {\n    db, _ := sqlx.Connect(\"postgres\", \"postgres://localhost/mydb?sslmode=disable\")\n    ctx := context.Background()\n\n    // Create exec\n    exec, _ := edamame.New[User](db, \"users\", postgres.New())\n\n    // Execute statements\n    users, _ := exec.ExecQuery(ctx, QueryAll, nil)\n    user, _ := exec.ExecSelect(ctx, SelectByID, map[string]any{\"id\": 1})\n    count, _ := exec.ExecAggregate(ctx, CountAll, nil)\n    active, _ := exec.ExecQuery(ctx, ActiveUsers, map[string]any{\"status\": \"active\"})\n\n    fmt.Printf(\"%d users, user #1: %s, %.0f total, %d active\\n\", len(users), user.Name, count, len(active))\n}\n",[105,593,594,602,606,614,619,624,628,633,645,653,658,662,666,680,691,702,713,724,729,734,740,747,778,783,814,855,860,865,910,915,949,966,997,1003,1008,1013,1018,1032,1064,1083,1088,1094,1141,1146,1152,1186,1235,1269,1318,1323,1407],{"__ignoreMap":103},[108,595,596,599],{"class":110,"line":111},[108,597,598],{"class":121},"package",[108,600,601],{"class":164}," main\n",[108,603,604],{"class":110,"line":118},[108,605,179],{"emptyLinePlaceholder":178},[108,607,608,611],{"class":110,"line":129},[108,609,610],{"class":121},"import",[108,612,126],{"class":613},"soy-K",[108,615,616],{"class":110,"line":175},[108,617,618],{"class":152},"    \"context\"\n",[108,620,621],{"class":110,"line":182},[108,622,623],{"class":152},"    \"fmt\"\n",[108,625,626],{"class":110,"line":217},[108,627,179],{"emptyLinePlaceholder":178},[108,629,630],{"class":110,"line":270},[108,631,632],{"class":152},"    \"github.com/jmoiron/sqlx\"\n",[108,634,635,639,642],{"class":110,"line":309},[108,636,638],{"class":637},"sSYET","    _",[108,640,641],{"class":152}," \"github.com/lib/pq\"",[108,643,644],{"class":114}," // or mariadb, sqlite3, mssql driver\n",[108,646,647,650],{"class":110,"line":329},[108,648,649],{"class":152},"    \"github.com/zoobz-io/astql/pkg/postgres\"",[108,651,652],{"class":114}," // or mariadb, sqlite, mssql\n",[108,654,655],{"class":110,"line":335},[108,656,657],{"class":152},"    \"github.com/zoobz-io/edamame\"\n",[108,659,660],{"class":110,"line":340},[108,661,426],{"class":613},[108,663,664],{"class":110,"line":376},[108,665,179],{"emptyLinePlaceholder":178},[108,667,668,671,674,677],{"class":110,"line":418},[108,669,670],{"class":121},"type",[108,672,673],{"class":164}," User",[108,675,676],{"class":121}," struct",[108,678,679],{"class":125}," {\n",[108,681,682,685,688],{"class":110,"line":423},[108,683,684],{"class":220},"    ID",[108,686,687],{"class":164},"     int",[108,689,690],{"class":152},"    `db:\"id\" type:\"integer\" constraints:\"primarykey\"`\n",[108,692,693,696,699],{"class":110,"line":429},[108,694,695],{"class":220},"    Email",[108,697,698],{"class":164},"  string",[108,700,701],{"class":152}," `db:\"email\" type:\"text\" constraints:\"notnull,unique\"`\n",[108,703,704,707,710],{"class":110,"line":434},[108,705,706],{"class":220},"    Name",[108,708,709],{"class":164},"   string",[108,711,712],{"class":152}," `db:\"name\" type:\"text\"`\n",[108,714,715,718,721],{"class":110,"line":440},[108,716,717],{"class":220},"    Status",[108,719,720],{"class":164}," string",[108,722,723],{"class":152}," `db:\"status\" type:\"text\"`\n",[108,725,726],{"class":110,"line":503},[108,727,728],{"class":125},"}\n",[108,730,732],{"class":110,"line":731},19,[108,733,179],{"emptyLinePlaceholder":178},[108,735,737],{"class":110,"line":736},20,[108,738,739],{"class":114},"// Define statements\n",[108,741,743,745],{"class":110,"line":742},21,[108,744,122],{"class":121},[108,746,126],{"class":125},[108,748,750,752,754,756,758,760,762,764,766,768,770,772,774,776],{"class":110,"line":749},22,[108,751,133],{"class":132},[108,753,136],{"class":132},[108,755,139],{"class":132},[108,757,142],{"class":125},[108,759,146],{"class":145},[108,761,149],{"class":125},[108,763,153],{"class":152},[108,765,156],{"class":125},[108,767,159],{"class":152},[108,769,156],{"class":125},[108,771,139],{"class":164},[108,773,142],{"class":125},[108,775,169],{"class":164},[108,777,172],{"class":125},[108,779,781],{"class":110,"line":780},23,[108,782,179],{"emptyLinePlaceholder":178},[108,784,786,788,790,792,794,796,798,800,802,804,806,808,810,812],{"class":110,"line":785},24,[108,787,343],{"class":132},[108,789,136],{"class":132},[108,791,139],{"class":132},[108,793,142],{"class":125},[108,795,352],{"class":145},[108,797,149],{"class":125},[108,799,357],{"class":152},[108,801,156],{"class":125},[108,803,362],{"class":152},[108,805,156],{"class":125},[108,807,139],{"class":164},[108,809,142],{"class":125},[108,811,371],{"class":164},[108,813,214],{"class":125},[108,815,817,819,821,823,825,827,829,831,833,835,837,839,841,843,845,847,849,851,853],{"class":110,"line":816},25,[108,818,221],{"class":220},[108,820,224],{"class":125},[108,822,278],{"class":125},[108,824,14],{"class":164},[108,826,142],{"class":125},[108,828,234],{"class":164},[108,830,237],{"class":125},[108,832,240],{"class":220},[108,834,224],{"class":125},[108,836,397],{"class":152},[108,838,156],{"class":125},[108,840,250],{"class":220},[108,842,224],{"class":125},[108,844,255],{"class":152},[108,846,156],{"class":125},[108,848,260],{"class":220},[108,850,224],{"class":125},[108,852,397],{"class":152},[108,854,267],{"class":125},[108,856,858],{"class":110,"line":857},26,[108,859,332],{"class":125},[108,861,863],{"class":110,"line":862},27,[108,864,179],{"emptyLinePlaceholder":178},[108,866,868,871,873,875,877,880,882,885,887,890,892,894,896,899,901,903,905,908],{"class":110,"line":867},28,[108,869,870],{"class":132},"    CountAll",[108,872,136],{"class":132},[108,874,139],{"class":132},[108,876,142],{"class":125},[108,878,879],{"class":145},"NewAggregateStatement",[108,881,149],{"class":125},[108,883,884],{"class":152},"\"count-all\"",[108,886,156],{"class":125},[108,888,889],{"class":152}," \"Count all users\"",[108,891,156],{"class":125},[108,893,139],{"class":132},[108,895,142],{"class":125},[108,897,898],{"class":132},"AggCount",[108,900,156],{"class":125},[108,902,139],{"class":164},[108,904,142],{"class":125},[108,906,907],{"class":164},"AggregateSpec",[108,909,172],{"class":125},[108,911,913],{"class":110,"line":912},29,[108,914,179],{"emptyLinePlaceholder":178},[108,916,918,921,923,925,927,929,931,934,936,939,941,943,945,947],{"class":110,"line":917},30,[108,919,920],{"class":132},"    ActiveUsers",[108,922,136],{"class":132},[108,924,139],{"class":132},[108,926,142],{"class":125},[108,928,146],{"class":145},[108,930,149],{"class":125},[108,932,933],{"class":152},"\"active\"",[108,935,156],{"class":125},[108,937,938],{"class":152}," \"Query active users\"",[108,940,156],{"class":125},[108,942,139],{"class":164},[108,944,142],{"class":125},[108,946,169],{"class":164},[108,948,214],{"class":125},[108,950,952,954,956,958,960,962,964],{"class":110,"line":951},31,[108,953,221],{"class":220},[108,955,224],{"class":125},[108,957,278],{"class":125},[108,959,14],{"class":164},[108,961,142],{"class":125},[108,963,234],{"class":164},[108,965,214],{"class":125},[108,967,969,972,974,976,978,980,982,984,986,988,990,992,994],{"class":110,"line":968},32,[108,970,971],{"class":125},"            {",[108,973,240],{"class":220},[108,975,224],{"class":125},[108,977,245],{"class":152},[108,979,156],{"class":125},[108,981,250],{"class":220},[108,983,224],{"class":125},[108,985,255],{"class":152},[108,987,156],{"class":125},[108,989,260],{"class":220},[108,991,224],{"class":125},[108,993,245],{"class":152},[108,995,996],{"class":125},"},\n",[108,998,1000],{"class":110,"line":999},33,[108,1001,1002],{"class":125},"        },\n",[108,1004,1006],{"class":110,"line":1005},34,[108,1007,332],{"class":125},[108,1009,1011],{"class":110,"line":1010},35,[108,1012,426],{"class":125},[108,1014,1016],{"class":110,"line":1015},36,[108,1017,179],{"emptyLinePlaceholder":178},[108,1019,1021,1024,1027,1030],{"class":110,"line":1020},37,[108,1022,1023],{"class":121},"func",[108,1025,1026],{"class":145}," main",[108,1028,1029],{"class":125},"()",[108,1031,679],{"class":125},[108,1033,1035,1038,1040,1042,1044,1047,1049,1052,1054,1057,1059,1062],{"class":110,"line":1034},38,[108,1036,1037],{"class":132},"    db",[108,1039,156],{"class":125},[108,1041,448],{"class":132},[108,1043,451],{"class":132},[108,1045,1046],{"class":132}," sqlx",[108,1048,142],{"class":125},[108,1050,1051],{"class":145},"Connect",[108,1053,149],{"class":125},[108,1055,1056],{"class":152},"\"postgres\"",[108,1058,156],{"class":125},[108,1060,1061],{"class":152}," \"postgres://localhost/mydb?sslmode=disable\"",[108,1063,426],{"class":125},[108,1065,1067,1070,1072,1075,1077,1080],{"class":110,"line":1066},39,[108,1068,1069],{"class":132},"    ctx",[108,1071,451],{"class":132},[108,1073,1074],{"class":132}," context",[108,1076,142],{"class":125},[108,1078,1079],{"class":145},"Background",[108,1081,1082],{"class":125},"()\n",[108,1084,1086],{"class":110,"line":1085},40,[108,1087,179],{"emptyLinePlaceholder":178},[108,1089,1091],{"class":110,"line":1090},41,[108,1092,1093],{"class":114},"    // Create exec\n",[108,1095,1097,1100,1102,1104,1106,1108,1110,1113,1115,1118,1121,1124,1126,1129,1131,1134,1136,1138],{"class":110,"line":1096},42,[108,1098,1099],{"class":132},"    exec",[108,1101,156],{"class":125},[108,1103,448],{"class":132},[108,1105,451],{"class":132},[108,1107,139],{"class":132},[108,1109,142],{"class":125},[108,1111,1112],{"class":145},"New",[108,1114,477],{"class":125},[108,1116,1117],{"class":164},"User",[108,1119,1120],{"class":125},"](",[108,1122,1123],{"class":132},"db",[108,1125,156],{"class":125},[108,1127,1128],{"class":152}," \"users\"",[108,1130,156],{"class":125},[108,1132,1133],{"class":132}," postgres",[108,1135,142],{"class":125},[108,1137,1112],{"class":145},[108,1139,1140],{"class":125},"())\n",[108,1142,1144],{"class":110,"line":1143},43,[108,1145,179],{"emptyLinePlaceholder":178},[108,1147,1149],{"class":110,"line":1148},44,[108,1150,1151],{"class":114},"    // Execute statements\n",[108,1153,1155,1158,1160,1162,1164,1166,1168,1170,1172,1174,1176,1179,1181,1184],{"class":110,"line":1154},45,[108,1156,1157],{"class":132},"    users",[108,1159,156],{"class":125},[108,1161,448],{"class":132},[108,1163,451],{"class":132},[108,1165,454],{"class":132},[108,1167,142],{"class":125},[108,1169,459],{"class":145},[108,1171,149],{"class":125},[108,1173,464],{"class":132},[108,1175,156],{"class":125},[108,1177,1178],{"class":132}," QueryAll",[108,1180,156],{"class":125},[108,1182,1183],{"class":121}," nil",[108,1185,426],{"class":125},[108,1187,1189,1192,1194,1196,1198,1200,1202,1204,1206,1208,1210,1212,1214,1216,1218,1220,1222,1224,1226,1228,1230,1233],{"class":110,"line":1188},46,[108,1190,1191],{"class":132},"    user",[108,1193,156],{"class":125},[108,1195,448],{"class":132},[108,1197,451],{"class":132},[108,1199,454],{"class":132},[108,1201,142],{"class":125},[108,1203,519],{"class":145},[108,1205,149],{"class":125},[108,1207,464],{"class":132},[108,1209,156],{"class":125},[108,1211,528],{"class":132},[108,1213,156],{"class":125},[108,1215,474],{"class":121},[108,1217,477],{"class":125},[108,1219,480],{"class":164},[108,1221,483],{"class":125},[108,1223,486],{"class":164},[108,1225,489],{"class":125},[108,1227,545],{"class":152},[108,1229,224],{"class":125},[108,1231,1232],{"class":322}," 1",[108,1234,500],{"class":125},[108,1236,1238,1241,1243,1245,1247,1249,1251,1254,1256,1258,1260,1263,1265,1267],{"class":110,"line":1237},47,[108,1239,1240],{"class":132},"    count",[108,1242,156],{"class":125},[108,1244,448],{"class":132},[108,1246,451],{"class":132},[108,1248,454],{"class":132},[108,1250,142],{"class":125},[108,1252,1253],{"class":145},"ExecAggregate",[108,1255,149],{"class":125},[108,1257,464],{"class":132},[108,1259,156],{"class":125},[108,1261,1262],{"class":132}," CountAll",[108,1264,156],{"class":125},[108,1266,1183],{"class":121},[108,1268,426],{"class":125},[108,1270,1272,1275,1277,1279,1281,1283,1285,1287,1289,1291,1293,1296,1298,1300,1302,1304,1306,1308,1310,1312,1314,1316],{"class":110,"line":1271},48,[108,1273,1274],{"class":132},"    active",[108,1276,156],{"class":125},[108,1278,448],{"class":132},[108,1280,451],{"class":132},[108,1282,454],{"class":132},[108,1284,142],{"class":125},[108,1286,459],{"class":145},[108,1288,149],{"class":125},[108,1290,464],{"class":132},[108,1292,156],{"class":125},[108,1294,1295],{"class":132}," ActiveUsers",[108,1297,156],{"class":125},[108,1299,474],{"class":121},[108,1301,477],{"class":125},[108,1303,480],{"class":164},[108,1305,483],{"class":125},[108,1307,486],{"class":164},[108,1309,489],{"class":125},[108,1311,492],{"class":152},[108,1313,224],{"class":125},[108,1315,497],{"class":152},[108,1317,500],{"class":125},[108,1319,1321],{"class":110,"line":1320},49,[108,1322,179],{"emptyLinePlaceholder":178},[108,1324,1326,1329,1331,1334,1336,1339,1343,1346,1349,1352,1355,1358,1360,1363,1367,1369,1371,1375,1377,1379,1382,1385,1387,1390,1392,1395,1397,1399,1401,1404],{"class":110,"line":1325},50,[108,1327,1328],{"class":132},"    fmt",[108,1330,142],{"class":125},[108,1332,1333],{"class":145},"Printf",[108,1335,149],{"class":125},[108,1337,1338],{"class":152},"\"",[108,1340,1342],{"class":1341},"scyPU","%d",[108,1344,1345],{"class":152}," users, user #1: ",[108,1347,1348],{"class":1341},"%s",[108,1350,1351],{"class":152},", ",[108,1353,1354],{"class":1341},"%.0f",[108,1356,1357],{"class":152}," total, ",[108,1359,1342],{"class":1341},[108,1361,1362],{"class":152}," active",[108,1364,1366],{"class":1365},"suWN2","\\n",[108,1368,1338],{"class":152},[108,1370,156],{"class":125},[108,1372,1374],{"class":1373},"skxcq"," len",[108,1376,149],{"class":125},[108,1378,443],{"class":132},[108,1380,1381],{"class":125},"),",[108,1383,1384],{"class":132}," user",[108,1386,142],{"class":125},[108,1388,1389],{"class":132},"Name",[108,1391,156],{"class":125},[108,1393,1394],{"class":132}," count",[108,1396,156],{"class":125},[108,1398,1374],{"class":1373},[108,1400,149],{"class":125},[108,1402,1403],{"class":132},"active",[108,1405,1406],{"class":125},"))\n",[108,1408,1410],{"class":110,"line":1409},51,[108,1411,728],{"class":125},[90,1413,1415],{"id":1414},"capabilities","Capabilities",[1417,1418,1419,1435],"table",{},[1420,1421,1422],"thead",{},[1423,1424,1425,1429,1432],"tr",{},[1426,1427,1428],"th",{},"Feature",[1426,1430,1431],{},"Description",[1426,1433,1434],{},"Docs",[1436,1437,1438,1453,1471,1488,1502],"tbody",{},[1423,1439,1440,1444,1447],{},[1441,1442,1443],"td",{},"Statement Types",[1441,1445,1446],{},"Query, Select, Aggregate, Insert, Update, Delete",[1441,1448,1449],{},[19,1450,1452],{"href":1451},"docs/guides/statements","Statements",[1423,1454,1455,1458,1465],{},[1441,1456,1457],{},"Generic Executor",[1441,1459,1460,1461,1464],{},"Type-safe ",[105,1462,1463],{},"Executor[T]"," with compile-time checking",[1441,1466,1467],{},[19,1468,1470],{"href":1469},"docs/learn/concepts","Concepts",[1423,1472,1473,1476,1482],{},[1441,1474,1475],{},"Multi-Dialect Support",[1441,1477,1478,1479],{},"PostgreSQL, MariaDB, SQLite, SQL Server via ",[19,1480,584],{"href":582,"rel":1481},[23],[1441,1483,1484],{},[19,1485,1487],{"href":1486},"docs/learn/quickstart","Quickstart",[1423,1489,1490,1493,1496],{},[1441,1491,1492],{},"Declarative Specs",[1441,1494,1495],{},"Queries as pure data structures",[1441,1497,1498],{},[19,1499,1501],{"href":1500},"docs/learn/architecture","Architecture",[1423,1503,1504,1507,1510],{},[1441,1505,1506],{},"Thread-Safe Execution",[1441,1508,1509],{},"Concurrent access, no shared mutable state",[1441,1511,1512],{},[19,1513,1515],{"href":1514},"docs/reference/api","API",[90,1517,1519],{"id":1518},"why-edamame","Why edamame?",[1521,1522,1523,1539,1545,1551,1557],"ul",{},[1524,1525,1526,1530,1531,1533,1534],"li",{},[1527,1528,1529],"strong",{},"Type-safe"," — Generic ",[105,1532,1463],{}," with compile-time safety via ",[19,1535,1538],{"href":1536,"rel":1537},"https://github.com/zoobz-io/soy",[23],"soy",[1524,1540,1541,1544],{},[1527,1542,1543],{},"No magic strings"," — Typed statements, not string keys",[1524,1546,1547,1550],{},[1527,1548,1549],{},"Declarative"," — Specs are data, statements wrap them with identity",[1524,1552,1553,1556],{},[1527,1554,1555],{},"Compile-time guarantees"," — Pass wrong statement type? Compiler catches it",[1524,1558,1559,1562],{},[1527,1560,1561],{},"Thread-safe"," — Concurrent execution, no shared mutable state",[90,1564,1566],{"id":1565},"structural-query-safety","Structural Query Safety",[16,1568,1569,1570,142],{},"Edamame enables a pattern: ",[1527,1571,1572],{},"define statements once, execute them anywhere",[16,1574,1575],{},"Your query logic lives in typed statements as package-level variables. The executor consumes them with full type safety. No string interpolation, no runtime query building, no SQL injection vectors.",[98,1577,1579],{"className":100,"code":1578,"language":102,"meta":103,"style":103},"// In your repository package\nvar FindActive = edamame.NewQueryStatement(\"find-active\", \"Find active users\", edamame.QuerySpec{\n    Where: []edamame.ConditionSpec{{Field: \"status\", Operator: \"=\", Param: \"status\"}},\n})\n\n// In your service layer\nusers, err := exec.ExecQuery(ctx, FindActive, map[string]any{\"status\": \"active\"})\n",[105,1580,1581,1586,1621,1662,1666,1670,1675],{"__ignoreMap":103},[108,1582,1583],{"class":110,"line":111},[108,1584,1585],{"class":114},"// In your repository package\n",[108,1587,1588,1590,1593,1595,1597,1599,1601,1603,1606,1608,1611,1613,1615,1617,1619],{"class":110,"line":118},[108,1589,122],{"class":121},[108,1591,1592],{"class":132}," FindActive",[108,1594,136],{"class":132},[108,1596,139],{"class":132},[108,1598,142],{"class":125},[108,1600,146],{"class":145},[108,1602,149],{"class":125},[108,1604,1605],{"class":152},"\"find-active\"",[108,1607,156],{"class":125},[108,1609,1610],{"class":152}," \"Find active users\"",[108,1612,156],{"class":125},[108,1614,139],{"class":164},[108,1616,142],{"class":125},[108,1618,169],{"class":164},[108,1620,214],{"class":125},[108,1622,1623,1626,1628,1630,1632,1634,1636,1638,1640,1642,1644,1646,1648,1650,1652,1654,1656,1658,1660],{"class":110,"line":129},[108,1624,1625],{"class":220},"    Where",[108,1627,224],{"class":125},[108,1629,278],{"class":125},[108,1631,14],{"class":164},[108,1633,142],{"class":125},[108,1635,234],{"class":164},[108,1637,237],{"class":125},[108,1639,240],{"class":220},[108,1641,224],{"class":125},[108,1643,245],{"class":152},[108,1645,156],{"class":125},[108,1647,250],{"class":220},[108,1649,224],{"class":125},[108,1651,255],{"class":152},[108,1653,156],{"class":125},[108,1655,260],{"class":220},[108,1657,224],{"class":125},[108,1659,245],{"class":152},[108,1661,267],{"class":125},[108,1663,1664],{"class":110,"line":175},[108,1665,500],{"class":125},[108,1667,1668],{"class":110,"line":182},[108,1669,179],{"emptyLinePlaceholder":178},[108,1671,1672],{"class":110,"line":217},[108,1673,1674],{"class":114},"// In your service layer\n",[108,1676,1677,1679,1681,1684,1686,1688,1690,1692,1694,1696,1698,1700,1702,1704,1706,1708,1710,1712,1714,1716,1718,1720],{"class":110,"line":270},[108,1678,443],{"class":132},[108,1680,156],{"class":125},[108,1682,1683],{"class":132}," err",[108,1685,451],{"class":132},[108,1687,454],{"class":132},[108,1689,142],{"class":125},[108,1691,459],{"class":145},[108,1693,149],{"class":125},[108,1695,464],{"class":132},[108,1697,156],{"class":125},[108,1699,1592],{"class":132},[108,1701,156],{"class":125},[108,1703,474],{"class":121},[108,1705,477],{"class":125},[108,1707,480],{"class":164},[108,1709,483],{"class":125},[108,1711,486],{"class":164},[108,1713,489],{"class":125},[108,1715,492],{"class":152},[108,1717,224],{"class":125},[108,1719,497],{"class":152},[108,1721,500],{"class":125},[16,1723,1724],{},"Statements are the single source of truth. The database dialect handles rendering. Your code stays clean.",[90,1726,1728],{"id":1727},"documentation","Documentation",[1521,1730,1731],{},[1524,1732,1733,1737],{},[19,1734,1736],{"href":1735},"docs/overview","Overview"," — Design philosophy and architecture",[1739,1740,1742],"h3",{"id":1741},"learn","Learn",[1521,1744,1745,1749,1754],{},[1524,1746,1747],{},[19,1748,1487],{"href":1486},[1524,1750,1751],{},[19,1752,1753],{"href":1469},"Core Concepts",[1524,1755,1756],{},[19,1757,1501],{"href":1500},[1739,1759,1761],{"id":1760},"guides","Guides",[1521,1763,1764,1768],{},[1524,1765,1766],{},[19,1767,1452],{"href":1451},[1524,1769,1770],{},[19,1771,1773],{"href":1772},"docs/guides/testing","Testing",[1739,1775,1777],{"id":1776},"reference","Reference",[1521,1779,1780],{},[1524,1781,1782],{},[19,1783,1784],{"href":1514},"API Reference",[90,1786,1788],{"id":1787},"contributing","Contributing",[16,1790,1791,1792,1796],{},"See ",[19,1793,1795],{"href":1794},"CONTRIBUTING","CONTRIBUTING.md"," for guidelines.",[90,1798,66],{"id":1799},"license",[16,1801,1802,1803,1805],{},"MIT License — see ",[19,1804,63],{"href":63}," for details.",[1807,1808,1809],"style",{},"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 .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .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 .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .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 .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":103,"searchDepth":118,"depth":118,"links":1811},[1812,1813,1814,1815,1816,1817,1818,1823,1824],{"id":92,"depth":118,"text":93},{"id":558,"depth":118,"text":559},{"id":587,"depth":118,"text":588},{"id":1414,"depth":118,"text":1415},{"id":1518,"depth":118,"text":1519},{"id":1565,"depth":118,"text":1566},{"id":1727,"depth":118,"text":1728,"children":1819},[1820,1821,1822],{"id":1741,"depth":129,"text":1742},{"id":1760,"depth":129,"text":1761},{"id":1776,"depth":129,"text":1777},{"id":1787,"depth":118,"text":1788},{"id":1799,"depth":118,"text":66},"md","book-open",{},"/readme",{"title":7,"description":103},"readme","BscYQNVE6yaJgKSiVGtWPnquY2UTEYnFwgQPUaLtbJU",{"id":1833,"title":1834,"body":1835,"description":103,"extension":1825,"icon":2269,"meta":2270,"navigation":178,"path":2271,"seo":2272,"stem":2273,"__hash__":2274},"resources/security.md","Security",{"type":9,"value":1836,"toc":2255},[1837,1841,1845,1848,1887,1891,1894,1898,1903,1906,1945,1949,1952,2001,2005,2031,2035,2038,2042,2045,2141,2145,2148,2180,2184,2187,2212,2216,2230,2234,2237,2243,2246,2252],[12,1838,1840],{"id":1839},"security-policy","Security Policy",[90,1842,1844],{"id":1843},"supported-versions","Supported Versions",[16,1846,1847],{},"We release patches for security vulnerabilities. Which versions are eligible for receiving such patches depends on the CVSS v3.0 Rating:",[1417,1849,1850,1863],{},[1420,1851,1852],{},[1423,1853,1854,1857,1860],{},[1426,1855,1856],{},"Version",[1426,1858,1859],{},"Supported",[1426,1861,1862],{},"Status",[1436,1864,1865,1876],{},[1423,1866,1867,1870,1873],{},[1441,1868,1869],{},"latest",[1441,1871,1872],{},"✅",[1441,1874,1875],{},"Active development",[1423,1877,1878,1881,1884],{},[1441,1879,1880],{},"\u003C latest",[1441,1882,1883],{},"❌",[1441,1885,1886],{},"Security fixes only for critical issues",[90,1888,1890],{"id":1889},"reporting-a-vulnerability","Reporting a Vulnerability",[16,1892,1893],{},"We take the security of edamame seriously. If you have discovered a security vulnerability in this project, please report it responsibly.",[1739,1895,1897],{"id":1896},"how-to-report","How to Report",[16,1899,1900],{},[1527,1901,1902],{},"Please DO NOT report security vulnerabilities through public GitHub issues.",[16,1904,1905],{},"Instead, please report them via one of the following methods:",[1907,1908,1909,1932],"ol",{},[1524,1910,1911,1914,1915],{},[1527,1912,1913],{},"GitHub Security Advisories"," (Preferred)",[1521,1916,1917,1926,1929],{},[1524,1918,1919,1920,1925],{},"Go to the ",[19,1921,1924],{"href":1922,"rel":1923},"https://github.com/zoobz-io/edamame/security",[23],"Security tab"," of this repository",[1524,1927,1928],{},"Click \"Report a vulnerability\"",[1524,1930,1931],{},"Fill out the form with details about the vulnerability",[1524,1933,1934,1937],{},[1527,1935,1936],{},"Email",[1521,1938,1939,1942],{},[1524,1940,1941],{},"Send details to the repository maintainer through GitHub profile contact information",[1524,1943,1944],{},"Use PGP encryption if possible for sensitive details",[1739,1946,1948],{"id":1947},"what-to-include","What to Include",[16,1950,1951],{},"Please include the following information (as much as you can provide) to help us better understand the nature and scope of the possible issue:",[1521,1953,1954,1960,1966,1972,1978,1983,1989,1995],{},[1524,1955,1956,1959],{},[1527,1957,1958],{},"Type of issue"," (e.g., SQL injection, race condition, information disclosure, etc.)",[1524,1961,1962,1965],{},[1527,1963,1964],{},"Full paths of source file(s)"," related to the manifestation of the issue",[1524,1967,1968,1971],{},[1527,1969,1970],{},"The location of the affected source code"," (tag/branch/commit or direct URL)",[1524,1973,1974,1977],{},[1527,1975,1976],{},"Any special configuration required"," to reproduce the issue",[1524,1979,1980,1977],{},[1527,1981,1982],{},"Step-by-step instructions",[1524,1984,1985,1988],{},[1527,1986,1987],{},"Proof-of-concept or exploit code"," (if possible)",[1524,1990,1991,1994],{},[1527,1992,1993],{},"Impact of the issue",", including how an attacker might exploit the issue",[1524,1996,1997,2000],{},[1527,1998,1999],{},"Your name and affiliation"," (optional)",[1739,2002,2004],{"id":2003},"what-to-expect","What to Expect",[1521,2006,2007,2013,2019,2025],{},[1524,2008,2009,2012],{},[1527,2010,2011],{},"Acknowledgment",": We will acknowledge receipt of your vulnerability report within 48 hours",[1524,2014,2015,2018],{},[1527,2016,2017],{},"Initial Assessment",": Within 7 days, we will provide an initial assessment of the report",[1524,2020,2021,2024],{},[1527,2022,2023],{},"Resolution Timeline",": We aim to resolve critical issues within 30 days",[1524,2026,2027,2030],{},[1527,2028,2029],{},"Disclosure",": We will coordinate with you on the disclosure timeline",[1739,2032,2034],{"id":2033},"preferred-languages","Preferred Languages",[16,2036,2037],{},"We prefer all communications to be in English.",[90,2039,2041],{"id":2040},"security-best-practices","Security Best Practices",[16,2043,2044],{},"When using edamame in your applications, we recommend:",[1907,2046,2047,2068,2087,2106,2125],{},[1524,2048,2049,2052],{},[1527,2050,2051],{},"Keep Dependencies Updated",[98,2053,2055],{"className":562,"code":2054,"language":564,"meta":103,"style":103},"go get -u github.com/zoobz-io/edamame\n",[105,2056,2057],{"__ignoreMap":103},[108,2058,2059,2061,2063,2066],{"class":110,"line":111},[108,2060,102],{"class":145},[108,2062,573],{"class":152},[108,2064,2065],{"class":121}," -u",[108,2067,576],{"class":152},[1524,2069,2070,2073],{},[1527,2071,2072],{},"Database Security",[1521,2074,2075,2078,2081,2084],{},[1524,2076,2077],{},"Use least-privilege database accounts",[1524,2079,2080],{},"Never expose factory specs publicly without authentication",[1524,2082,2083],{},"Validate all user input before passing to capabilities",[1524,2085,2086],{},"Use transactions for multi-step operations",[1524,2088,2089,2092],{},[1527,2090,2091],{},"LLM Integration Security",[1521,2093,2094,2097,2100,2103],{},[1524,2095,2096],{},"Always validate LLM-generated capability names and params",[1524,2098,2099],{},"Rate limit LLM-driven database operations",[1524,2101,2102],{},"Audit log all LLM-executed queries",[1524,2104,2105],{},"Never expose raw SQL generation to external systems",[1524,2107,2108,2111],{},[1527,2109,2110],{},"Capability Management",[1521,2112,2113,2116,2119,2122],{},[1524,2114,2115],{},"Only expose capabilities that are safe for your use case",[1524,2117,2118],{},"Use explicit params with validation",[1524,2120,2121],{},"Avoid overly permissive WHERE clauses",[1524,2123,2124],{},"Review auto-derived params before deployment",[1524,2126,2127,2130],{},[1527,2128,2129],{},"Error Handling",[1521,2131,2132,2135,2138],{},[1524,2133,2134],{},"Implement proper error handling",[1524,2136,2137],{},"Don't expose internal errors to users",[1524,2139,2140],{},"Log errors appropriately",[90,2142,2144],{"id":2143},"security-features","Security Features",[16,2146,2147],{},"edamame includes several built-in security features:",[1521,2149,2150,2156,2162,2168,2174],{},[1524,2151,2152,2155],{},[1527,2153,2154],{},"Parameterized Queries",": All queries use parameterized SQL via sqlx, preventing SQL injection",[1524,2157,2158,2161],{},[1527,2159,2160],{},"Type Safety",": Generic types prevent type confusion",[1524,2163,2164,2167],{},[1527,2165,2166],{},"Spec Validation",": Capability specs are validated before execution",[1524,2169,2170,2173],{},[1527,2171,2172],{},"No Raw SQL",": Users define specs, not raw SQL strings",[1524,2175,2176,2179],{},[1527,2177,2178],{},"Introspection Only",": Spec export is read-only, doesn't expose credentials",[90,2181,2183],{"id":2182},"automated-security-scanning","Automated Security Scanning",[16,2185,2186],{},"This project uses:",[1521,2188,2189,2194,2200,2206],{},[1524,2190,2191,2193],{},[1527,2192,51],{},": GitHub's semantic code analysis for security vulnerabilities",[1524,2195,2196,2199],{},[1527,2197,2198],{},"gosec",": Go security checker for common vulnerabilities",[1524,2201,2202,2205],{},[1527,2203,2204],{},"golangci-lint",": Static analysis including security linters (sqlclosecheck, noctx, bodyclose)",[1524,2207,2208,2211],{},[1527,2209,2210],{},"Codecov",": Coverage tracking to ensure security-critical code is tested",[90,2213,2215],{"id":2214},"vulnerability-disclosure-policy","Vulnerability Disclosure Policy",[1521,2217,2218,2221,2224,2227],{},[1524,2219,2220],{},"Security vulnerabilities will be disclosed via GitHub Security Advisories",[1524,2222,2223],{},"We follow a 90-day disclosure timeline for non-critical issues",[1524,2225,2226],{},"Critical vulnerabilities may be disclosed sooner after patches are available",[1524,2228,2229],{},"We will credit reporters who follow responsible disclosure practices",[90,2231,2233],{"id":2232},"credits","Credits",[16,2235,2236],{},"We thank the following individuals for responsibly disclosing security issues:",[16,2238,2239],{},[2240,2241,2242],"em",{},"This list is currently empty. Be the first to help improve our security!",[2244,2245],"hr",{},[16,2247,2248,2251],{},[1527,2249,2250],{},"Last Updated",": 2025-12-17",[1807,2253,2254],{},"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":103,"searchDepth":118,"depth":118,"links":2256},[2257,2258,2264,2265,2266,2267,2268],{"id":1843,"depth":118,"text":1844},{"id":1889,"depth":118,"text":1890,"children":2259},[2260,2261,2262,2263],{"id":1896,"depth":129,"text":1897},{"id":1947,"depth":129,"text":1948},{"id":2003,"depth":129,"text":2004},{"id":2033,"depth":129,"text":2034},{"id":2040,"depth":118,"text":2041},{"id":2143,"depth":118,"text":2144},{"id":2182,"depth":118,"text":2183},{"id":2214,"depth":118,"text":2215},{"id":2232,"depth":118,"text":2233},"shield",{},"/security",{"title":1834,"description":103},"security","Sx8ErY-iTzxHLX7iGbih6ydeebS0veDo30qsuQcZLEI",{"id":2276,"title":1788,"body":2277,"description":2285,"extension":1825,"icon":105,"meta":2780,"navigation":178,"path":2781,"seo":2782,"stem":1787,"__hash__":2783},"resources/contributing.md",{"type":9,"value":2278,"toc":2756},[2279,2283,2286,2290,2293,2297,2341,2345,2349,2367,2370,2393,2395,2409,2413,2417,2431,2435,2446,2450,2453,2467,2471,2499,2502,2505,2518,2521,2533,2536,2548,2551,2563,2566,2578,2582,2590,2594,2597,2641,2645,2649,2652,2663,2666,2680,2684,2711,2717,2721,2724,2735,2739,2750,2753],[12,2280,2282],{"id":2281},"contributing-to-edamame","Contributing to edamame",[16,2284,2285],{},"Thank you for your interest in contributing to edamame! This guide will help you get started.",[90,2287,2289],{"id":2288},"code-of-conduct","Code of Conduct",[16,2291,2292],{},"By participating in this project, you agree to maintain a respectful and inclusive environment for all contributors.",[90,2294,2296],{"id":2295},"getting-started","Getting Started",[1907,2298,2299,2302,2308,2314,2317,2323,2329,2332,2338],{},[1524,2300,2301],{},"Fork the repository",[1524,2303,2304,2305],{},"Clone your fork: ",[105,2306,2307],{},"git clone https://github.com/yourusername/edamame.git",[1524,2309,2310,2311],{},"Create a feature branch: ",[105,2312,2313],{},"git checkout -b feature/your-feature-name",[1524,2315,2316],{},"Make your changes",[1524,2318,2319,2320],{},"Run tests: ",[105,2321,2322],{},"make test",[1524,2324,2325,2326],{},"Run linters: ",[105,2327,2328],{},"make lint",[1524,2330,2331],{},"Commit your changes with a descriptive message",[1524,2333,2334,2335],{},"Push to your fork: ",[105,2336,2337],{},"git push origin feature/your-feature-name",[1524,2339,2340],{},"Create a Pull Request",[90,2342,2344],{"id":2343},"development-guidelines","Development Guidelines",[1739,2346,2348],{"id":2347},"code-style","Code Style",[1521,2350,2351,2354,2361,2364],{},[1524,2352,2353],{},"Follow standard Go conventions",[1524,2355,2356,2357,2360],{},"Run ",[105,2358,2359],{},"go fmt"," before committing",[1524,2362,2363],{},"Add comments for exported functions and types",[1524,2365,2366],{},"Keep functions small and focused",[1739,2368,1773],{"id":2369},"testing",[1521,2371,2372,2375,2380,2383,2386],{},[1524,2373,2374],{},"Write tests for new functionality",[1524,2376,2377,2378],{},"Ensure all tests pass: ",[105,2379,2322],{},[1524,2381,2382],{},"Maintain 1:1 file-to-test ratio",[1524,2384,2385],{},"Aim for 80%+ test coverage",[1524,2387,2388,2389,2392],{},"Use the ",[105,2390,2391],{},"testing/"," package helpers for capability testing",[1739,2394,1728],{"id":1727},[1521,2396,2397,2400,2403,2406],{},[1524,2398,2399],{},"Update README.md for API changes",[1524,2401,2402],{},"Add comments to all exported types",[1524,2404,2405],{},"Keep doc comments clear and concise",[1524,2407,2408],{},"Update docs/ for significant changes",[90,2410,2412],{"id":2411},"types-of-contributions","Types of Contributions",[1739,2414,2416],{"id":2415},"bug-reports","Bug Reports",[1521,2418,2419,2422,2425,2428],{},[1524,2420,2421],{},"Use GitHub Issues",[1524,2423,2424],{},"Include minimal reproduction code",[1524,2426,2427],{},"Describe expected vs actual behavior",[1524,2429,2430],{},"Include Go version and OS",[1739,2432,2434],{"id":2433},"feature-requests","Feature Requests",[1521,2436,2437,2440,2443],{},[1524,2438,2439],{},"Open an issue for discussion first",[1524,2441,2442],{},"Explain the use case",[1524,2444,2445],{},"Consider backwards compatibility",[1739,2447,2449],{"id":2448},"code-contributions","Code Contributions",[16,2451,2452],{},"All contributions should:",[1521,2454,2455,2458,2461,2464],{},[1524,2456,2457],{},"Include comprehensive tests",[1524,2459,2460],{},"Pass linter checks",[1524,2462,2463],{},"Maintain existing code style",[1524,2465,2466],{},"Update documentation as needed",[90,2468,2470],{"id":2469},"pull-request-process","Pull Request Process",[1907,2472,2473,2479,2484,2489,2494],{},[1524,2474,2475,2478],{},[1527,2476,2477],{},"Keep PRs focused"," - One feature/fix per PR",[1524,2480,2481],{},[1527,2482,2483],{},"Write descriptive commit messages",[1524,2485,2486],{},[1527,2487,2488],{},"Update tests and documentation",[1524,2490,2491],{},[1527,2492,2493],{},"Ensure CI passes",[1524,2495,2496],{},[1527,2497,2498],{},"Respond to review feedback",[90,2500,1773],{"id":2501},"testing-1",[16,2503,2504],{},"Run the full test suite:",[98,2506,2508],{"className":562,"code":2507,"language":564,"meta":103,"style":103},"make test\n",[105,2509,2510],{"__ignoreMap":103},[108,2511,2512,2515],{"class":110,"line":111},[108,2513,2514],{"class":145},"make",[108,2516,2517],{"class":152}," test\n",[16,2519,2520],{},"Run with coverage:",[98,2522,2524],{"className":562,"code":2523,"language":564,"meta":103,"style":103},"make coverage\n",[105,2525,2526],{"__ignoreMap":103},[108,2527,2528,2530],{"class":110,"line":111},[108,2529,2514],{"class":145},[108,2531,2532],{"class":152}," coverage\n",[16,2534,2535],{},"Run linters:",[98,2537,2539],{"className":562,"code":2538,"language":564,"meta":103,"style":103},"make lint\n",[105,2540,2541],{"__ignoreMap":103},[108,2542,2543,2545],{"class":110,"line":111},[108,2544,2514],{"class":145},[108,2546,2547],{"class":152}," lint\n",[16,2549,2550],{},"Run integration tests (requires Docker):",[98,2552,2554],{"className":562,"code":2553,"language":564,"meta":103,"style":103},"make test-integration\n",[105,2555,2556],{"__ignoreMap":103},[108,2557,2558,2560],{"class":110,"line":111},[108,2559,2514],{"class":145},[108,2561,2562],{"class":152}," test-integration\n",[16,2564,2565],{},"Run full CI simulation:",[98,2567,2569],{"className":562,"code":2568,"language":564,"meta":103,"style":103},"make ci\n",[105,2570,2571],{"__ignoreMap":103},[108,2572,2573,2575],{"class":110,"line":111},[108,2574,2514],{"class":145},[108,2576,2577],{"class":152}," ci\n",[90,2579,2581],{"id":2580},"project-structure","Project Structure",[98,2583,2588],{"className":2584,"code":2586,"language":2587},[2585],"language-text","edamame/\n├── *.go              # Core library files\n├── *_test.go         # Tests (1:1 with source files)\n├── testing/          # Test helpers and benchmarks\n│   ├── helpers.go    # Test utilities\n│   ├── benchmarks/   # Performance benchmarks\n│   └── integration/  # Database integration tests\n├── docs/             # Documentation\n├── .github/          # GitHub workflows and templates\n├── README.md         # Project documentation\n└── Makefile          # Build and test commands\n","text",[105,2589,2586],{"__ignoreMap":103},[90,2591,2593],{"id":2592},"commit-messages","Commit Messages",[16,2595,2596],{},"Follow conventional commits:",[1521,2598,2599,2605,2611,2617,2623,2629,2635],{},[1524,2600,2601,2604],{},[105,2602,2603],{},"feat:"," New feature",[1524,2606,2607,2610],{},[105,2608,2609],{},"fix:"," Bug fix",[1524,2612,2613,2616],{},[105,2614,2615],{},"docs:"," Documentation changes",[1524,2618,2619,2622],{},[105,2620,2621],{},"test:"," Test additions/changes",[1524,2624,2625,2628],{},[105,2626,2627],{},"refactor:"," Code refactoring",[1524,2630,2631,2634],{},[105,2632,2633],{},"perf:"," Performance improvements",[1524,2636,2637,2640],{},[105,2638,2639],{},"chore:"," Maintenance tasks",[90,2642,2644],{"id":2643},"release-process","Release Process",[1739,2646,2648],{"id":2647},"automated-releases","Automated Releases",[16,2650,2651],{},"This project uses automated release versioning. To create a release:",[1907,2653,2654,2657,2660],{},[1524,2655,2656],{},"Go to Actions → Release → Run workflow",[1524,2658,2659],{},"Leave \"Version override\" empty for automatic version inference",[1524,2661,2662],{},"Click \"Run workflow\"",[16,2664,2665],{},"The system will:",[1521,2667,2668,2671,2674,2677],{},[1524,2669,2670],{},"Automatically determine the next version from conventional commits",[1524,2672,2673],{},"Create a git tag",[1524,2675,2676],{},"Generate release notes via GoReleaser",[1524,2678,2679],{},"Publish the release to GitHub",[1739,2681,2683],{"id":2682},"commit-conventions-for-versioning","Commit Conventions for Versioning",[1521,2685,2686,2691,2696,2702],{},[1524,2687,2688,2690],{},[105,2689,2603],{}," new features (minor version: 1.2.0 → 1.3.0)",[1524,2692,2693,2695],{},[105,2694,2609],{}," bug fixes (patch version: 1.2.0 → 1.2.1)",[1524,2697,2698,2701],{},[105,2699,2700],{},"feat!:"," breaking changes (major version: 1.2.0 → 2.0.0)",[1524,2703,2704,1351,2706,1351,2708,2710],{},[105,2705,2615],{},[105,2707,2621],{},[105,2709,2639],{}," no version change",[16,2712,2713,2714],{},"Example: ",[105,2715,2716],{},"feat(factory): add batch delete support",[1739,2718,2720],{"id":2719},"version-preview-on-pull-requests","Version Preview on Pull Requests",[16,2722,2723],{},"Every PR automatically shows the next version that will be created:",[1521,2725,2726,2729,2732],{},[1524,2727,2728],{},"Check PR comments for \"Version Preview\"",[1524,2730,2731],{},"Updates automatically as you add commits",[1524,2733,2734],{},"Helps verify your commits have the intended effect",[90,2736,2738],{"id":2737},"questions","Questions?",[1521,2740,2741,2744,2747],{},[1524,2742,2743],{},"Open an issue for questions",[1524,2745,2746],{},"Check existing issues first",[1524,2748,2749],{},"Be patient and respectful",[16,2751,2752],{},"Thank you for contributing to edamame!",[1807,2754,2755],{},"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);}",{"title":103,"searchDepth":118,"depth":118,"links":2757},[2758,2759,2760,2765,2770,2771,2772,2773,2774,2779],{"id":2288,"depth":118,"text":2289},{"id":2295,"depth":118,"text":2296},{"id":2343,"depth":118,"text":2344,"children":2761},[2762,2763,2764],{"id":2347,"depth":129,"text":2348},{"id":2369,"depth":129,"text":1773},{"id":1727,"depth":129,"text":1728},{"id":2411,"depth":118,"text":2412,"children":2766},[2767,2768,2769],{"id":2415,"depth":129,"text":2416},{"id":2433,"depth":129,"text":2434},{"id":2448,"depth":129,"text":2449},{"id":2469,"depth":118,"text":2470},{"id":2501,"depth":118,"text":1773},{"id":2580,"depth":118,"text":2581},{"id":2592,"depth":118,"text":2593},{"id":2643,"depth":118,"text":2644,"children":2775},[2776,2777,2778],{"id":2647,"depth":129,"text":2648},{"id":2682,"depth":129,"text":2683},{"id":2719,"depth":129,"text":2720},{"id":2737,"depth":118,"text":2738},{},"/contributing",{"title":1788,"description":2285},"QcMmj_qs3DqAuZIWSIkA2CW6kAKXQPr0ftDXK0qwA9k",[2785,2789,2793,2797,2802,2806,2810,2814,2819,2823,2828,2832,2835,2840,2845,2850,2855,2860,2865,2870,2874,2878,2883,2888,2892,2896,2901,2906,2911,2915,2919,2924,2929,2933,2938,2943,2948,2953,2958,2963,2968,2973,2978,2983,2987,2991,2996,3000,3005,3010,3015,3020,3025,3030,3035,3039,3043,3047,3052,3057,3062,3067,3072,3077,3082,3087,3092,3097,3102,3107,3112,3117,3122,3127,3132,3137,3142,3147,3152,3157,3162,3167,3172,3177,3182,3187,3192,3197,3202,3207,3210,3215,3220,3224,3228,3232,3237,3242,3247,3252,3257,3262,3267,3272,3277,3282,3287,3291,3296,3300,3305,3310,3315,3320,3325,3330,3335,3340,3345,3350,3354,3358,3362,3366,3370,3374,3378,3383,3388,3392,3396,3401,3406,3411,3416,3421,3425,3429,3434,3439,3444,3449,3454,3459,3464,3468,3473,3478,3483,3488,3493,3498,3503,3507,3512,3517,3522,3527,3532,3537,3542,3546,3551,3555,3559,3564,3568,3572,3576,3580,3584,3588,3592,3597,3601,3606,3611,3616,3621,3626,3631,3636,3641],{"id":2786,"title":1736,"titles":2787,"content":2788,"level":111},"/v1.0.3/overview",[],"Statement-driven query exec for Go applications",{"id":2790,"title":1736,"titles":2791,"content":2792,"level":111},"/v1.0.3/overview#overview",[],"Database operations in Go often mean choosing between raw SQL and heavy ORMs. Edamame offers a third path: a statement-driven query exec that stays out of your way while providing type-safe, declarative query definitions. // Define your model\ntype User struct {\n    ID    int    `db:\"id\" type:\"integer\" constraints:\"primarykey\"`\n    Email string `db:\"email\" type:\"text\" constraints:\"notnull,unique\"`\n    Name  string `db:\"name\" type:\"text\"`\n    Age   *int   `db:\"age\" type:\"integer\"`\n}\n\n// Define statements as package-level variables\nvar (\n    QueryAll = edamame.NewQueryStatement(\"query-all\", \"Query all users\", edamame.QuerySpec{})\n\n    SelectByID = edamame.NewSelectStatement(\"select-by-id\", \"Select user by ID\", edamame.SelectSpec{\n        Where: []edamame.ConditionSpec{{Field: \"id\", Operator: \"=\", Param: \"id\"}},\n    })\n\n    DeleteByID = edamame.NewDeleteStatement(\"delete-by-id\", \"Delete user by ID\", edamame.DeleteSpec{\n        Where: []edamame.ConditionSpec{{Field: \"id\", Operator: \"=\", Param: \"id\"}},\n    })\n\n    Adults = edamame.NewQueryStatement(\"adults\", \"Find users over a minimum age\", edamame.QuerySpec{\n        Where: []edamame.ConditionSpec{\n            {Field: \"age\", Operator: \">=\", Param: \"min_age\"},\n        },\n        OrderBy: []edamame.OrderBySpec{\n            {Field: \"name\", Direction: \"asc\"},\n        },\n    })\n)\n\n// Create a exec and execute\nexec, err := edamame.New[User](db, \"users\", renderer)\n\nusers, err := exec.ExecQuery(ctx, QueryAll, nil)\nuser, err := exec.ExecSelect(ctx, SelectByID, map[string]any{\"id\": 123})\ninserted, err := exec.ExecInsert(ctx, &user)\ndeleted, err := exec.ExecDelete(ctx, DeleteByID, map[string]any{\"id\": 123})\nadults, err := exec.ExecQuery(ctx, Adults, map[string]any{\"min_age\": 18}) Type-safe, injection-protected, declarative.",{"id":2794,"title":1501,"titles":2795,"content":2796,"level":118},"/v1.0.3/overview#architecture",[1736],"┌─────────────────────────────────────────────────────────────┐\n│                         Edamame                             │\n│                                                             │\n│  ┌─────────────────────────────────────────────────────┐    │\n│  │                   Executor[T]                        │    │\n│  │                                                     │    │\n│  │  ┌───────────────────────────────────────────────┐  │    │\n│  │  │              Statement Types                   │  │    │\n│  │  │                                                │  │    │\n│  │  │  QueryStatement  SelectStatement               │  │    │\n│  │  │  UpdateStatement DeleteStatement               │  │    │\n│  │  │  AggregateStatement                            │  │    │\n│  │  └────────────────────┬───────────────────────────┘  │    │\n│  │                       │                              │    │\n│  │                 ┌─────▼─────┐                        │    │\n│  │                 │    Soy    │                        │    │\n│  │                 │ (Builder) │                        │    │\n│  │                 └─────┬─────┘                        │    │\n│  │                       │                              │    │\n│  └───────────────────────┼──────────────────────────────┘    │\n│                          │                                   │\n│                    ┌─────▼─────┐                             │\n│                    │   sqlx    │                             │\n│                    │    DB     │                             │\n│                    └───────────┘                             │\n└─────────────────────────────────────────────────────────────┘ Edamame provides typed statements over soy's query builder. Each statement type encapsulates a declarative spec that maps to a parameterized SQL builder.",{"id":2798,"title":2799,"titles":2800,"content":2801,"level":118},"/v1.0.3/overview#philosophy","Philosophy",[1736],"Edamame bridges two worlds: the declarative simplicity of specs and the type safety of Go generics. Define what you want, get SQL that's validated at build time and parameterized at runtime. // Define statements as package-level variables\nvar ActiveByRole = edamame.NewQueryStatement(\"active-by-role\", \"Find active users by role\", edamame.QuerySpec{\n    Where: []edamame.ConditionSpec{\n        {Field: \"active\", Operator: \"=\", Param: \"active\"},\n        {Field: \"role\", Operator: \"=\", Param: \"role\"},\n    },\n})\n\n// In your API handler\nusers, err := exec.ExecQuery(ctx, ActiveByRole, map[string]any{\n    \"active\": true,\n    \"role\":   \"admin\",\n}) Typed statements, compile-time safety, no magic strings.",{"id":2803,"title":1443,"titles":2804,"content":2805,"level":118},"/v1.0.3/overview#statement-types",[1736],"Edamame provides five statement types for different operations: Statement TypeDescriptionQueryStatementMulti-record retrieval with filteringSelectStatementSingle-record retrievalUpdateStatementTargeted updates with SET/WHEREDeleteStatementConditional deletion with WHEREAggregateStatementCOUNT, SUM, AVG, MIN, MAX operations Each statement type accepts a spec that defines the query behavior: QuerySpec - Filtering, sorting, pagination, grouping, locking SelectSpec - Single-record filtering with optional locking UpdateSpec - SET clauses and WHERE conditions DeleteSpec - WHERE conditions for targeted deletion AggregateSpec - Field to aggregate and optional filtering",{"id":2807,"title":2808,"titles":2809,"content":103,"level":118},"/v1.0.3/overview#priorities","Priorities",[1736],{"id":2811,"title":2160,"titles":2812,"content":2813,"level":129},"/v1.0.3/overview#type-safety",[1736,2808],"Fields, operators, and params are validated at query build time. No runtime SQL injection, no magic strings. // Spec-based: field names validated against model metadata\nvar Adults = edamame.NewQueryStatement(\"adults\", \"Find adults\", edamame.QuerySpec{\n    Where: []edamame.ConditionSpec{\n        {Field: \"age\", Operator: \">=\", Param: \"min_age\"},\n    },\n})\n\n// Execution: params bound safely via sqlx\nusers, err := exec.ExecQuery(ctx, Adults, map[string]any{\n    \"min_age\": 18,  // Parameterized, never interpolated\n})",{"id":2815,"title":2816,"titles":2817,"content":2818,"level":129},"/v1.0.3/overview#compile-time-guarantees","Compile-Time Guarantees",[1736,2808],"Statements are typed. Pass a QueryStatement to ExecQuery, a SelectStatement to ExecSelect. The compiler catches mismatches. // Compiler ensures correct statement types\nusers, err := exec.ExecQuery(ctx, QueryAll, nil)       // QueryStatement\nuser, err := exec.ExecSelect(ctx, SelectByID, params)  // SelectStatement\ncount, err := exec.ExecAggregate(ctx, CountAll, nil)   // AggregateStatement",{"id":2820,"title":1834,"titles":2821,"content":2822,"level":129},"/v1.0.3/overview#security",[1736,2808],"All SQL generation flows through soy's validated builder: Field names validated against model metadataOperators validated against allowlistAll values bound as parameters, never interpolatedNo raw SQL construction from user input",{"id":2824,"title":2825,"titles":2826,"content":2827,"level":129},"/v1.0.3/overview#performance","Performance",[1736,2808],"Lazy initialization - Builders created on demandMinimal allocations - Spec-to-builder conversion is lightweightBatch operations - Insert, update, delete batches in single transactions 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 .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 .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 .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"id":2829,"title":1487,"titles":2830,"content":2831,"level":111},"/v1.0.3/learn/quickstart",[],"Get started with edamame in minutes",{"id":2833,"title":1487,"titles":2834,"content":103,"level":111},"/v1.0.3/learn/quickstart#quickstart",[],{"id":2836,"title":2837,"titles":2838,"content":2839,"level":118},"/v1.0.3/learn/quickstart#requirements","Requirements",[1487],"Go 1.24 or later.",{"id":2841,"title":2842,"titles":2843,"content":2844,"level":118},"/v1.0.3/learn/quickstart#installation","Installation",[1487],"go get github.com/zoobz-io/edamame",{"id":2846,"title":2847,"titles":2848,"content":2849,"level":118},"/v1.0.3/learn/quickstart#basic-usage","Basic Usage",[1487],"package main\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"log\"\n\n    \"github.com/jmoiron/sqlx\"\n    _ \"github.com/lib/pq\" // or mariadb, sqlite3, mssql driver\n    \"github.com/zoobz-io/astql/pkg/postgres\" // or mariadb, sqlite, mssql\n    \"github.com/zoobz-io/edamame\"\n)\n\n// Define your model with struct tags\ntype User struct {\n    ID    int    `db:\"id\" type:\"integer\" constraints:\"primarykey\"`\n    Email string `db:\"email\" type:\"text\" constraints:\"notnull,unique\"`\n    Name  string `db:\"name\" type:\"text\"`\n    Age   *int   `db:\"age\" type:\"integer\"`\n}\n\n// Define statements as package-level variables\nvar (\n    QueryAll = edamame.NewQueryStatement(\"query-all\", \"Query all users\", edamame.QuerySpec{})\n\n    SelectByID = edamame.NewSelectStatement(\"select-by-id\", \"Select user by ID\", edamame.SelectSpec{\n        Where: []edamame.ConditionSpec{{Field: \"id\", Operator: \"=\", Param: \"id\"}},\n    })\n\n    CountAll = edamame.NewAggregateStatement(\"count-all\", \"Count all users\", edamame.AggCount, edamame.AggregateSpec{})\n)\n\nfunc main() {\n    // Connect to database (PostgreSQL shown; MariaDB, SQLite, SQL Server also supported)\n    db, err := sqlx.Connect(\"postgres\", \"postgres://user:pass@localhost/mydb?sslmode=disable\")\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    // Create exec\n    exec, err := edamame.New[User](db, \"users\", postgres.New())\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    ctx := context.Background()\n\n    // Query all users\n    users, err := exec.ExecQuery(ctx, QueryAll, nil)\n    if err != nil {\n        log.Fatal(err)\n    }\n    fmt.Printf(\"Found %d users\\n\", len(users))\n\n    // Select user by ID\n    user, err := exec.ExecSelect(ctx, SelectByID, map[string]any{\"id\": 1})\n    if err != nil {\n        log.Fatal(err)\n    }\n    fmt.Printf(\"User: %s\\n\", user.Name)\n\n    // Insert a new user\n    age := 25\n    newUser := &User{\n        Email: \"alice@example.com\",\n        Name:  \"Alice\",\n        Age:   &age,\n    }\n    inserted, err := exec.ExecInsert(ctx, newUser)\n    if err != nil {\n        log.Fatal(err)\n    }\n    fmt.Printf(\"Inserted user ID: %d\\n\", inserted.ID)\n\n    // Count users\n    count, err := exec.ExecAggregate(ctx, CountAll, nil)\n    if err != nil {\n        log.Fatal(err)\n    }\n    fmt.Printf(\"Total users: %.0f\\n\", count)\n}",{"id":2851,"title":2852,"titles":2853,"content":2854,"level":118},"/v1.0.3/learn/quickstart#whats-happening","What's Happening",[1487],"New[User](db, \"users\", postgres.New()) creates a exec for the User type bound to the \"users\" table with the PostgreSQL rendererStatements are defined as package-level variables with unique names and descriptionsExecQuery returns all records matching the statement's specExecSelect returns a single record (or error if not found)ExecInsert inserts a record and returns it with generated fields (like ID)ExecAggregate runs an aggregate function and returns the result",{"id":2856,"title":2857,"titles":2858,"content":2859,"level":118},"/v1.0.3/learn/quickstart#defining-custom-statements","Defining Custom Statements",[1487],"// Define statements for your domain\nvar (\n    // Query for active adult users\n    ActiveAdults = edamame.NewQueryStatement(\"active-adults\", \"Find active users above minimum age\", edamame.QuerySpec{\n        Where: []edamame.ConditionSpec{\n            {Field: \"active\", Operator: \"=\", Param: \"active\"},\n            {Field: \"age\", Operator: \">=\", Param: \"min_age\"},\n        },\n        OrderBy: []edamame.OrderBySpec{\n            {Field: \"name\", Direction: \"asc\"},\n        },\n    })\n\n    // Update user name\n    UpdateName = edamame.NewUpdateStatement(\"update-name\", \"Update user name by ID\", edamame.UpdateSpec{\n        Set:   map[string]string{\"name\": \"new_name\"},\n        Where: []edamame.ConditionSpec{{Field: \"id\", Operator: \"=\", Param: \"id\"}},\n    })\n\n    // Delete inactive users\n    DeleteInactive = edamame.NewDeleteStatement(\"delete-inactive\", \"Delete inactive users\", edamame.DeleteSpec{\n        Where: []edamame.ConditionSpec{{Field: \"active\", Operator: \"=\", Param: \"active\"}},\n    })\n\n    // Sum of ages\n    SumAges = edamame.NewAggregateStatement(\"sum-ages\", \"Sum all user ages\", edamame.AggSum, edamame.AggregateSpec{\n        Field: \"age\",\n    })\n)\n\n// Use the statements\nusers, err := exec.ExecQuery(ctx, ActiveAdults, map[string]any{\n    \"active\":  true,\n    \"min_age\": 18,\n})\n\naffected, err := exec.ExecUpdate(ctx, UpdateName, map[string]any{\n    \"id\":       123,\n    \"new_name\": \"New Name\",\n})\n\ndeleted, err := exec.ExecDelete(ctx, DeleteInactive, map[string]any{\n    \"active\": false,\n})\n\ntotal, err := exec.ExecAggregate(ctx, SumAges, nil)",{"id":2861,"title":2862,"titles":2863,"content":2864,"level":118},"/v1.0.3/learn/quickstart#statement-parameters","Statement Parameters",[1487],"Statements automatically derive their parameters from the spec. You can inspect them: // Check statement parameters\nfor _, param := range ActiveAdults.Params() {\n    fmt.Printf(\"Param: %s (type: %s, required: %v)\\n\", param.Name, param.Type, param.Required)\n}\n// Output:\n// Param: active (type: any, required: true)\n// Param: min_age (type: any, required: true)",{"id":2866,"title":2867,"titles":2868,"content":2869,"level":118},"/v1.0.3/learn/quickstart#next-steps","Next Steps",[1487],"Core Concepts - Understand factories, statements, and specsStatement Guide - Define custom queries, updates, and moreTesting - Test your database operations 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)}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 .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 .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 .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 pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}",{"id":2871,"title":1753,"titles":2872,"content":2873,"level":111},"/v1.0.3/learn/concepts",[],"Executors, statements, and specs - the building blocks of edamame",{"id":2875,"title":1753,"titles":2876,"content":2877,"level":111},"/v1.0.3/learn/concepts#core-concepts",[],"Edamame has three primitives: executors, statements, and specs. Understanding these unlocks the full API.",{"id":2879,"title":2880,"titles":2881,"content":2882,"level":118},"/v1.0.3/learn/concepts#executor","Executor",[1753],"An executor is the execution context for a single model type. It wraps soy and provides methods to execute typed statements. exec, err := edamame.New[User](db, \"users\", renderer) The executor: Wraps a soy instance for SQL buildingProvides execution methods that accept typed statementsSupports transactions via *Tx method variants",{"id":2884,"title":2885,"titles":2886,"content":2887,"level":129},"/v1.0.3/learn/concepts#struct-tags","Struct Tags",[1753,2880],"Edamame uses struct tags to understand your model: type User struct {\n    ID    int    `db:\"id\" type:\"integer\" constraints:\"primarykey\"`\n    Email string `db:\"email\" type:\"text\" constraints:\"notnull,unique\"`\n    Name  string `db:\"name\" type:\"text\"`\n    Age   *int   `db:\"age\" type:\"integer\"`\n} TagPurposeExampledbColumn namedb:\"user_id\"typeSQL typetype:\"text\", type:\"integer\"constraintsColumn constraintsconstraints:\"primarykey,notnull\"",{"id":2889,"title":1452,"titles":2890,"content":2891,"level":118},"/v1.0.3/learn/concepts#statements",[1753],"A statement is a typed, named database operation. Define statements as package-level variables: var (\n    QueryAll = edamame.NewQueryStatement(\"query-all\", \"Query all users\", edamame.QuerySpec{})\n\n    SelectByID = edamame.NewSelectStatement(\"select-by-id\", \"Select user by ID\", edamame.SelectSpec{\n        Where: []edamame.ConditionSpec{{Field: \"id\", Operator: \"=\", Param: \"id\"}},\n    })\n) Each statement has: Name - Human-readable identifierDescription - What the statement doesID - Auto-generated UUID for uniquenessSpec - Declarative definition of the operationParams - Required parameters (auto-derived from spec)Tags - Optional metadata for categorization",{"id":2893,"title":1443,"titles":2894,"content":2895,"level":129},"/v1.0.3/learn/concepts#statement-types",[1753,1452],"TypeConstructorReturnsUse CaseQueryStatementNewQueryStatement[]*TMulti-record retrievalSelectStatementNewSelectStatement*TSingle-record retrievalUpdateStatementNewUpdateStatement*TModify and return recordDeleteStatementNewDeleteStatementint64Remove records, return countAggregateStatementNewAggregateStatementfloat64COUNT, SUM, AVG, MIN, MAX",{"id":2897,"title":2898,"titles":2899,"content":2900,"level":129},"/v1.0.3/learn/concepts#defining-statements","Defining Statements",[1753,1452],"// Query statement\nvar ByStatus = edamame.NewQueryStatement(\"by-status\", \"Find users by status\", edamame.QuerySpec{\n    Where: []edamame.ConditionSpec{\n        {Field: \"status\", Operator: \"=\", Param: \"status\"},\n    },\n}, \"user\", \"filter\") // Optional tags\n\n// Select statement\nvar ByEmail = edamame.NewSelectStatement(\"by-email\", \"Find user by email\", edamame.SelectSpec{\n    Where: []edamame.ConditionSpec{\n        {Field: \"email\", Operator: \"=\", Param: \"email\"},\n    },\n})\n\n// Update statement\nvar Activate = edamame.NewUpdateStatement(\"activate\", \"Activate a user by ID\", edamame.UpdateSpec{\n    Set: map[string]string{\"active\": \"active\"},\n    Where: []edamame.ConditionSpec{\n        {Field: \"id\", Operator: \"=\", Param: \"id\"},\n    },\n})\n\n// Delete statement\nvar DeleteByID = edamame.NewDeleteStatement(\"delete-by-id\", \"Delete user by ID\", edamame.DeleteSpec{\n    Where: []edamame.ConditionSpec{\n        {Field: \"id\", Operator: \"=\", Param: \"id\"},\n    },\n})\n\n// Aggregate statement\nvar AvgAge = edamame.NewAggregateStatement(\"avg-age\", \"Average age of all users\", edamame.AggAvg, edamame.AggregateSpec{\n    Field: \"age\",\n})",{"id":2902,"title":2903,"titles":2904,"content":2905,"level":129},"/v1.0.3/learn/concepts#statement-metadata","Statement Metadata",[1753,1452],"Inspect statement properties: fmt.Println(ByStatus.Name())        // \"by-status\"\nfmt.Println(ByStatus.Description()) // \"Find users by status\"\nfmt.Println(ByStatus.ID())          // UUID\nfmt.Println(ByStatus.Tags())        // [\"user\", \"filter\"]\n\nfor _, p := range ByStatus.Params() {\n    fmt.Printf(\"Param: %s (type: %s, required: %v)\\n\", p.Name, p.Type, p.Required)\n}",{"id":2907,"title":2908,"titles":2909,"content":2910,"level":118},"/v1.0.3/learn/concepts#specs","Specs",[1753],"A spec is a declarative definition of a database operation. Specs are pure data—no SQL strings, no builder calls.",{"id":2912,"title":169,"titles":2913,"content":2914,"level":129},"/v1.0.3/learn/concepts#queryspec",[1753,2908],"For multi-record retrieval: spec := edamame.QuerySpec{\n    Fields:     []string{\"id\", \"name\", \"email\"},  // SELECT columns (empty = all)\n    Where:      []edamame.ConditionSpec{...},     // WHERE clauses\n    OrderBy:    []edamame.OrderBySpec{...},       // ORDER BY clauses\n    GroupBy:    []string{\"status\"},               // GROUP BY columns\n    Having:     []edamame.ConditionSpec{...},     // HAVING clauses\n    Limit:      &limit,                           // LIMIT\n    Offset:     &offset,                          // OFFSET\n    Distinct:   true,                             // SELECT DISTINCT\n    ForLocking: \"update\",                         // FOR UPDATE/SHARE\n}",{"id":2916,"title":371,"titles":2917,"content":2918,"level":129},"/v1.0.3/learn/concepts#selectspec",[1753,2908],"For single-record retrieval (same structure as QuerySpec): spec := edamame.SelectSpec{\n    Fields:     []string{\"id\", \"name\"},\n    Where:      []edamame.ConditionSpec{...},\n    ForLocking: \"share\",\n}",{"id":2920,"title":2921,"titles":2922,"content":2923,"level":129},"/v1.0.3/learn/concepts#updatespec","UpdateSpec",[1753,2908],"For modifications: spec := edamame.UpdateSpec{\n    Set: map[string]string{\n        \"name\":   \"new_name\",    // field -> param mapping\n        \"status\": \"new_status\",\n    },\n    Where: []edamame.ConditionSpec{...},\n}",{"id":2925,"title":2926,"titles":2927,"content":2928,"level":129},"/v1.0.3/learn/concepts#deletespec","DeleteSpec",[1753,2908],"For deletions: spec := edamame.DeleteSpec{\n    Where: []edamame.ConditionSpec{...},\n}",{"id":2930,"title":907,"titles":2931,"content":2932,"level":129},"/v1.0.3/learn/concepts#aggregatespec",[1753,2908],"For aggregate functions: spec := edamame.AggregateSpec{\n    Field: \"age\",                           // Field to aggregate\n    Where: []edamame.ConditionSpec{...},    // Optional filter\n} Use with AggCount, AggSum, AggAvg, AggMin, or AggMax.",{"id":2934,"title":2935,"titles":2936,"content":2937,"level":118},"/v1.0.3/learn/concepts#conditions","Conditions",[1753],"Conditions define WHERE clauses. They can be simple or grouped.",{"id":2939,"title":2940,"titles":2941,"content":2942,"level":129},"/v1.0.3/learn/concepts#simple-conditions","Simple Conditions",[1753,2935],"cond := edamame.ConditionSpec{\n    Field:    \"age\",\n    Operator: \">=\",\n    Param:    \"min_age\",\n}",{"id":2944,"title":2945,"titles":2946,"content":2947,"level":129},"/v1.0.3/learn/concepts#null-conditions","NULL Conditions",[1753,2935],"// IS NULL\ncond := edamame.ConditionSpec{\n    Field:    \"deleted_at\",\n    IsNull:   true,\n    Operator: \"IS NULL\",\n}\n\n// IS NOT NULL\ncond := edamame.ConditionSpec{\n    Field:    \"email\",\n    IsNull:   true,\n    Operator: \"IS NOT NULL\",\n}",{"id":2949,"title":2950,"titles":2951,"content":2952,"level":129},"/v1.0.3/learn/concepts#grouped-conditions-or","Grouped Conditions (OR)",[1753,2935],"cond := edamame.ConditionSpec{\n    Logic: \"OR\",\n    Group: []edamame.ConditionSpec{\n        {Field: \"status\", Operator: \"=\", Param: \"status1\"},\n        {Field: \"status\", Operator: \"=\", Param: \"status2\"},\n    },\n}",{"id":2954,"title":2955,"titles":2956,"content":2957,"level":129},"/v1.0.3/learn/concepts#supported-operators","Supported Operators",[1753,2935],"OperatorDescription=Equal!=, \u003C>Not equal\u003C, \u003C=Less than>, >=Greater thanLIKE, ILIKEPattern matchingINValue in listIS NULLNULL checkIS NOT NULLNOT NULL check",{"id":2959,"title":2960,"titles":2961,"content":2962,"level":118},"/v1.0.3/learn/concepts#ordering","Ordering",[1753],"order := edamame.OrderBySpec{\n    Field:     \"created_at\",\n    Direction: \"desc\",      // \"asc\" or \"desc\"\n    Nulls:     \"last\",      // \"first\" or \"last\" (optional)\n}",{"id":2964,"title":2965,"titles":2966,"content":2967,"level":129},"/v1.0.3/learn/concepts#expression-based-ordering","Expression-Based Ordering",[1753,2960],"For vector similarity or computed distances: order := edamame.OrderBySpec{\n    Field:     \"embedding\",\n    Operator:  \"\u003C->\",           // pgvector distance operator\n    Param:     \"query_vector\",\n    Direction: \"asc\",\n}",{"id":2969,"title":2970,"titles":2971,"content":2972,"level":118},"/v1.0.3/learn/concepts#execution","Execution",[1753],"Execute statements with params: // Query (multiple records)\nusers, err := exec.ExecQuery(ctx, ByStatus, map[string]any{\n    \"status\": \"active\",\n})\n\n// Select (single record)\nuser, err := exec.ExecSelect(ctx, ByEmail, map[string]any{\n    \"email\": \"alice@example.com\",\n})\n\n// Update\nupdated, err := exec.ExecUpdate(ctx, Activate, map[string]any{\n    \"id\":     123,\n    \"active\": true,\n})\n\n// Delete\ncount, err := exec.ExecDelete(ctx, DeleteByID, map[string]any{\n    \"id\": 123,\n})\n\n// Aggregate\navg, err := exec.ExecAggregate(ctx, AvgAge, nil)\n\n// Insert (no statement needed)\ninserted, err := exec.ExecInsert(ctx, &user)",{"id":2974,"title":2975,"titles":2976,"content":2977,"level":129},"/v1.0.3/learn/concepts#transaction-support","Transaction Support",[1753,2970],"All execution methods have *Tx variants: tx, err := db.BeginTxx(ctx, nil)\n\nuser, err := exec.ExecSelectTx(ctx, tx, SelectByID, params)\n_, err = exec.ExecUpdateTx(ctx, tx, Activate, params)\n\ntx.Commit()",{"id":2979,"title":2980,"titles":2981,"content":2982,"level":129},"/v1.0.3/learn/concepts#batch-operations","Batch Operations",[1753,2970],"// Batch insert\ncount, err := exec.ExecInsertBatch(ctx, users)\n\n// Batch update\ncount, err := exec.ExecUpdateBatch(ctx, Activate, []map[string]any{\n    {\"id\": 1, \"active\": true},\n    {\"id\": 2, \"active\": true},\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 .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 .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .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 .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}",{"id":2984,"title":1501,"titles":2985,"content":2986,"level":111},"/v1.0.3/learn/architecture",[],"How edamame works with soy for SQL generation",{"id":2988,"title":1501,"titles":2989,"content":2990,"level":111},"/v1.0.3/learn/architecture#architecture",[],"Edamame is a semantic layer over soy. Understanding this relationship helps you use both effectively.",{"id":2992,"title":2993,"titles":2994,"content":2995,"level":118},"/v1.0.3/learn/architecture#layer-diagram","Layer Diagram",[1501],"┌─────────────────────────────────────────────────────────────┐\n│                      Your Application                       │\n│                                                             │\n│   exec.ExecQuery(ctx, ByStatus, params)                     │\n└─────────────────────────┬───────────────────────────────────┘\n                          │\n┌─────────────────────────▼───────────────────────────────────┐\n│                         Edamame                             │\n│                                                             │\n│  ┌──────────────┐  Typed statements                         │\n│  │ Executor[T]  │  Spec → Builder conversion                │\n│  │              │  Event emission (capitan)                 │\n│  └──────┬───────┘                                           │\n│         │                                                   │\n│  ┌──────▼───────────────────────────────────────────────┐   │\n│  │                  Statement Types                      │   │\n│  │  QueryStatement  SelectStatement  UpdateStatement     │   │\n│  │  DeleteStatement  AggregateStatement                  │   │\n│  └──────────────────────────────────────────────────────┘   │\n└─────────┬───────────────────────────────────────────────────┘\n          │\n┌─────────▼───────────────────────────────────────────────────┐\n│                          Soy                                │\n│                                                             │\n│  Query builder API                                          │\n│  Field validation (via ASTQL)                               │\n│  Parameterized SQL generation                               │\n│  Render() → {SQL, Params}                                   │\n└─────────┬───────────────────────────────────────────────────┘\n          │\n┌─────────▼───────────────────────────────────────────────────┐\n│                          sqlx                               │\n│                                                             │\n│  Connection pooling                                         │\n│  Named parameter binding                                    │\n│  Struct scanning                                            │\n└─────────────────────────────────────────────────────────────┘",{"id":2997,"title":2998,"titles":2999,"content":103,"level":118},"/v1.0.3/learn/architecture#responsibilities","Responsibilities",[1501],{"id":3001,"title":3002,"titles":3003,"content":3004,"level":129},"/v1.0.3/learn/architecture#edamame","Edamame",[1501,2998],"Typed statements - Compile-time safe query definitionsSpec-to-builder conversion - Transform declarative specs into soy buildersExecution wrappers - Convenient Exec* methods with paramsEvents - Emit executor lifecycle events via capitan",{"id":3006,"title":3007,"titles":3008,"content":3009,"level":129},"/v1.0.3/learn/architecture#soy","Soy",[1501,2998],"Query building - Fluent API for constructing SQLField validation - Validate field names against model metadataOperator validation - Ensure operators are safe and validSQL generation - Render builders to parameterized SQLExecution - Execute queries via sqlx",{"id":3011,"title":3012,"titles":3013,"content":3014,"level":129},"/v1.0.3/learn/architecture#when-to-use-each","When to Use Each",[1501,2998],"TaskUseDefine named operationsStatement typesExecute named operationsexec.Exec* methodsAd-hoc queriesexec.Soy().Query()...Custom SQL constructionSoy builder API",{"id":3016,"title":3017,"titles":3018,"content":3019,"level":118},"/v1.0.3/learn/architecture#spec-to-builder-flow","Spec-to-Builder Flow",[1501],"When you call exec.ExecQuery(ctx, ByStatus, params): Extract spec - Statement contains the spec internallyConvert - Spec is converted to a soy builder:\n// QuerySpec → soy.Query[T]\nbuilder := exec.queryFromSpec(stmt.spec)\nRender - Builder generates SQL:\nresult, err := builder.Render()\n// result.SQL = \"SELECT ... FROM users WHERE status = $1\"\n// result.Params = []any{\"active\"}\nExecute - sqlx runs the parameterized query:\nrows, err := db.QueryxContext(ctx, result.SQL, result.Params...)\nScan - Results are scanned into structs",{"id":3021,"title":3022,"titles":3023,"content":3024,"level":118},"/v1.0.3/learn/architecture#security-model","Security Model",[1501],"SQL injection protection flows through the entire stack: Edamame - Specs are data, not SQL stringsSoy - Field names validated against model metadataSoy - Operators validated against allowlistSoy - All values become bound parameterssqlx - Parameters passed separately from SQL // User input\nparams := map[string]any{\"status\": userInput}\n\n// Never interpolated into SQL\nexec.ExecQuery(ctx, ByStatus, params)\n\n// Becomes:\n// SQL: \"SELECT ... WHERE status = $1\"\n// Args: [userInput]  // Bound separately",{"id":3026,"title":3027,"titles":3028,"content":3029,"level":118},"/v1.0.3/learn/architecture#event-integration","Event Integration",[1501],"Edamame emits events via capitan for observability: SignalWhenFieldsExecutorCreatedExecutor initializedtable Hook for monitoring: capitan.Hook(edamame.ExecutorCreated, func(ctx context.Context, e *capitan.Event) {\n    table, _ := edamame.KeyTable.From(e)\n    log.Printf(\"Executor created for table: %s\", table)\n})",{"id":3031,"title":3032,"titles":3033,"content":3034,"level":118},"/v1.0.3/learn/architecture#direct-soy-access","Direct Soy Access",[1501],"For operations not covered by statements, access soy directly: // Get the underlying soy instance\ns := exec.Soy()\n\n// Use soy's fluent API\nusers, err := s.Query().\n    Where(\"age\", \">=\", \"min_age\").\n    Where(\"status\", \"=\", \"status\").\n    OrderBy(\"name\", \"asc\").\n    Limit(10).\n    Exec(ctx, params) This bypasses statement types but still benefits from: Field validationParameterized queriesType-safe results",{"id":3036,"title":2160,"titles":3037,"content":3038,"level":118},"/v1.0.3/learn/architecture#type-safety",[1501],"Statements provide compile-time guarantees: // Compiler enforces correct statement types\nusers, err := exec.ExecQuery(ctx, QueryAll, nil)       // Must be QueryStatement\nuser, err := exec.ExecSelect(ctx, SelectByID, params)  // Must be SelectStatement\ncount, err := exec.ExecAggregate(ctx, CountAll, nil)   // Must be AggregateStatement\n\n// This won't compile:\n// exec.ExecQuery(ctx, SelectByID, nil)  // SelectStatement not allowed No magic strings, no runtime lookup failures. html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .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 .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 .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}",{"id":3040,"title":1452,"titles":3041,"content":3042,"level":111},"/v1.0.3/guides/statements",[],"Defining and using typed database statements",{"id":3044,"title":1452,"titles":3045,"content":3046,"level":111},"/v1.0.3/guides/statements#statements",[],"This guide covers defining typed statements for your database operations.",{"id":3048,"title":3049,"titles":3050,"content":3051,"level":118},"/v1.0.3/guides/statements#queries","Queries",[1452],"Queries return multiple records. Use for lists, search results, and filtered collections.",{"id":3053,"title":3054,"titles":3055,"content":3056,"level":129},"/v1.0.3/guides/statements#basic-query","Basic Query",[1452,3049],"var ActiveUsers = edamame.NewQueryStatement(\"active-users\", \"Find all active users\", edamame.QuerySpec{\n    Where: []edamame.ConditionSpec{\n        {Field: \"active\", Operator: \"=\", Param: \"active\"},\n    },\n})\n\n// Usage\nusers, err := exec.ExecQuery(ctx, ActiveUsers, map[string]any{\n    \"active\": true,\n})",{"id":3058,"title":3059,"titles":3060,"content":3061,"level":129},"/v1.0.3/guides/statements#with-ordering-and-pagination","With Ordering and Pagination",[1452,3049],"limit := 20\noffset := 0\n\nvar RecentUsers = edamame.NewQueryStatement(\"recent-users\", \"Get users ordered by creation date\", edamame.QuerySpec{\n    OrderBy: []edamame.OrderBySpec{\n        {Field: \"created_at\", Direction: \"desc\"},\n    },\n    Limit:  &limit,\n    Offset: &offset,\n})",{"id":3063,"title":3064,"titles":3065,"content":3066,"level":129},"/v1.0.3/guides/statements#with-field-selection","With Field Selection",[1452,3049],"var UserNames = edamame.NewQueryStatement(\"user-names\", \"Get user names only\", edamame.QuerySpec{\n    Fields: []string{\"id\", \"name\"},  // Only select these columns\n})",{"id":3068,"title":3069,"titles":3070,"content":3071,"level":129},"/v1.0.3/guides/statements#with-grouping","With Grouping",[1452,3049],"var UsersByRole = edamame.NewQueryStatement(\"users-by-role\", \"Group users by role\", edamame.QuerySpec{\n    Fields:  []string{\"role\", \"COUNT(*) as count\"},\n    GroupBy: []string{\"role\"},\n})",{"id":3073,"title":3074,"titles":3075,"content":3076,"level":129},"/v1.0.3/guides/statements#having-with-aggregates","HAVING with Aggregates",[1452,3049],"Use HavingAgg for aggregate conditions in HAVING clauses: var PopularRoles = edamame.NewQueryStatement(\"popular-roles\", \"Roles with minimum user count\", edamame.QuerySpec{\n    Fields:  []string{\"role\"},\n    GroupBy: []string{\"role\"},\n    HavingAgg: []edamame.HavingAggSpec{\n        {Func: \"count\", Field: \"*\", Operator: \">=\", Param: \"min_count\"},\n    },\n})\n\n// Generates: SELECT role FROM users GROUP BY role HAVING COUNT(*) >= $1 Multiple aggregate conditions: var HighValueRoles = edamame.NewQueryStatement(\"high-value-roles\", \"Roles with high balance and count\", edamame.QuerySpec{\n    Fields:  []string{\"role\"},\n    GroupBy: []string{\"role\"},\n    HavingAgg: []edamame.HavingAggSpec{\n        {Func: \"count\", Field: \"*\", Operator: \">=\", Param: \"min_count\"},\n        {Func: \"sum\", Field: \"balance\", Operator: \">=\", Param: \"min_total\"},\n    },\n})",{"id":3078,"title":3079,"titles":3080,"content":3081,"level":129},"/v1.0.3/guides/statements#distinct-on-postgresql","DISTINCT ON (PostgreSQL)",[1452,3049],"Use DistinctOn for PostgreSQL's DISTINCT ON clause: var LatestPerUser = edamame.NewQueryStatement(\"latest-per-user\", \"Latest record per user\", edamame.QuerySpec{\n    DistinctOn: []string{\"user_id\"},\n    OrderBy: []edamame.OrderBySpec{\n        {Field: \"user_id\", Direction: \"asc\"},\n        {Field: \"created_at\", Direction: \"desc\"},\n    },\n})\n\n// Generates: SELECT DISTINCT ON (user_id) * FROM ... ORDER BY user_id ASC, created_at DESC",{"id":3083,"title":3084,"titles":3085,"content":3086,"level":129},"/v1.0.3/guides/statements#complex-conditions","Complex Conditions",[1452,3049],"var FilteredUsers = edamame.NewQueryStatement(\"filtered-users\", \"Filter users by age and role\", edamame.QuerySpec{\n    Where: []edamame.ConditionSpec{\n        {Field: \"age\", Operator: \">=\", Param: \"min_age\"},\n        {Field: \"age\", Operator: \"\u003C=\", Param: \"max_age\"},\n        {\n            Logic: \"OR\",\n            Group: []edamame.ConditionSpec{\n                {Field: \"role\", Operator: \"=\", Param: \"role1\"},\n                {Field: \"role\", Operator: \"=\", Param: \"role2\"},\n            },\n        },\n    },\n})\n\n// Generates: WHERE age >= $1 AND age \u003C= $2 AND (role = $3 OR role = $4)",{"id":3088,"title":3089,"titles":3090,"content":3091,"level":129},"/v1.0.3/guides/statements#between-conditions","BETWEEN Conditions",[1452,3049],"var UsersInAgeRange = edamame.NewQueryStatement(\"users-in-age-range\", \"Users in age range\", edamame.QuerySpec{\n    Where: []edamame.ConditionSpec{\n        {Field: \"age\", Between: true, LowParam: \"min_age\", HighParam: \"max_age\"},\n    },\n})\n\n// Generates: WHERE age BETWEEN $1 AND $2\n\n// NOT BETWEEN\nvar UsersOutsideRange = edamame.NewQueryStatement(\"users-outside-range\", \"Users outside age range\", edamame.QuerySpec{\n    Where: []edamame.ConditionSpec{\n        {Field: \"age\", NotBetween: true, LowParam: \"min_age\", HighParam: \"max_age\"},\n    },\n})\n\n// Generates: WHERE age NOT BETWEEN $1 AND $2",{"id":3093,"title":3094,"titles":3095,"content":3096,"level":129},"/v1.0.3/guides/statements#field-to-field-comparisons","Field-to-Field Comparisons",[1452,3049],"Compare two columns directly without parameters: var ModifiedAfterCreated = edamame.NewQueryStatement(\"modified-after-created\", \"Records updated after creation\", edamame.QuerySpec{\n    Where: []edamame.ConditionSpec{\n        {Field: \"updated_at\", Operator: \">\", RightField: \"created_at\"},\n    },\n})\n\n// Generates: WHERE updated_at > created_at",{"id":3098,"title":3099,"titles":3100,"content":3101,"level":129},"/v1.0.3/guides/statements#parameterized-pagination","Parameterized Pagination",[1452,3049],"Use parameter-driven limits and offsets for flexible pagination: var PaginatedUsers = edamame.NewQueryStatement(\"paginated-users\", \"Paginated user list\", edamame.QuerySpec{\n    LimitParam:  \"page_size\",\n    OffsetParam: \"offset\",\n    OrderBy: []edamame.OrderBySpec{\n        {Field: \"created_at\", Direction: \"desc\"},\n    },\n})\n\n// Usage\nusers, err := exec.ExecQuery(ctx, PaginatedUsers, map[string]any{\n    \"page_size\": 20,\n    \"offset\":    40,  // Page 3\n})",{"id":3103,"title":3104,"titles":3105,"content":3106,"level":129},"/v1.0.3/guides/statements#select-expressions","Select Expressions",[1452,3049],"Add computed columns using SQL functions: var UsersWithComputed = edamame.NewQueryStatement(\"users-with-computed\", \"Users with computed columns\", edamame.QuerySpec{\n    Fields: []string{\"id\", \"name\"},\n    SelectExprs: []edamame.SelectExprSpec{\n        {Func: \"upper\", Field: \"name\", Alias: \"upper_name\"},\n        {Func: \"length\", Field: \"email\", Alias: \"email_length\"},\n        {Func: \"count_star\", Alias: \"total\"},\n        {Func: \"now\", Alias: \"query_time\"},\n    },\n    GroupBy: []string{\"id\", \"name\"},\n}) Available functions include: upper, lower, length, trim, concat, abs, ceil, floor, round, now, current_date, cast, count, sum, avg, min, max, coalesce, nullif.",{"id":3108,"title":3109,"titles":3110,"content":3111,"level":129},"/v1.0.3/guides/statements#with-row-locking","With Row Locking",[1452,3049],"var ForUpdate = edamame.NewQueryStatement(\"for-update\", \"Query with row lock\", edamame.QuerySpec{\n    Where:      []edamame.ConditionSpec{{Field: \"status\", Operator: \"=\", Param: \"status\"}},\n    ForLocking: \"update\",  // FOR UPDATE\n}) Locking options: \"update\", \"no_key_update\", \"share\", \"key_share\"",{"id":3113,"title":3114,"titles":3115,"content":3116,"level":118},"/v1.0.3/guides/statements#selects","Selects",[1452],"Selects return a single record. Use for lookups by unique identifier.",{"id":3118,"title":3119,"titles":3120,"content":3121,"level":129},"/v1.0.3/guides/statements#by-unique-field","By Unique Field",[1452,3114],"var ByEmail = edamame.NewSelectStatement(\"by-email\", \"Find user by email address\", edamame.SelectSpec{\n    Where: []edamame.ConditionSpec{\n        {Field: \"email\", Operator: \"=\", Param: \"email\"},\n    },\n})\n\n// Usage - returns error if not found\nuser, err := exec.ExecSelect(ctx, ByEmail, map[string]any{\n    \"email\": \"alice@example.com\",\n})",{"id":3123,"title":3124,"titles":3125,"content":3126,"level":129},"/v1.0.3/guides/statements#with-locking","With Locking",[1452,3114],"var ForShare = edamame.NewSelectStatement(\"for-share\", \"Select with shared lock\", edamame.SelectSpec{\n    Where:      []edamame.ConditionSpec{{Field: \"id\", Operator: \"=\", Param: \"id\"}},\n    ForLocking: \"share\",  // FOR SHARE\n})",{"id":3128,"title":3129,"titles":3130,"content":3131,"level":118},"/v1.0.3/guides/statements#updates","Updates",[1452],"Updates modify records and return the updated row.",{"id":3133,"title":3134,"titles":3135,"content":3136,"level":129},"/v1.0.3/guides/statements#single-field-update","Single Field Update",[1452,3129],"var Activate = edamame.NewUpdateStatement(\"activate\", \"Activate a user by ID\", edamame.UpdateSpec{\n    Set: map[string]string{\n        \"active\": \"active\",  // field -> param\n    },\n    Where: []edamame.ConditionSpec{\n        {Field: \"id\", Operator: \"=\", Param: \"id\"},\n    },\n})\n\n// Usage\nupdated, err := exec.ExecUpdate(ctx, Activate, map[string]any{\n    \"id\":     123,\n    \"active\": true,\n})",{"id":3138,"title":3139,"titles":3140,"content":3141,"level":129},"/v1.0.3/guides/statements#multi-field-update","Multi-Field Update",[1452,3129],"var UpdateProfile = edamame.NewUpdateStatement(\"update-profile\", \"Update user profile\", edamame.UpdateSpec{\n    Set: map[string]string{\n        \"name\":  \"new_name\",\n        \"email\": \"new_email\",\n        \"bio\":   \"new_bio\",\n    },\n    Where: []edamame.ConditionSpec{\n        {Field: \"id\", Operator: \"=\", Param: \"id\"},\n    },\n})",{"id":3143,"title":3144,"titles":3145,"content":3146,"level":129},"/v1.0.3/guides/statements#batch-updates","Batch Updates",[1452,3129],"// Execute same update with different params\ncount, err := exec.ExecUpdateBatch(ctx, Activate, []map[string]any{\n    {\"id\": 1, \"active\": true},\n    {\"id\": 2, \"active\": true},\n    {\"id\": 3, \"active\": false},\n})",{"id":3148,"title":3149,"titles":3150,"content":3151,"level":118},"/v1.0.3/guides/statements#deletes","Deletes",[1452],"Deletes remove records and return the count of deleted rows.",{"id":3153,"title":3154,"titles":3155,"content":3156,"level":129},"/v1.0.3/guides/statements#by-single-field","By Single Field",[1452,3149],"var ByStatus = edamame.NewDeleteStatement(\"by-status\", \"Delete all users with given status\", edamame.DeleteSpec{\n    Where: []edamame.ConditionSpec{\n        {Field: \"status\", Operator: \"=\", Param: \"status\"},\n    },\n})\n\n// Usage\ncount, err := exec.ExecDelete(ctx, ByStatus, map[string]any{\n    \"status\": \"inactive\",\n})",{"id":3158,"title":3159,"titles":3160,"content":3161,"level":129},"/v1.0.3/guides/statements#with-multiple-conditions","With Multiple Conditions",[1452,3149],"var ExpiredSessions = edamame.NewDeleteStatement(\"expired-sessions\", \"Delete expired sessions\", edamame.DeleteSpec{\n    Where: []edamame.ConditionSpec{\n        {Field: \"expires_at\", Operator: \"\u003C\", Param: \"now\"},\n        {Field: \"active\", Operator: \"=\", Param: \"active\"},\n    },\n})",{"id":3163,"title":3164,"titles":3165,"content":3166,"level":118},"/v1.0.3/guides/statements#aggregates","Aggregates",[1452],"Aggregates compute values across records.",{"id":3168,"title":3169,"titles":3170,"content":3171,"level":129},"/v1.0.3/guides/statements#count","Count",[1452,3164],"var CountActive = edamame.NewAggregateStatement(\"count-active\", \"Count active users\", edamame.AggCount, edamame.AggregateSpec{\n    Where: []edamame.ConditionSpec{\n        {Field: \"active\", Operator: \"=\", Param: \"active\"},\n    },\n})\n\ncount, err := exec.ExecAggregate(ctx, CountActive, map[string]any{\n    \"active\": true,\n})",{"id":3173,"title":3174,"titles":3175,"content":3176,"level":129},"/v1.0.3/guides/statements#sum-avg-min-max","Sum, Avg, Min, Max",[1452,3164],"// Sum\nvar TotalBalance = edamame.NewAggregateStatement(\"total-balance\", \"Total balance\", edamame.AggSum, edamame.AggregateSpec{\n    Field: \"balance\",\n})\n\n// Average\nvar AvgAge = edamame.NewAggregateStatement(\"avg-age\", \"Average age\", edamame.AggAvg, edamame.AggregateSpec{\n    Field: \"age\",\n})\n\n// Min/Max\nvar Youngest = edamame.NewAggregateStatement(\"youngest\", \"Youngest user\", edamame.AggMin, edamame.AggregateSpec{\n    Field: \"age\",\n})",{"id":3178,"title":3179,"titles":3180,"content":3181,"level":118},"/v1.0.3/guides/statements#inserts","Inserts",[1452],"Inserts don't use statements - they're driven by struct fields: // Single insert\ninserted, err := exec.ExecInsert(ctx, &user)\n\n// Batch insert\ncount, err := exec.ExecInsertBatch(ctx, users)",{"id":3183,"title":3184,"titles":3185,"content":3186,"level":129},"/v1.0.3/guides/statements#with-conflict-handling","With Conflict Handling",[1452,3179],"For upsert patterns, use the underlying soy API: s := exec.Soy()\n\n// ON CONFLICT DO NOTHING\nresult, err := s.Insert().\n    OnConflictDoNothing(\"email\").\n    Exec(ctx, &user)\n\n// ON CONFLICT DO UPDATE\nresult, err := s.Insert().\n    OnConflict(\"email\").\n    DoUpdate(map[string]string{\"name\": \"name\"}).\n    Exec(ctx, &user)",{"id":3188,"title":3189,"titles":3190,"content":3191,"level":118},"/v1.0.3/guides/statements#compound-queries","Compound Queries",[1452],"Compound queries combine multiple SELECT statements using set operations.",{"id":3193,"title":3194,"titles":3195,"content":3196,"level":129},"/v1.0.3/guides/statements#union","UNION",[1452,3189],"spec := edamame.CompoundQuerySpec{\n    Base: edamame.QuerySpec{\n        Where: []edamame.ConditionSpec{\n            {Field: \"role\", Operator: \"=\", Param: \"role1\"},\n        },\n    },\n    Operands: []edamame.CompoundOperand{\n        {\n            Operation: \"union\",\n            Query: edamame.QuerySpec{\n                Where: []edamame.ConditionSpec{\n                    {Field: \"role\", Operator: \"=\", Param: \"role2\"},\n                },\n            },\n        },\n    },\n    OrderBy: []edamame.OrderBySpec{\n        {Field: \"name\", Direction: \"asc\"},\n    },\n}\n\nusers, err := exec.ExecCompound(ctx, spec, map[string]any{\n    \"role1\": \"admin\",\n    \"role2\": \"moderator\",\n})",{"id":3198,"title":3199,"titles":3200,"content":3201,"level":129},"/v1.0.3/guides/statements#available-operations","Available Operations",[1452,3189],"OperationDescriptionunionCombine results, remove duplicatesunion_allCombine results, keep duplicatesintersectOnly rows in both queriesintersect_allIntersection with duplicatesexceptRows in first but not secondexcept_allExcept with duplicates",{"id":3203,"title":3204,"titles":3205,"content":3206,"level":129},"/v1.0.3/guides/statements#rendering-for-inspection","Rendering for Inspection",[1452,3189],"sql, err := exec.RenderCompound(spec)\n// SELECT ... WHERE role = $1 UNION SELECT ... WHERE role = $2 ORDER BY name ASC",{"id":3208,"title":2903,"titles":3209,"content":2905,"level":118},"/v1.0.3/guides/statements#statement-metadata",[1452],{"id":3211,"title":3212,"titles":3213,"content":3214,"level":118},"/v1.0.3/guides/statements#tags","Tags",[1452],"Statements support optional tags for categorization: var ByRole = edamame.NewQueryStatement(\"by-role\", \"Find users by role\", edamame.QuerySpec{\n    Where: []edamame.ConditionSpec{\n        {Field: \"role\", Operator: \"=\", Param: \"role\"},\n    },\n}, \"user\", \"filter\", \"security\")  // Tags as variadic args",{"id":3216,"title":3217,"titles":3218,"content":3219,"level":118},"/v1.0.3/guides/statements#parameter-derivation","Parameter Derivation",[1452],"Params are automatically derived from specs: var Example = edamame.NewQueryStatement(\"example\", \"Example query\", edamame.QuerySpec{\n    Where: []edamame.ConditionSpec{\n        {Field: \"age\", Operator: \">=\", Param: \"min_age\"},\n        {Field: \"status\", Operator: \"=\", Param: \"status\"},\n    },\n})\n\n// Params auto-derived:\n// - min_age (required)\n// - status (required)\n\nfor _, p := range Example.Params() {\n    fmt.Printf(\"Param: %s (required: %v)\\n\", p.Name, p.Required)\n} html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}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 .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html pre.shiki code .suWN2, html code.shiki .suWN2{--shiki-default:var(--shiki-tag)}",{"id":3221,"title":1773,"titles":3222,"content":3223,"level":111},"/v1.0.3/guides/testing",[],"Testing strategies for edamame-based applications",{"id":3225,"title":1773,"titles":3226,"content":3227,"level":111},"/v1.0.3/guides/testing#testing",[],"Edamame provides testing utilities in the github.com/zoobz-io/edamame/testing package.",{"id":3229,"title":3230,"titles":3231,"content":103,"level":118},"/v1.0.3/guides/testing#test-helpers","Test Helpers",[1773],{"id":3233,"title":3234,"titles":3235,"content":3236,"level":129},"/v1.0.3/guides/testing#querycapture","QueryCapture",[1773,3230],"Capture rendered SQL for verification: import (\n    edamametesting \"github.com/zoobz-io/edamame/testing\"\n    \"github.com/zoobz-io/astql/pkg/postgres\"\n)\n\nfunc TestQueryRendering(t *testing.T) {\n    capture := edamametesting.NewQueryCapture()\n\n    exec, _ := edamame.New[User](nil, \"users\", postgres.New())\n\n    // Define a statement\n    var ByStatus = edamame.NewQueryStatement(\"by-status\", \"Find by status\", edamame.QuerySpec{\n        Where: []edamame.ConditionSpec{{Field: \"status\", Operator: \"=\", Param: \"status\"}},\n    })\n\n    // Get the builder and render\n    q, _ := exec.Query(ByStatus)\n    result, _ := q.Render()\n\n    capture.CaptureQuery(\"by-status\", \"query\", result.SQL, nil)\n\n    // Verify\n    if capture.Count() != 1 {\n        t.Errorf(\"expected 1 query, got %d\", capture.Count())\n    }\n\n    last := capture.Last()\n    if last.Type != \"query\" {\n        t.Errorf(\"expected type 'query', got %q\", last.Type)\n    }\n}",{"id":3238,"title":3239,"titles":3240,"content":3241,"level":129},"/v1.0.3/guides/testing#executoreventcapture","ExecutorEventCapture",[1773,3230],"Capture executor creation events via capitan: func TestExecutorEvents(t *testing.T) {\n    c := capitan.New(capitan.WithSyncMode())\n    defer c.Shutdown()\n\n    capture := edamametesting.NewExecutorEventCapture()\n    c.Hook(edamame.ExecutorCreated, capture.Handler())\n\n    exec, _ := edamame.New[User](nil, \"users\", postgres.New())\n\n    // Verify event captured\n    if capture.Count() != 1 {\n        t.Error(\"expected executor created event\")\n    }\n\n    tables := capture.Tables()\n    if tables[0].Table != \"users\" {\n        t.Errorf(\"expected table 'users', got %q\", tables[0].Table)\n    }\n}",{"id":3243,"title":3244,"titles":3245,"content":3246,"level":129},"/v1.0.3/guides/testing#parambuilder","ParamBuilder",[1773,3230],"Build test parameters fluently: func TestWithParams(t *testing.T) {\n    params := edamametesting.NewParamBuilder().\n        Set(\"id\", 123).\n        Set(\"status\", \"active\").\n        Set(\"limit\", 10).\n        Build()\n\n    var Filtered = edamame.NewQueryStatement(\"filtered\", \"Filtered query\", edamame.QuerySpec{\n        Where: []edamame.ConditionSpec{{Field: \"status\", Operator: \"=\", Param: \"status\"}},\n    })\n\n    // Use in tests\n    users, err := exec.ExecQuery(ctx, Filtered, params)\n}",{"id":3248,"title":3249,"titles":3250,"content":3251,"level":118},"/v1.0.3/guides/testing#unit-testing-without-database","Unit Testing Without Database",[1773],"Test statement creation and specs without a database: func TestStatements(t *testing.T) {\n    // nil db is valid for testing statements\n    exec, err := edamame.New[User](nil, \"users\", postgres.New())\n    if err != nil {\n        t.Fatal(err)\n    }\n\n    // Define statements\n    var QueryAll = edamame.NewQueryStatement(\"query-all\", \"Query all\", edamame.QuerySpec{})\n\n    var ByStatus = edamame.NewQueryStatement(\"by-status\", \"Find by status\", edamame.QuerySpec{\n        Where: []edamame.ConditionSpec{\n            {Field: \"status\", Operator: \"=\", Param: \"status\"},\n        },\n    })\n\n    // Test statement metadata\n    if ByStatus.Name() != \"by-status\" {\n        t.Errorf(\"expected name 'by-status', got %q\", ByStatus.Name())\n    }\n\n    // Test params derived correctly\n    params := ByStatus.Params()\n    if len(params) != 1 || params[0].Name != \"status\" {\n        t.Error(\"expected 1 param named 'status'\")\n    }\n\n    // Test builder creation\n    q, err := exec.Query(ByStatus)\n    if err != nil {\n        t.Fatal(err)\n    }\n\n    // Test rendering\n    result, err := q.Render()\n    if err != nil {\n        t.Fatal(err)\n    }\n\n    if result.SQL == \"\" {\n        t.Error(\"empty SQL rendered\")\n    }\n}",{"id":3253,"title":3254,"titles":3255,"content":3256,"level":118},"/v1.0.3/guides/testing#testing-sql-rendering","Testing SQL Rendering",[1773],"Verify generated SQL without executing: func TestSQLRendering(t *testing.T) {\n    exec, _ := edamame.New[User](nil, \"users\", postgres.New())\n\n    var Adults = edamame.NewQueryStatement(\"adults\", \"Find adults\", edamame.QuerySpec{\n        Where: []edamame.ConditionSpec{\n            {Field: \"age\", Operator: \">=\", Param: \"min_age\"},\n        },\n        OrderBy: []edamame.OrderBySpec{\n            {Field: \"name\", Direction: \"asc\"},\n        },\n    })\n\n    q, _ := exec.Query(Adults)\n    result, err := q.Render()\n    if err != nil {\n        t.Fatal(err)\n    }\n\n    // Verify SQL structure\n    if !strings.Contains(result.SQL, \"WHERE\") {\n        t.Error(\"SQL missing WHERE clause\")\n    }\n    if !strings.Contains(result.SQL, \"ORDER BY\") {\n        t.Error(\"SQL missing ORDER BY clause\")\n    }\n}",{"id":3258,"title":3259,"titles":3260,"content":3261,"level":118},"/v1.0.3/guides/testing#integration-testing-with-testcontainers","Integration Testing with Testcontainers",[1773],"For database integration tests, use testcontainers: //go:build integration\n\npackage integration\n\nimport (\n    \"context\"\n    \"testing\"\n\n    \"github.com/testcontainers/testcontainers-go\"\n    \"github.com/testcontainers/testcontainers-go/wait\"\n)\n\nfunc TestWithPostgres(t *testing.T) {\n    ctx := context.Background()\n\n    // Start PostgreSQL container\n    req := testcontainers.ContainerRequest{\n        Image:        \"postgres:16-alpine\",\n        ExposedPorts: []string{\"5432/tcp\"},\n        WaitingFor:   wait.ForLog(\"database system is ready to accept connections\"),\n        Env: map[string]string{\n            \"POSTGRES_USER\":     \"test\",\n            \"POSTGRES_PASSWORD\": \"test\",\n            \"POSTGRES_DB\":       \"testdb\",\n        },\n    }\n\n    container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{\n        ContainerRequest: req,\n        Started:          true,\n    })\n    if err != nil {\n        t.Fatal(err)\n    }\n    defer container.Terminate(ctx)\n\n    // Get connection details\n    host, _ := container.Host(ctx)\n    port, _ := container.MappedPort(ctx, \"5432\")\n\n    // Connect and test\n    dsn := fmt.Sprintf(\"host=%s port=%s user=test password=test dbname=testdb sslmode=disable\", host, port.Port())\n    db, err := sqlx.Connect(\"postgres\", dsn)\n    if err != nil {\n        t.Fatal(err)\n    }\n\n    // Create table\n    db.ExecContext(ctx, `CREATE TABLE users (\n        id SERIAL PRIMARY KEY,\n        email TEXT NOT NULL UNIQUE,\n        name TEXT,\n        age INTEGER\n    )`)\n\n    // Test executor\n    exec, _ := edamame.New[User](db, \"users\", postgres.New())\n\n    age := 25\n    user := &User{Email: \"test@example.com\", Name: \"Test\", Age: &age}\n    inserted, err := exec.ExecInsert(ctx, user)\n    if err != nil {\n        t.Fatal(err)\n    }\n\n    if inserted.ID == 0 {\n        t.Error(\"expected non-zero ID\")\n    }\n} Run integration tests: go test -tags=integration ./testing/integration/...",{"id":3263,"title":3264,"titles":3265,"content":3266,"level":118},"/v1.0.3/guides/testing#benchmarking","Benchmarking",[1773],"The testing/benchmarks package provides performance benchmarks: func BenchmarkQueryBuilding(b *testing.B) {\n    exec, _ := edamame.New[User](nil, \"users\", postgres.New())\n\n    var QueryAll = edamame.NewQueryStatement(\"query-all\", \"Query all\", edamame.QuerySpec{})\n\n    b.ResetTimer()\n    b.ReportAllocs()\n\n    for i := 0; i \u003C b.N; i++ {\n        _, _ = exec.Query(QueryAll)\n    }\n} Run benchmarks: go test ./testing/benchmarks/... -bench=. -benchmem",{"id":3268,"title":3269,"titles":3270,"content":3271,"level":118},"/v1.0.3/guides/testing#testing-events","Testing Events",[1773],"Verify capitan events are emitted correctly: func TestExecutorEmitsCreatedEvent(t *testing.T) {\n    c := capitan.New(capitan.WithSyncMode())\n    defer c.Shutdown()\n\n    capture := edamametesting.NewExecutorEventCapture()\n    c.Hook(edamame.ExecutorCreated, capture.Handler())\n\n    _, _ = edamame.New[User](nil, \"users\", postgres.New())\n\n    if capture.Count() != 1 {\n        t.Errorf(\"expected 1 executor created event, got %d\", capture.Count())\n    }\n\n    tables := capture.Tables()\n    if tables[0].Table != \"users\" {\n        t.Errorf(\"expected table 'users', got %q\", tables[0].Table)\n    }\n}",{"id":3273,"title":3274,"titles":3275,"content":3276,"level":118},"/v1.0.3/guides/testing#testing-transaction-behavior","Testing Transaction Behavior",[1773],"func TestTransaction(t *testing.T) {\n    // ... setup db and exec ...\n\n    var QueryAll = edamame.NewQueryStatement(\"query-all\", \"Query all\", edamame.QuerySpec{})\n    var SelectByID = edamame.NewSelectStatement(\"select-by-id\", \"Select by ID\", edamame.SelectSpec{\n        Where: []edamame.ConditionSpec{{Field: \"id\", Operator: \"=\", Param: \"id\"}},\n    })\n\n    tx, _ := db.BeginTxx(ctx, nil)\n\n    // Insert in transaction\n    user := &User{Email: \"tx@test.com\", Name: \"TxTest\"}\n    inserted, err := exec.ExecInsertTx(ctx, tx, user)\n    if err != nil {\n        tx.Rollback()\n        t.Fatal(err)\n    }\n\n    // Verify visible in transaction\n    users, _ := exec.ExecQueryTx(ctx, tx, QueryAll, nil)\n    if len(users) != 1 {\n        t.Error(\"expected 1 user in transaction\")\n    }\n\n    // Rollback\n    tx.Rollback()\n\n    // Verify not visible after rollback\n    users, _ = exec.ExecQuery(ctx, QueryAll, nil)\n    if len(users) != 0 {\n        t.Error(\"expected 0 users after rollback\")\n    }\n}",{"id":3278,"title":3279,"titles":3280,"content":3281,"level":118},"/v1.0.3/guides/testing#async-event-testing","Async Event Testing",[1773],"Use WaitForCount for async event verification: func TestAsyncEvents(t *testing.T) {\n    c := capitan.New()  // async mode\n    defer c.Shutdown()\n\n    capture := edamametesting.NewExecutorEventCapture()\n    c.Hook(edamame.ExecutorCreated, capture.Handler())\n\n    _, _ = edamame.New[User](nil, \"users\", postgres.New())\n\n    // Wait for async event processing\n    if !capture.WaitForCount(1, 500*time.Millisecond) {\n        t.Error(\"timed out waiting for event\")\n    }\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 pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .skxcq, html code.shiki .skxcq{--shiki-default:var(--shiki-builtin)}",{"id":3283,"title":3284,"titles":3285,"content":3286,"level":111},"/v1.0.3/cookbook/llm-integration","LLM Integration",[],"Using edamame statements with AI assistants",{"id":3288,"title":3284,"titles":3289,"content":3290,"level":111},"/v1.0.3/cookbook/llm-integration#llm-integration",[],"Edamame's typed statements with self-describing metadata make it ideal for AI-assisted database operations.",{"id":3292,"title":3293,"titles":3294,"content":3295,"level":118},"/v1.0.3/cookbook/llm-integration#the-pattern","The Pattern",[3284],"Define statements with descriptive names and descriptionsCollect statement metadata into a registrySerialize registry as JSON for LLM contextLLM selects statement by name and provides paramsExecute statement with LLM-provided inputs User Query → LLM (with statement metadata) → Statement Name + Params → Edamame → Results",{"id":3297,"title":2898,"titles":3298,"content":3299,"level":118},"/v1.0.3/cookbook/llm-integration#defining-statements",[3284],"Statements are self-describing with name, description, params, and tags: var (\n    QueryAll = edamame.NewQueryStatement(\"query-all\", \"Query all users\", edamame.QuerySpec{})\n\n    ByRole = edamame.NewQueryStatement(\"by-role\", \"Find users by role\", edamame.QuerySpec{\n        Where: []edamame.ConditionSpec{\n            {Field: \"role\", Operator: \"=\", Param: \"role\"},\n        },\n    }, \"filter\", \"security\")\n\n    ActiveAdults = edamame.NewQueryStatement(\"active-adults\", \"Find active users over 18\", edamame.QuerySpec{\n        Where: []edamame.ConditionSpec{\n            {Field: \"active\", Operator: \"=\", Param: \"active\"},\n            {Field: \"age\", Operator: \">=\", Param: \"min_age\"},\n        },\n    }, \"filter\")\n\n    SelectByID = edamame.NewSelectStatement(\"select-by-id\", \"Select a single user by ID\", edamame.SelectSpec{\n        Where: []edamame.ConditionSpec{\n            {Field: \"id\", Operator: \"=\", Param: \"id\"},\n        },\n    })\n\n    CountAll = edamame.NewAggregateStatement(\"count-all\", \"Count all users\", edamame.AggCount, edamame.AggregateSpec{})\n)",{"id":3301,"title":3302,"titles":3303,"content":3304,"level":118},"/v1.0.3/cookbook/llm-integration#building-a-statement-registry","Building a Statement Registry",[3284],"Create a registry to collect statements for LLM consumption: type StatementInfo struct {\n    Name        string      `json:\"name\"`\n    Description string      `json:\"description\"`\n    Type        string      `json:\"type\"`\n    Params      []ParamInfo `json:\"params\"`\n    Tags        []string    `json:\"tags,omitempty\"`\n}\n\ntype ParamInfo struct {\n    Name     string `json:\"name\"`\n    Type     string `json:\"type\"`\n    Required bool   `json:\"required\"`\n}\n\ntype StatementRegistry struct {\n    Table      string          `json:\"table\"`\n    Queries    []StatementInfo `json:\"queries,omitempty\"`\n    Selects    []StatementInfo `json:\"selects,omitempty\"`\n    Updates    []StatementInfo `json:\"updates,omitempty\"`\n    Deletes    []StatementInfo `json:\"deletes,omitempty\"`\n    Aggregates []StatementInfo `json:\"aggregates,omitempty\"`\n}\n\nfunc NewRegistry(table string) *StatementRegistry {\n    return &StatementRegistry{Table: table}\n}\n\nfunc (r *StatementRegistry) AddQuery(stmt edamame.QueryStatement) {\n    r.Queries = append(r.Queries, toStatementInfo(stmt.Name(), stmt.Description(), \"query\", stmt.Params(), stmt.Tags()))\n}\n\nfunc (r *StatementRegistry) AddSelect(stmt edamame.SelectStatement) {\n    r.Selects = append(r.Selects, toStatementInfo(stmt.Name(), stmt.Description(), \"select\", stmt.Params(), stmt.Tags()))\n}\n\nfunc (r *StatementRegistry) AddAggregate(stmt edamame.AggregateStatement) {\n    r.Aggregates = append(r.Aggregates, toStatementInfo(stmt.Name(), stmt.Description(), \"aggregate\", stmt.Params(), stmt.Tags()))\n}\n\nfunc toStatementInfo(name, desc, typ string, params []edamame.ParamSpec, tags []string) StatementInfo {\n    info := StatementInfo{\n        Name:        name,\n        Description: desc,\n        Type:        typ,\n        Tags:        tags,\n    }\n    for _, p := range params {\n        info.Params = append(info.Params, ParamInfo{\n            Name:     p.Name,\n            Type:     p.Type,\n            Required: p.Required,\n        })\n    }\n    return info\n}\n\nfunc (r *StatementRegistry) JSON() (string, error) {\n    data, err := json.MarshalIndent(r, \"\", \"  \")\n    return string(data), err\n}",{"id":3306,"title":3307,"titles":3308,"content":3309,"level":118},"/v1.0.3/cookbook/llm-integration#exporting-for-llm-context","Exporting for LLM Context",[3284],"// Build registry\nregistry := NewRegistry(\"users\")\nregistry.AddQuery(QueryAll)\nregistry.AddQuery(ByRole)\nregistry.AddQuery(ActiveAdults)\nregistry.AddSelect(SelectByID)\nregistry.AddAggregate(CountAll)\n\n// Export as JSON\njson, _ := registry.JSON() Example output: {\n  \"table\": \"users\",\n  \"queries\": [\n    {\n      \"name\": \"query-all\",\n      \"description\": \"Query all users\",\n      \"type\": \"query\",\n      \"params\": []\n    },\n    {\n      \"name\": \"by-role\",\n      \"description\": \"Find users by role\",\n      \"type\": \"query\",\n      \"params\": [\n        {\"name\": \"role\", \"type\": \"any\", \"required\": true}\n      ],\n      \"tags\": [\"filter\", \"security\"]\n    },\n    {\n      \"name\": \"active-adults\",\n      \"description\": \"Find active users over 18\",\n      \"type\": \"query\",\n      \"params\": [\n        {\"name\": \"active\", \"type\": \"any\", \"required\": true},\n        {\"name\": \"min_age\", \"type\": \"any\", \"required\": true}\n      ],\n      \"tags\": [\"filter\"]\n    }\n  ],\n  \"selects\": [\n    {\n      \"name\": \"select-by-id\",\n      \"description\": \"Select a single user by ID\",\n      \"type\": \"select\",\n      \"params\": [\n        {\"name\": \"id\", \"type\": \"any\", \"required\": true}\n      ]\n    }\n  ],\n  \"aggregates\": [\n    {\n      \"name\": \"count-all\",\n      \"description\": \"Count all users\",\n      \"type\": \"aggregate\",\n      \"params\": []\n    }\n  ]\n}",{"id":3311,"title":3312,"titles":3313,"content":3314,"level":118},"/v1.0.3/cookbook/llm-integration#system-prompt-design","System Prompt Design",[3284],"Provide statement metadata in your LLM system prompt: You are a database assistant. You have access to the following operations:\n\n{registry_json}\n\nWhen the user asks a question about data:\n1. Identify the appropriate operation\n2. Extract required parameters from the user's request\n3. Respond with JSON: {\"statement\": \"name\", \"type\": \"query|select|...\", \"params\": {...}}\n\nExamples:\n- \"How many users are there?\" → {\"statement\": \"count-all\", \"type\": \"aggregate\", \"params\": {}}\n- \"Find admins\" → {\"statement\": \"by-role\", \"type\": \"query\", \"params\": {\"role\": \"admin\"}}\n- \"Get user 123\" → {\"statement\": \"select-by-id\", \"type\": \"select\", \"params\": {\"id\": 123}}",{"id":3316,"title":3317,"titles":3318,"content":3319,"level":118},"/v1.0.3/cookbook/llm-integration#executing-llm-responses","Executing LLM Responses",[3284],"Create a dispatcher that maps statement names to actual statements: type LLMResponse struct {\n    Statement string         `json:\"statement\"`\n    Type      string         `json:\"type\"`\n    Params    map[string]any `json:\"params\"`\n}\n\ntype StatementDispatcher struct {\n    exec       *edamame.Executor[User]\n    queries    map[string]edamame.QueryStatement\n    selects    map[string]edamame.SelectStatement\n    aggregates map[string]edamame.AggregateStatement\n}\n\nfunc NewDispatcher(exec *edamame.Executor[User]) *StatementDispatcher {\n    return &StatementDispatcher{\n        exec:       exec,\n        queries:    make(map[string]edamame.QueryStatement),\n        selects:    make(map[string]edamame.SelectStatement),\n        aggregates: make(map[string]edamame.AggregateStatement),\n    }\n}\n\nfunc (d *StatementDispatcher) RegisterQuery(stmt edamame.QueryStatement) {\n    d.queries[stmt.Name()] = stmt\n}\n\nfunc (d *StatementDispatcher) RegisterSelect(stmt edamame.SelectStatement) {\n    d.selects[stmt.Name()] = stmt\n}\n\nfunc (d *StatementDispatcher) RegisterAggregate(stmt edamame.AggregateStatement) {\n    d.aggregates[stmt.Name()] = stmt\n}\n\nfunc (d *StatementDispatcher) Execute(ctx context.Context, resp LLMResponse) (any, error) {\n    switch resp.Type {\n    case \"query\":\n        stmt, ok := d.queries[resp.Statement]\n        if !ok {\n            return nil, fmt.Errorf(\"unknown query: %s\", resp.Statement)\n        }\n        return d.exec.ExecQuery(ctx, stmt, resp.Params)\n    case \"select\":\n        stmt, ok := d.selects[resp.Statement]\n        if !ok {\n            return nil, fmt.Errorf(\"unknown select: %s\", resp.Statement)\n        }\n        return d.exec.ExecSelect(ctx, stmt, resp.Params)\n    case \"aggregate\":\n        stmt, ok := d.aggregates[resp.Statement]\n        if !ok {\n            return nil, fmt.Errorf(\"unknown aggregate: %s\", resp.Statement)\n        }\n        return d.exec.ExecAggregate(ctx, stmt, resp.Params)\n    default:\n        return nil, fmt.Errorf(\"unknown type: %s\", resp.Type)\n    }\n}",{"id":3321,"title":3322,"titles":3323,"content":3324,"level":118},"/v1.0.3/cookbook/llm-integration#complete-setup","Complete Setup",[3284],"// Create executor\nexec, _ := edamame.New[User](db, \"users\", postgres.New())\n\n// Create dispatcher and register statements\ndispatcher := NewDispatcher(exec)\ndispatcher.RegisterQuery(QueryAll)\ndispatcher.RegisterQuery(ByRole)\ndispatcher.RegisterQuery(ActiveAdults)\ndispatcher.RegisterSelect(SelectByID)\ndispatcher.RegisterAggregate(CountAll)\n\n// Build registry for LLM context\nregistry := NewRegistry(\"users\")\nregistry.AddQuery(QueryAll)\nregistry.AddQuery(ByRole)\nregistry.AddQuery(ActiveAdults)\nregistry.AddSelect(SelectByID)\nregistry.AddAggregate(CountAll)\n\nllmContext, _ := registry.JSON()",{"id":3326,"title":3327,"titles":3328,"content":3329,"level":118},"/v1.0.3/cookbook/llm-integration#validation","Validation",[3284],"Validate LLM responses before execution: func (d *StatementDispatcher) Validate(resp LLMResponse) error {\n    switch resp.Type {\n    case \"query\":\n        stmt, ok := d.queries[resp.Statement]\n        if !ok {\n            return fmt.Errorf(\"unknown query: %s\", resp.Statement)\n        }\n        return validateParams(stmt.Params(), resp.Params)\n    case \"select\":\n        stmt, ok := d.selects[resp.Statement]\n        if !ok {\n            return fmt.Errorf(\"unknown select: %s\", resp.Statement)\n        }\n        return validateParams(stmt.Params(), resp.Params)\n    case \"aggregate\":\n        stmt, ok := d.aggregates[resp.Statement]\n        if !ok {\n            return fmt.Errorf(\"unknown aggregate: %s\", resp.Statement)\n        }\n        return validateParams(stmt.Params(), resp.Params)\n    default:\n        return fmt.Errorf(\"unknown type: %s\", resp.Type)\n    }\n}\n\nfunc validateParams(specs []edamame.ParamSpec, provided map[string]any) error {\n    for _, spec := range specs {\n        if spec.Required {\n            if _, ok := provided[spec.Name]; !ok {\n                return fmt.Errorf(\"missing required param: %s\", spec.Name)\n            }\n        }\n    }\n    return nil\n}",{"id":3331,"title":3332,"titles":3333,"content":3334,"level":118},"/v1.0.3/cookbook/llm-integration#rate-limiting","Rate Limiting",[3284],"Protect against LLM-driven query floods: type RateLimitedDispatcher struct {\n    dispatcher *StatementDispatcher\n    limiter    *rate.Limiter\n}\n\nfunc NewRateLimitedDispatcher(dispatcher *StatementDispatcher, rps float64) *RateLimitedDispatcher {\n    return &RateLimitedDispatcher{\n        dispatcher: dispatcher,\n        limiter:    rate.NewLimiter(rate.Limit(rps), 10),\n    }\n}\n\nfunc (d *RateLimitedDispatcher) Execute(ctx context.Context, resp LLMResponse) (any, error) {\n    if err := d.limiter.Wait(ctx); err != nil {\n        return nil, err\n    }\n    return d.dispatcher.Execute(ctx, resp)\n}",{"id":3336,"title":3337,"titles":3338,"content":3339,"level":118},"/v1.0.3/cookbook/llm-integration#audit-logging","Audit Logging",[3284],"Log LLM-driven operations: func (d *StatementDispatcher) ExecuteWithAudit(ctx context.Context, resp LLMResponse, userID string) (any, error) {\n    start := time.Now()\n\n    result, err := d.Execute(ctx, resp)\n\n    log.Printf(\"LLM execution: user=%s statement=%s type=%s params=%v duration=%v error=%v\",\n        userID,\n        resp.Statement,\n        resp.Type,\n        resp.Params,\n        time.Since(start),\n        err,\n    )\n\n    return result, err\n}",{"id":3341,"title":3342,"titles":3343,"content":3344,"level":118},"/v1.0.3/cookbook/llm-integration#example-chat-interface","Example: Chat Interface",[3284],"Complete example with a simple chat interface: func HandleChat(ctx context.Context, dispatcher *StatementDispatcher, registry *StatementRegistry, llm LLMClient, userMessage string) string {\n    // 1. Get statement metadata\n    specs, _ := registry.JSON()\n\n    // 2. Build prompt\n    prompt := fmt.Sprintf(`You are a database assistant. Available operations:\n%s\n\nUser: %s\n\nRespond with JSON: {\"statement\": \"...\", \"type\": \"...\", \"params\": {...}}\nOr respond with {\"error\": \"...\"} if the request cannot be fulfilled.`, specs, userMessage)\n\n    // 3. Get LLM response\n    llmResp, err := llm.Complete(ctx, prompt)\n    if err != nil {\n        return \"I couldn't process that request.\"\n    }\n\n    // 4. Parse response\n    var resp LLMResponse\n    if err := json.Unmarshal([]byte(llmResp), &resp); err != nil {\n        return \"I couldn't understand how to query the database.\"\n    }\n\n    // 5. Validate\n    if err := dispatcher.Validate(resp); err != nil {\n        return fmt.Sprintf(\"Invalid request: %v\", err)\n    }\n\n    // 6. Execute\n    result, err := dispatcher.Execute(ctx, resp)\n    if err != nil {\n        return fmt.Sprintf(\"Query failed: %v\", err)\n    }\n\n    // 7. Format response\n    return formatResult(result)\n}",{"id":3346,"title":3347,"titles":3348,"content":3349,"level":118},"/v1.0.3/cookbook/llm-integration#security-considerations","Security Considerations",[3284],"Validate all LLM outputs before executionUse parameterized queries (edamame handles this)Limit exposed statements to what's safeRate limit LLM-driven operationsAudit log all executionsNever expose raw SQL generation to LLMs html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .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 .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html .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 .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 .skxcq, html code.shiki .skxcq{--shiki-default:var(--shiki-builtin)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}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":3351,"title":1784,"titles":3352,"content":3353,"level":111},"/v1.0.3/reference/api",[],"Complete API reference for the edamame package",{"id":3355,"title":1784,"titles":3356,"content":3357,"level":111},"/v1.0.3/reference/api#api-reference",[],"Complete API reference for the github.com/zoobz-io/edamame package.",{"id":3359,"title":2880,"titles":3360,"content":3361,"level":118},"/v1.0.3/reference/api#executor",[1784],"The main execution context for a model type.",{"id":3363,"title":1112,"titles":3364,"content":3365,"level":129},"/v1.0.3/reference/api#new",[1784,2880],"func New[T any](db *sqlx.DB, tableName string, renderer astql.Renderer) (*Executor[T], error) Creates a new executor for type T bound to the given table with the specified SQL renderer. The db parameter can be nil for testing statement rendering without database access. Supported renderers via astql: github.com/zoobz-io/astql/pkg/postgres - PostgreSQLgithub.com/zoobz-io/astql/pkg/mariadb - MariaDBgithub.com/zoobz-io/astql/pkg/sqlite - SQLitegithub.com/zoobz-io/astql/pkg/mssql - SQL Server import \"github.com/zoobz-io/astql/pkg/postgres\" // or mariadb, sqlite, mssql\n\nexec, err := edamame.New[User](db, \"users\", postgres.New())",{"id":3367,"title":3368,"titles":3369,"content":103,"level":118},"/v1.0.3/reference/api#statement-constructors","Statement Constructors",[1784],{"id":3371,"title":146,"titles":3372,"content":3373,"level":129},"/v1.0.3/reference/api#newquerystatement",[1784,3368],"func NewQueryStatement(name, description string, spec QuerySpec, tags ...string) QueryStatement Creates a typed query statement for multi-record retrieval. Parameters are automatically derived from the spec.",{"id":3375,"title":352,"titles":3376,"content":3377,"level":129},"/v1.0.3/reference/api#newselectstatement",[1784,3368],"func NewSelectStatement(name, description string, spec SelectSpec, tags ...string) SelectStatement Creates a typed select statement for single-record retrieval.",{"id":3379,"title":3380,"titles":3381,"content":3382,"level":129},"/v1.0.3/reference/api#newupdatestatement","NewUpdateStatement",[1784,3368],"func NewUpdateStatement(name, description string, spec UpdateSpec, tags ...string) UpdateStatement Creates a typed update statement for modifications.",{"id":3384,"title":3385,"titles":3386,"content":3387,"level":129},"/v1.0.3/reference/api#newdeletestatement","NewDeleteStatement",[1784,3368],"func NewDeleteStatement(name, description string, spec DeleteSpec, tags ...string) DeleteStatement Creates a typed delete statement for deletions.",{"id":3389,"title":879,"titles":3390,"content":3391,"level":129},"/v1.0.3/reference/api#newaggregatestatement",[1784,3368],"func NewAggregateStatement(name, description string, fn AggregateFunc, spec AggregateSpec, tags ...string) AggregateStatement Creates a typed aggregate statement for COUNT, SUM, AVG, MIN, MAX operations.",{"id":3393,"title":1443,"titles":3394,"content":3395,"level":118},"/v1.0.3/reference/api#statement-types",[1784],"All statement types share common methods: func (s Statement) ID() uuid.UUID      // Unique identifier\nfunc (s Statement) Name() string       // Human-readable name\nfunc (s Statement) Description() string // What the statement does\nfunc (s Statement) Params() []ParamSpec // Required parameters\nfunc (s Statement) Tags() []string     // Optional categorization tags",{"id":3397,"title":3398,"titles":3399,"content":3400,"level":129},"/v1.0.3/reference/api#querystatement","QueryStatement",[1784,1443],"For multi-record retrieval operations.",{"id":3402,"title":3403,"titles":3404,"content":3405,"level":129},"/v1.0.3/reference/api#selectstatement","SelectStatement",[1784,1443],"For single-record retrieval operations.",{"id":3407,"title":3408,"titles":3409,"content":3410,"level":129},"/v1.0.3/reference/api#updatestatement","UpdateStatement",[1784,1443],"For modification operations.",{"id":3412,"title":3413,"titles":3414,"content":3415,"level":129},"/v1.0.3/reference/api#deletestatement","DeleteStatement",[1784,1443],"For deletion operations.",{"id":3417,"title":3418,"titles":3419,"content":3420,"level":129},"/v1.0.3/reference/api#aggregatestatement","AggregateStatement",[1784,1443],"For aggregate operations (COUNT, SUM, AVG, MIN, MAX).",{"id":3422,"title":3423,"titles":3424,"content":103,"level":118},"/v1.0.3/reference/api#executor-methods","Executor Methods",[1784],{"id":3426,"title":3427,"titles":3428,"content":103,"level":129},"/v1.0.3/reference/api#builder-access","Builder Access",[1784,3423],{"id":3430,"title":3431,"titles":3432,"content":3433,"level":175},"/v1.0.3/reference/api#query","Query",[1784,3423,3427],"func (e *Executor[T]) Query(stmt QueryStatement) (*soy.Query[T], error) Returns a soy Query builder for the statement.",{"id":3435,"title":3436,"titles":3437,"content":3438,"level":175},"/v1.0.3/reference/api#select","Select",[1784,3423,3427],"func (e *Executor[T]) Select(stmt SelectStatement) (*soy.Select[T], error) Returns a soy Select builder for the statement.",{"id":3440,"title":3441,"titles":3442,"content":3443,"level":175},"/v1.0.3/reference/api#update","Update",[1784,3423,3427],"func (e *Executor[T]) Update(stmt UpdateStatement) (*soy.Update[T], error) Returns a soy Update builder for the statement.",{"id":3445,"title":3446,"titles":3447,"content":3448,"level":175},"/v1.0.3/reference/api#delete","Delete",[1784,3423,3427],"func (e *Executor[T]) Delete(stmt DeleteStatement) (*soy.Delete[T], error) Returns a soy Delete builder for the statement.",{"id":3450,"title":3451,"titles":3452,"content":3453,"level":175},"/v1.0.3/reference/api#aggregate","Aggregate",[1784,3423,3427],"func (e *Executor[T]) Aggregate(stmt AggregateStatement) *soy.Aggregate[T] Returns a soy Aggregate builder for the statement.",{"id":3455,"title":3456,"titles":3457,"content":3458,"level":175},"/v1.0.3/reference/api#insert","Insert",[1784,3423,3427],"func (e *Executor[T]) Insert() *soy.Create[T] Returns a soy Create builder for inserts.",{"id":3460,"title":3461,"titles":3462,"content":3463,"level":175},"/v1.0.3/reference/api#compound","Compound",[1784,3423,3427],"func (e *Executor[T]) Compound(spec CompoundQuerySpec) (*soy.Compound[T], error) Returns a soy Compound builder for set operations (UNION, INTERSECT, EXCEPT).",{"id":3465,"title":3466,"titles":3467,"content":103,"level":129},"/v1.0.3/reference/api#execution-methods","Execution Methods",[1784,3423],{"id":3469,"title":3470,"titles":3471,"content":3472,"level":175},"/v1.0.3/reference/api#execquery-execquerytx","ExecQuery / ExecQueryTx",[1784,3423,3466],"func (e *Executor[T]) ExecQuery(ctx context.Context, stmt QueryStatement, params map[string]any) ([]*T, error)\nfunc (e *Executor[T]) ExecQueryTx(ctx context.Context, tx *sqlx.Tx, stmt QueryStatement, params map[string]any) ([]*T, error) Executes a query statement, returning multiple records.",{"id":3474,"title":3475,"titles":3476,"content":3477,"level":175},"/v1.0.3/reference/api#execselect-execselecttx","ExecSelect / ExecSelectTx",[1784,3423,3466],"func (e *Executor[T]) ExecSelect(ctx context.Context, stmt SelectStatement, params map[string]any) (*T, error)\nfunc (e *Executor[T]) ExecSelectTx(ctx context.Context, tx *sqlx.Tx, stmt SelectStatement, params map[string]any) (*T, error) Executes a select statement, returning a single record.",{"id":3479,"title":3480,"titles":3481,"content":3482,"level":175},"/v1.0.3/reference/api#execupdate-execupdatetx","ExecUpdate / ExecUpdateTx",[1784,3423,3466],"func (e *Executor[T]) ExecUpdate(ctx context.Context, stmt UpdateStatement, params map[string]any) (*T, error)\nfunc (e *Executor[T]) ExecUpdateTx(ctx context.Context, tx *sqlx.Tx, stmt UpdateStatement, params map[string]any) (*T, error) Executes an update statement, returning the updated record.",{"id":3484,"title":3485,"titles":3486,"content":3487,"level":175},"/v1.0.3/reference/api#execdelete-execdeletetx","ExecDelete / ExecDeleteTx",[1784,3423,3466],"func (e *Executor[T]) ExecDelete(ctx context.Context, stmt DeleteStatement, params map[string]any) (int64, error)\nfunc (e *Executor[T]) ExecDeleteTx(ctx context.Context, tx *sqlx.Tx, stmt DeleteStatement, params map[string]any) (int64, error) Executes a delete statement, returning the count of deleted rows.",{"id":3489,"title":3490,"titles":3491,"content":3492,"level":175},"/v1.0.3/reference/api#execaggregate-execaggregatetx","ExecAggregate / ExecAggregateTx",[1784,3423,3466],"func (e *Executor[T]) ExecAggregate(ctx context.Context, stmt AggregateStatement, params map[string]any) (float64, error)\nfunc (e *Executor[T]) ExecAggregateTx(ctx context.Context, tx *sqlx.Tx, stmt AggregateStatement, params map[string]any) (float64, error) Executes an aggregate statement, returning the result.",{"id":3494,"title":3495,"titles":3496,"content":3497,"level":175},"/v1.0.3/reference/api#execinsert-execinserttx","ExecInsert / ExecInsertTx",[1784,3423,3466],"func (e *Executor[T]) ExecInsert(ctx context.Context, record *T) (*T, error)\nfunc (e *Executor[T]) ExecInsertTx(ctx context.Context, tx *sqlx.Tx, record *T) (*T, error) Inserts a record, returning it with generated fields populated.",{"id":3499,"title":3500,"titles":3501,"content":3502,"level":175},"/v1.0.3/reference/api#execcompound-execcompoundtx","ExecCompound / ExecCompoundTx",[1784,3423,3466],"func (e *Executor[T]) ExecCompound(ctx context.Context, spec CompoundQuerySpec, params map[string]any) ([]*T, error)\nfunc (e *Executor[T]) ExecCompoundTx(ctx context.Context, tx *sqlx.Tx, spec CompoundQuerySpec, params map[string]any) ([]*T, error) Executes a compound query (UNION, INTERSECT, EXCEPT), returning multiple records.",{"id":3504,"title":3505,"titles":3506,"content":103,"level":129},"/v1.0.3/reference/api#batch-execution","Batch Execution",[1784,3423],{"id":3508,"title":3509,"titles":3510,"content":3511,"level":175},"/v1.0.3/reference/api#execinsertbatch-execinsertbatchtx","ExecInsertBatch / ExecInsertBatchTx",[1784,3423,3505],"func (e *Executor[T]) ExecInsertBatch(ctx context.Context, records []*T) (int64, error)\nfunc (e *Executor[T]) ExecInsertBatchTx(ctx context.Context, tx *sqlx.Tx, records []*T) (int64, error) Inserts multiple records, returning the count.",{"id":3513,"title":3514,"titles":3515,"content":3516,"level":175},"/v1.0.3/reference/api#execupdatebatch-execupdatebatchtx","ExecUpdateBatch / ExecUpdateBatchTx",[1784,3423,3505],"func (e *Executor[T]) ExecUpdateBatch(ctx context.Context, stmt UpdateStatement, batchParams []map[string]any) (int64, error)\nfunc (e *Executor[T]) ExecUpdateBatchTx(ctx context.Context, tx *sqlx.Tx, stmt UpdateStatement, batchParams []map[string]any) (int64, error) Executes an update statement with multiple parameter sets.",{"id":3518,"title":3519,"titles":3520,"content":3521,"level":175},"/v1.0.3/reference/api#execdeletebatch-execdeletebatchtx","ExecDeleteBatch / ExecDeleteBatchTx",[1784,3423,3505],"func (e *Executor[T]) ExecDeleteBatch(ctx context.Context, stmt DeleteStatement, batchParams []map[string]any) (int64, error)\nfunc (e *Executor[T]) ExecDeleteBatchTx(ctx context.Context, tx *sqlx.Tx, stmt DeleteStatement, batchParams []map[string]any) (int64, error) Executes a delete statement with multiple parameter sets.",{"id":3523,"title":3524,"titles":3525,"content":3526,"level":129},"/v1.0.3/reference/api#type-erased-execution-atom","Type-Erased Execution (Atom)",[1784,3423],"These methods return results as atom.Atom types, enabling type-erased execution where the concrete type T is not known at consumption time. Useful for dynamic query handling and LLM-driven database operations.",{"id":3528,"title":3529,"titles":3530,"content":3531,"level":175},"/v1.0.3/reference/api#execqueryatom","ExecQueryAtom",[1784,3423,3524],"func (e *Executor[T]) ExecQueryAtom(ctx context.Context, stmt QueryStatement, params map[string]any) ([]*atom.Atom, error) Executes a query statement and returns results as Atoms.",{"id":3533,"title":3534,"titles":3535,"content":3536,"level":175},"/v1.0.3/reference/api#execselectatom","ExecSelectAtom",[1784,3423,3524],"func (e *Executor[T]) ExecSelectAtom(ctx context.Context, stmt SelectStatement, params map[string]any) (*atom.Atom, error) Executes a select statement and returns the result as an Atom.",{"id":3538,"title":3539,"titles":3540,"content":3541,"level":175},"/v1.0.3/reference/api#execinsertatom","ExecInsertAtom",[1784,3423,3524],"func (e *Executor[T]) ExecInsertAtom(ctx context.Context, params map[string]any) (*atom.Atom, error) Executes an insert using parameter map and returns the result as an Atom.",{"id":3543,"title":3544,"titles":3545,"content":103,"level":129},"/v1.0.3/reference/api#rendering","Rendering",[1784,3423],{"id":3547,"title":3548,"titles":3549,"content":3550,"level":175},"/v1.0.3/reference/api#rendercompound","RenderCompound",[1784,3423,3544],"func (e *Executor[T]) RenderCompound(spec CompoundQuerySpec) (string, error) Renders a compound query to SQL for inspection or debugging.",{"id":3552,"title":3553,"titles":3554,"content":103,"level":129},"/v1.0.3/reference/api#other","Other",[1784,3423],{"id":3556,"title":3007,"titles":3557,"content":3558,"level":175},"/v1.0.3/reference/api#soy",[1784,3423,3553],"func (e *Executor[T]) Soy() *soy.Soy[T] Returns the underlying soy instance for direct builder access.",{"id":3560,"title":3561,"titles":3562,"content":3563,"level":175},"/v1.0.3/reference/api#tablename","TableName",[1784,3423,3553],"func (e *Executor[T]) TableName() string Returns the table name.",{"id":3565,"title":3566,"titles":3567,"content":103,"level":118},"/v1.0.3/reference/api#spec-types","Spec Types",[1784],{"id":3569,"title":169,"titles":3570,"content":3571,"level":129},"/v1.0.3/reference/api#queryspec",[1784,3566],"type QuerySpec struct {\n    Fields      []string\n    SelectExprs []SelectExprSpec  // Expression-based SELECT (functions, aggregates)\n    Where       []ConditionSpec\n    OrderBy     []OrderBySpec\n    GroupBy     []string\n    Having      []ConditionSpec\n    HavingAgg   []HavingAggSpec\n    Limit       *int\n    LimitParam  string            // Parameterized LIMIT\n    Offset      *int\n    OffsetParam string            // Parameterized OFFSET\n    Distinct    bool\n    DistinctOn  []string\n    ForLocking  string\n}",{"id":3573,"title":371,"titles":3574,"content":3575,"level":129},"/v1.0.3/reference/api#selectspec",[1784,3566],"type SelectSpec struct {\n    Fields      []string\n    SelectExprs []SelectExprSpec  // Expression-based SELECT (functions, aggregates)\n    Where       []ConditionSpec\n    OrderBy     []OrderBySpec\n    GroupBy     []string\n    Having      []ConditionSpec\n    HavingAgg   []HavingAggSpec\n    Limit       *int\n    LimitParam  string            // Parameterized LIMIT\n    Offset      *int\n    OffsetParam string            // Parameterized OFFSET\n    Distinct    bool\n    DistinctOn  []string\n    ForLocking  string\n}",{"id":3577,"title":2921,"titles":3578,"content":3579,"level":129},"/v1.0.3/reference/api#updatespec",[1784,3566],"type UpdateSpec struct {\n    Set   map[string]string  // field -> param\n    Where []ConditionSpec\n}",{"id":3581,"title":2926,"titles":3582,"content":3583,"level":129},"/v1.0.3/reference/api#deletespec",[1784,3566],"type DeleteSpec struct {\n    Where []ConditionSpec\n}",{"id":3585,"title":907,"titles":3586,"content":3587,"level":129},"/v1.0.3/reference/api#aggregatespec",[1784,3566],"type AggregateSpec struct {\n    Field string\n    Where []ConditionSpec\n}",{"id":3589,"title":234,"titles":3590,"content":3591,"level":129},"/v1.0.3/reference/api#conditionspec",[1784,3566],"type ConditionSpec struct {\n    Field      string\n    Operator   string\n    Param      string\n    IsNull     bool\n    Logic      string           // \"AND\" or \"OR\" for groups\n    Group      []ConditionSpec  // Nested conditions\n    Between    bool             // Use BETWEEN with LowParam/HighParam\n    NotBetween bool             // Use NOT BETWEEN with LowParam/HighParam\n    LowParam   string           // Lower bound param for BETWEEN\n    HighParam  string           // Upper bound param for BETWEEN\n    RightField string           // For field-to-field comparisons (WHERE a.field = b.field)\n}",{"id":3593,"title":3594,"titles":3595,"content":3596,"level":175},"/v1.0.3/reference/api#helper-methods","Helper Methods",[1784,3566,234],"func (c ConditionSpec) IsGroup() bool           // Returns true if this is a grouped condition\nfunc (c ConditionSpec) IsBetween() bool         // Returns true if Between is set\nfunc (c ConditionSpec) IsNotBetween() bool      // Returns true if NotBetween is set\nfunc (c ConditionSpec) IsFieldComparison() bool // Returns true if RightField is set",{"id":3598,"title":285,"titles":3599,"content":3600,"level":129},"/v1.0.3/reference/api#orderbyspec",[1784,3566],"type OrderBySpec struct {\n    Field     string\n    Direction string  // \"asc\" or \"desc\"\n    Nulls     string  // \"first\" or \"last\"\n    Operator  string  // For expressions (e.g., \"\u003C->\")\n    Param     string  // For expression parameters\n}",{"id":3602,"title":3603,"titles":3604,"content":3605,"level":129},"/v1.0.3/reference/api#paramspec","ParamSpec",[1784,3566],"type ParamSpec struct {\n    Name        string\n    Type        string\n    Required    bool\n    Default     any\n    Description string\n}",{"id":3607,"title":3608,"titles":3609,"content":3610,"level":129},"/v1.0.3/reference/api#selectexprspec","SelectExprSpec",[1784,3566],"Defines expression-based SELECT columns (functions, aggregates, casts). type SelectExprSpec struct {\n    Func     string          // Function name (see supported functions below)\n    Field    string          // Primary field for single-field functions\n    Fields   []string        // Multiple fields for multi-field functions (concat)\n    Params   []string        // Additional parameters (substring positions, power exponent)\n    CastType string          // Target type for cast operations\n    Filter   *ConditionSpec  // Filter clause for aggregate functions\n    Alias    string          // Required: column alias in result\n} Supported Functions: CategoryFunctionsStringupper, lower, length, trim, ltrim, rtrim, substring, replace, concatMathabs, ceil, floor, round, sqrt, powerDate/Timenow, current_date, current_time, current_timestampTypecastAggregatecount, count_star, count_distinct, sum, avg, min, maxConditionalcoalesce, nullif",{"id":3612,"title":3613,"titles":3614,"content":3615,"level":129},"/v1.0.3/reference/api#havingaggspec","HavingAggSpec",[1784,3566],"Defines aggregate conditions for HAVING clauses. type HavingAggSpec struct {\n    Func     string  // Aggregate function: \"count\", \"sum\", \"avg\", \"min\", \"max\"\n    Field    string  // Field to aggregate\n    Operator string  // Comparison operator\n    Param    string  // Parameter name for comparison value\n}",{"id":3617,"title":3618,"titles":3619,"content":3620,"level":129},"/v1.0.3/reference/api#compoundqueryspec","CompoundQuerySpec",[1784,3566],"Defines compound queries using set operations (UNION, INTERSECT, EXCEPT). type CompoundQuerySpec struct {\n    Base     QuerySpec         // The base query\n    Operands []CompoundOperand // Set operations with additional queries\n    OrderBy  []OrderBySpec     // Final ORDER BY (applies to combined result)\n    Limit    *int              // Final LIMIT\n    Offset   *int              // Final OFFSET\n}",{"id":3622,"title":3623,"titles":3624,"content":3625,"level":129},"/v1.0.3/reference/api#compoundoperand","CompoundOperand",[1784,3566],"Defines a set operation in a compound query. type CompoundOperand struct {\n    Operation string    // \"union\", \"union_all\", \"intersect\", \"intersect_all\", \"except\", \"except_all\"\n    Query     QuerySpec // The query to combine\n}",{"id":3627,"title":3628,"titles":3629,"content":3630,"level":118},"/v1.0.3/reference/api#aggregatefunc","AggregateFunc",[1784],"type AggregateFunc string\n\nconst (\n    AggCount AggregateFunc = \"COUNT\"\n    AggSum   AggregateFunc = \"SUM\"\n    AggAvg   AggregateFunc = \"AVG\"\n    AggMin   AggregateFunc = \"MIN\"\n    AggMax   AggregateFunc = \"MAX\"\n)",{"id":3632,"title":3633,"titles":3634,"content":3635,"level":118},"/v1.0.3/reference/api#event-keys","Event Keys",[1784],"var (\n    KeyTable    = capitan.NewStringKey(\"table\")\n    KeyError    = capitan.NewStringKey(\"error\")\n    KeyDuration = capitan.NewDurationKey(\"duration\")\n)",{"id":3637,"title":3638,"titles":3639,"content":3640,"level":118},"/v1.0.3/reference/api#signals","Signals",[1784],"var (\n    ExecutorCreated = capitan.NewSignal(\"edamame.executor.created\", \"Executor instance created\")\n) Hook for monitoring: capitan.Hook(edamame.ExecutorCreated, func(ctx context.Context, e *capitan.Event) {\n    table, _ := edamame.KeyTable.From(e)\n    log.Printf(\"Executor created for table: %s\", table)\n})",{"id":3642,"title":2885,"titles":3643,"content":3644,"level":118},"/v1.0.3/reference/api#struct-tags",[1784],"Edamame uses struct tags to understand your model: type User struct {\n    ID    int    `db:\"id\" type:\"integer\" constraints:\"primarykey\"`\n    Email string `db:\"email\" type:\"text\" constraints:\"notnull,unique\"`\n    Name  string `db:\"name\" type:\"text\"`\n    Age   *int   `db:\"age\" type:\"integer\"`\n} TagPurposeExampledbColumn namedb:\"user_id\"typeSQL typetype:\"text\", type:\"integer\"constraintsColumn constraintsconstraints:\"primarykey,notnull\" 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 .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 .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sfm-E, html code.shiki .sfm-E{--shiki-default:var(--shiki-variable)}html pre.shiki code .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}",[3646],{"title":3647,"path":3648,"stem":3649,"children":3650,"page":3663},"V103","/v1.0.3","v1.0.3",[3651,3653,3664,3672,3679],{"title":1736,"path":2786,"stem":3652,"description":2788},"v1.0.3/1.overview",{"title":1742,"path":3654,"stem":3655,"children":3656,"page":3663},"/v1.0.3/learn","v1.0.3/2.learn",[3657,3659,3661],{"title":1487,"path":2829,"stem":3658,"description":2831},"v1.0.3/2.learn/1.quickstart",{"title":1753,"path":2871,"stem":3660,"description":2873},"v1.0.3/2.learn/2.concepts",{"title":1501,"path":2984,"stem":3662,"description":2986},"v1.0.3/2.learn/3.architecture",false,{"title":1761,"path":3665,"stem":3666,"children":3667,"page":3663},"/v1.0.3/guides","v1.0.3/3.guides",[3668,3670],{"title":1452,"path":3040,"stem":3669,"description":3042},"v1.0.3/3.guides/1.statements",{"title":1773,"path":3221,"stem":3671,"description":3223},"v1.0.3/3.guides/2.testing",{"title":3673,"path":3674,"stem":3675,"children":3676,"page":3663},"Cookbook","/v1.0.3/cookbook","v1.0.3/4.cookbook",[3677],{"title":3284,"path":3283,"stem":3678,"description":3286},"v1.0.3/4.cookbook/1.llm-integration",{"title":1777,"path":3680,"stem":3681,"children":3682,"page":3663},"/v1.0.3/reference","v1.0.3/5.reference",[3683],{"title":1784,"path":3351,"stem":3684,"description":3353},"v1.0.3/5.reference/1.api",[3686],{"title":3647,"path":3648,"stem":3649,"children":3687,"page":3663},[3688,3689,3694,3698,3701],{"title":1736,"path":2786,"stem":3652},{"title":1742,"path":3654,"stem":3655,"children":3690,"page":3663},[3691,3692,3693],{"title":1487,"path":2829,"stem":3658},{"title":1753,"path":2871,"stem":3660},{"title":1501,"path":2984,"stem":3662},{"title":1761,"path":3665,"stem":3666,"children":3695,"page":3663},[3696,3697],{"title":1452,"path":3040,"stem":3669},{"title":1773,"path":3221,"stem":3671},{"title":3673,"path":3674,"stem":3675,"children":3699,"page":3663},[3700],{"title":3284,"path":3283,"stem":3678},{"title":1777,"path":3680,"stem":3681,"children":3702,"page":3663},[3703],{"title":1784,"path":3351,"stem":3684},[3705],{"title":3647,"path":3648,"stem":3649,"children":3706,"page":3663},[3707,3708,3713,3717,3720],{"title":1736,"path":2786,"stem":3652,"description":2788},{"title":1742,"path":3654,"stem":3655,"children":3709,"page":3663},[3710,3711,3712],{"title":1487,"path":2829,"stem":3658,"description":2831},{"title":1753,"path":2871,"stem":3660,"description":2873},{"title":1501,"path":2984,"stem":3662,"description":2986},{"title":1761,"path":3665,"stem":3666,"children":3714,"page":3663},[3715,3716],{"title":1452,"path":3040,"stem":3669,"description":3042},{"title":1773,"path":3221,"stem":3671,"description":3223},{"title":3673,"path":3674,"stem":3675,"children":3718,"page":3663},[3719],{"title":3284,"path":3283,"stem":3678,"description":3286},{"title":1777,"path":3680,"stem":3681,"children":3721,"page":3663},[3722],{"title":1784,"path":3351,"stem":3684,"description":3353},1776270501536]