[{"data":1,"prerenderedAt":2656},["ShallowReactive",2],{"navigation_docs_en":3,"-en-http-pagination":342,"-en-http-pagination-surround":2651},[4,45,61,96,132,166,196,217,241,265],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":44},"Getting Started","i-lucide-rocket","\u002Fen\u002Fgetting-started","en\u002F1.getting-started",[10,15,20,24,29,34,39],{"title":11,"path":12,"stem":13,"icon":14},"Introduction","\u002Fen\u002Fgetting-started\u002Fintroduction","en\u002F1.getting-started\u002F1.introduction","i-lucide-house",{"title":16,"path":17,"stem":18,"icon":19},"Coming from Laravel","\u002Fen\u002Fgetting-started\u002Fcoming-from-laravel","en\u002F1.getting-started\u002F2.coming-from-laravel","i-lucide-arrow-right-left",{"title":21,"path":22,"stem":23,"icon":19},"Coming from Symfony","\u002Fen\u002Fgetting-started\u002Fcoming-from-symfony","en\u002F1.getting-started\u002F3.coming-from-symfony",{"title":25,"path":26,"stem":27,"icon":28},"Installation","\u002Fen\u002Fgetting-started\u002Finstallation","en\u002F1.getting-started\u002F4.installation","i-lucide-download",{"title":30,"path":31,"stem":32,"icon":33},"Your first module","\u002Fen\u002Fgetting-started\u002Ffirst-module","en\u002F1.getting-started\u002F5.first-module","i-lucide-package-plus",{"title":35,"path":36,"stem":37,"icon":38},"Project structure","\u002Fen\u002Fgetting-started\u002Fproject-structure","en\u002F1.getting-started\u002F6.project-structure","i-lucide-folder-tree",{"title":40,"path":41,"stem":42,"icon":43},"The shop sample application","\u002Fen\u002Fgetting-started\u002Fsample-app","en\u002F1.getting-started\u002F7.sample-app","i-lucide-shopping-cart",false,{"title":46,"icon":47,"path":48,"stem":49,"children":50,"page":44},"Reference","i-lucide-book-open","\u002Fen\u002Freference","en\u002F10.reference",[51,56],{"title":52,"path":53,"stem":54,"icon":55},"Configuration","\u002Fen\u002Freference\u002Fconfiguration","en\u002F10.reference\u002F1.configuration","i-lucide-settings",{"title":57,"path":58,"stem":59,"icon":60},"External dependencies","\u002Fen\u002Freference\u002Fdependencies","en\u002F10.reference\u002F2.dependencies","i-lucide-package",{"title":62,"icon":63,"path":64,"stem":65,"children":66,"page":44},"Concepts","i-lucide-lightbulb","\u002Fen\u002Fconcepts","en\u002F2.concepts",[67,72,77,82,86,91],{"title":68,"path":69,"stem":70,"icon":71},"Architecture","\u002Fen\u002Fconcepts\u002Farchitecture","en\u002F2.concepts\u002F1.architecture","i-lucide-layers",{"title":73,"path":74,"stem":75,"icon":76},"Application lifecycle","\u002Fen\u002Fconcepts\u002Fapplication-lifecycle","en\u002F2.concepts\u002F2.application-lifecycle","i-lucide-power",{"title":78,"path":79,"stem":80,"icon":81},"Error model","\u002Fen\u002Fconcepts\u002Ferror-model","en\u002F2.concepts\u002F3.error-model","i-lucide-shield-alert",{"title":52,"path":83,"stem":84,"icon":85},"\u002Fen\u002Fconcepts\u002Fconfiguration","en\u002F2.concepts\u002F4.configuration","i-lucide-settings-2",{"title":87,"path":88,"stem":89,"icon":90},"Codegen pipeline","\u002Fen\u002Fconcepts\u002Fcodegen-pipeline","en\u002F2.concepts\u002F5.codegen-pipeline","i-lucide-file-json",{"title":92,"path":93,"stem":94,"icon":95},"Design patterns","\u002Fen\u002Fconcepts\u002Fdesign-patterns","en\u002F2.concepts\u002F6.design-patterns","i-lucide-puzzle",{"title":97,"icon":98,"path":99,"stem":100,"children":101,"page":44},"HTTP & Routing","i-lucide-globe","\u002Fen\u002Fhttp","en\u002F3.http",[102,107,112,117,122,127],{"title":103,"path":104,"stem":105,"icon":106},"The server core","\u002Fen\u002Fhttp\u002Fserver","en\u002F3.http\u002F1.server","i-lucide-server",{"title":108,"path":109,"stem":110,"icon":111},"The Fiber engine","\u002Fen\u002Fhttp\u002Ffiber","en\u002F3.http\u002F2.fiber","i-lucide-zap",{"title":113,"path":114,"stem":115,"icon":116},"The chi engine","\u002Fen\u002Fhttp\u002Fchi","en\u002F3.http\u002F3.chi","i-lucide-route",{"title":118,"path":119,"stem":120,"icon":121},"Error responses","\u002Fen\u002Fhttp\u002Ferror-responses","en\u002F3.http\u002F4.error-responses","i-lucide-octagon-alert",{"title":123,"path":124,"stem":125,"icon":126},"Validation","\u002Fen\u002Fhttp\u002Fvalidation","en\u002F3.http\u002F5.validation","i-lucide-badge-check",{"title":128,"path":129,"stem":130,"icon":131},"Pagination","\u002Fen\u002Fhttp\u002Fpagination","en\u002F3.http\u002F6.pagination","i-lucide-list-ordered",{"title":133,"icon":134,"path":135,"stem":136,"children":137,"page":44},"Database","i-lucide-database","\u002Fen\u002Fdatabase","en\u002F4.database",[138,142,147,151,156,161],{"title":139,"path":140,"stem":141,"icon":134},"Overview","\u002Fen\u002Fdatabase\u002Foverview","en\u002F4.database\u002F1.overview",{"title":143,"path":144,"stem":145,"icon":146},"pgx","\u002Fen\u002Fdatabase\u002Fpgx","en\u002F4.database\u002F2.pgx","i-lucide-plug",{"title":148,"path":149,"stem":150,"icon":71},"bun","\u002Fen\u002Fdatabase\u002Fbun","en\u002F4.database\u002F3.bun",{"title":152,"path":153,"stem":154,"icon":155},"Transactions","\u002Fen\u002Fdatabase\u002Ftransactions","en\u002F4.database\u002F4.transactions","i-lucide-git-merge",{"title":157,"path":158,"stem":159,"icon":160},"Migrations","\u002Fen\u002Fdatabase\u002Fmigrations","en\u002F4.database\u002F5.migrations","i-lucide-file-stack",{"title":162,"path":163,"stem":164,"icon":165},"Seeders","\u002Fen\u002Fdatabase\u002Fseeding","en\u002F4.database\u002F6.seeding","i-lucide-sprout",{"title":167,"icon":168,"path":169,"stem":170,"children":171,"page":44},"Async & Background","i-lucide-workflow","\u002Fen\u002Fasync","en\u002F5.async",[172,176,181,186,191],{"title":173,"path":174,"stem":175,"icon":131},"Queue","\u002Fen\u002Fasync\u002Fqueue","en\u002F5.async\u002F1.queue",{"title":177,"path":178,"stem":179,"icon":180},"Events","\u002Fen\u002Fasync\u002Fevents","en\u002F5.async\u002F2.events","i-lucide-radio",{"title":182,"path":183,"stem":184,"icon":185},"Outbox","\u002Fen\u002Fasync\u002Foutbox","en\u002F5.async\u002F3.outbox","i-lucide-inbox",{"title":187,"path":188,"stem":189,"icon":190},"Scheduling","\u002Fen\u002Fasync\u002Fscheduler","en\u002F5.async\u002F4.scheduler","i-lucide-calendar-clock",{"title":192,"path":193,"stem":194,"icon":195},"Worker","\u002Fen\u002Fasync\u002Fworker","en\u002F5.async\u002F5.worker","i-lucide-cog",{"title":197,"icon":198,"path":199,"stem":200,"children":201,"page":44},"Security","i-lucide-shield-check","\u002Fen\u002Fsecurity","en\u002F6.security",[202,207,212],{"title":203,"path":204,"stem":205,"icon":206},"Authentication","\u002Fen\u002Fsecurity\u002Fauthentication","en\u002F6.security\u002F1.authentication","i-lucide-key-round",{"title":208,"path":209,"stem":210,"icon":211},"RBAC","\u002Fen\u002Fsecurity\u002Frbac","en\u002F6.security\u002F2.rbac","i-lucide-users",{"title":213,"path":214,"stem":215,"icon":216},"Policies","\u002Fen\u002Fsecurity\u002Fpolicies","en\u002F6.security\u002F3.policies","i-lucide-gavel",{"title":218,"icon":219,"path":220,"stem":221,"children":222,"page":44},"Observability","i-lucide-activity","\u002Fen\u002Fobservability","en\u002F7.observability",[223,226,231,236],{"title":139,"path":224,"stem":225,"icon":219},"\u002Fen\u002Fobservability\u002Foverview","en\u002F7.observability\u002F1.overview",{"title":227,"path":228,"stem":229,"icon":230},"Logging","\u002Fen\u002Fobservability\u002Flogging","en\u002F7.observability\u002F2.logging","i-lucide-scroll-text",{"title":232,"path":233,"stem":234,"icon":235},"OpenTelemetry","\u002Fen\u002Fobservability\u002Fopentelemetry","en\u002F7.observability\u002F3.opentelemetry","i-lucide-radar",{"title":237,"path":238,"stem":239,"icon":240},"Sentry","\u002Fen\u002Fobservability\u002Fsentry","en\u002F7.observability\u002F4.sentry","i-lucide-bug",{"title":242,"icon":243,"path":244,"stem":245,"children":246,"page":44},"Storage & Mail","i-lucide-hard-drive","\u002Fen\u002Fstorage-mail","en\u002F8.storage-mail",[247,251,256,261],{"title":248,"path":249,"stem":250,"icon":243},"Storage","\u002Fen\u002Fstorage-mail\u002Fstorage","en\u002F8.storage-mail\u002F1.storage",{"title":252,"path":253,"stem":254,"icon":255},"Email","\u002Fen\u002Fstorage-mail\u002Fmail","en\u002F8.storage-mail\u002F2.mail","i-lucide-mail",{"title":257,"path":258,"stem":259,"icon":260},"Notifications","\u002Fen\u002Fstorage-mail\u002Fnotifications","en\u002F8.storage-mail\u002F3.notifications","i-lucide-bell",{"title":262,"path":263,"stem":264,"icon":180},"Realtime","\u002Fen\u002Fstorage-mail\u002Frealtime","en\u002F8.storage-mail\u002F4.realtime",{"title":266,"icon":267,"path":268,"stem":269,"children":270,"page":44},"CLI Reference","i-lucide-terminal","\u002Fen\u002Fcli","en\u002F9.cli",[271,274,279,284,289,294,299,303,308,313,318,323,328,333,337],{"title":139,"path":272,"stem":273,"icon":267},"\u002Fen\u002Fcli\u002Foverview","en\u002F9.cli\u002F1.overview",{"title":275,"path":276,"stem":277,"icon":278},"add db","\u002Fen\u002Fcli\u002Fadd-db","en\u002F9.cli\u002F10.add-db","i-lucide-database-zap",{"title":280,"path":281,"stem":282,"icon":283},"add compose \u002F add docker","\u002Fen\u002Fcli\u002Fadd-compose","en\u002F9.cli\u002F11.add-compose","i-lucide-container",{"title":285,"path":286,"stem":287,"icon":288},"add mail","\u002Fen\u002Fcli\u002Fadd-mail","en\u002F9.cli\u002F12.add-mail","i-lucide-mail-plus",{"title":290,"path":291,"stem":292,"icon":293},"add notification","\u002Fen\u002Fcli\u002Fadd-notification","en\u002F9.cli\u002F13.add-notification","i-lucide-bell-plus",{"title":295,"path":296,"stem":297,"icon":298},"add realtime","\u002Fen\u002Fcli\u002Fadd-realtime","en\u002F9.cli\u002F14.add-realtime","i-lucide-radio-tower",{"title":300,"path":301,"stem":302,"icon":165},"add seeder","\u002Fen\u002Fcli\u002Fadd-seeder","en\u002F9.cli\u002F15.add-seeder",{"title":304,"path":305,"stem":306,"icon":307},"new project","\u002Fen\u002Fcli\u002Fnew-project","en\u002F9.cli\u002F2.new-project","i-lucide-folder-plus",{"title":309,"path":310,"stem":311,"icon":312},"new module","\u002Fen\u002Fcli\u002Fnew-module","en\u002F9.cli\u002F3.new-module","i-lucide-blocks",{"title":314,"path":315,"stem":316,"icon":317},"add surface","\u002Fen\u002Fcli\u002Fadd-surface","en\u002F9.cli\u002F4.add-surface","i-lucide-layers-2",{"title":319,"path":320,"stem":321,"icon":322},"add core","\u002Fen\u002Fcli\u002Fadd-core","en\u002F9.cli\u002F5.add-core","i-lucide-box",{"title":324,"path":325,"stem":326,"icon":327},"add handler","\u002Fen\u002Fcli\u002Fadd-handler","en\u002F9.cli\u002F6.add-handler","i-lucide-webhook",{"title":329,"path":330,"stem":331,"icon":332},"new migration","\u002Fen\u002Fcli\u002Fnew-migration","en\u002F9.cli\u002F7.new-migration","i-lucide-file-plus",{"title":334,"path":335,"stem":336,"icon":195},"worker generators","\u002Fen\u002Fcli\u002Fworker-generators","en\u002F9.cli\u002F8.worker-generators",{"title":338,"path":339,"stem":340,"icon":341},"upgrade templates","\u002Fen\u002Fcli\u002Fupgrade-templates","en\u002F9.cli\u002F9.upgrade-templates","i-lucide-refresh-cw",{"id":343,"title":128,"body":344,"description":2644,"extension":2645,"links":2646,"meta":2647,"navigation":2648,"path":129,"seo":2649,"stem":130,"__hash__":2650},"docs_en\u002Fen\u002F3.http\u002F6.pagination.md",{"type":345,"value":346,"toc":2634},"minimark",[347,365,394,399,414,550,671,674,768,795,802,926,939,943,966,1311,1344,1348,1359,1570,1770,2324,2368,2372,2379,2434,2438,2456,2464,2482,2486,2569,2572,2576,2597,2601,2630],[348,349,350,351,355,356,360,361,364],"p",{},"The ",[352,353,354],"code",{},"paginate"," package centralizes the two boring-but-dangerous halves of pagination: ",[357,358,359],"strong",{},"normalizing"," client input (bounds, defaults, overflow protection) and ",[357,362,363],{},"wrapping"," results for the generated response models. It offers two modes: classic page\u002Fper-page (offset) pagination and cursor (keyset) pagination.",[366,367,372],"pre",{"className":368,"code":369,"language":370,"meta":371,"style":371},"language-go shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","import \"github.com\u002Fgp-system\u002Fgpsystem\u002Fpaginate\"\n","go","",[352,373,374],{"__ignoreMap":371},[375,376,379,383,387,391],"span",{"class":377,"line":378},"line",1,[375,380,382],{"class":381},"s7zQu","import",[375,384,386],{"class":385},"sMK4o"," \"",[375,388,390],{"class":389},"sBMFI","github.com\u002Fgp-system\u002Fgpsystem\u002Fpaginate",[375,392,393],{"class":385},"\"\n",[395,396,398],"h2",{"id":397},"offset-pagination","Offset pagination",[348,400,401,405,406,409,410,413],{},[402,403,404],"a",{"href":88},"Generated"," request parameters arrive as optional pointers (",[352,407,408],{},"*int","); ",[352,411,412],{},"Normalize"," clamps them once, instead of in every handler:",[366,415,417],{"className":368,"code":416,"language":370,"meta":371,"style":371},"func Normalize(page, perPage *int, opts ...Option) Params\n\ntype Params struct{ Page, PerPage int }\nfunc (p Params) Limit() int  \u002F\u002F SQL LIMIT\nfunc (p Params) Offset() int \u002F\u002F SQL OFFSET: (Page-1)*PerPage\n",[352,418,419,465,472,498,527],{"__ignoreMap":371},[375,420,421,424,428,431,435,438,441,444,448,450,453,456,459,462],{"class":377,"line":378},[375,422,423],{"class":385},"func",[375,425,427],{"class":426},"s2Zo4"," Normalize",[375,429,430],{"class":385},"(",[375,432,434],{"class":433},"sHdIc","page",[375,436,437],{"class":385},",",[375,439,440],{"class":433}," perPage",[375,442,443],{"class":385}," *",[375,445,447],{"class":446},"spNyl","int",[375,449,437],{"class":385},[375,451,452],{"class":433}," opts",[375,454,455],{"class":385}," ...",[375,457,458],{"class":389},"Option",[375,460,461],{"class":385},")",[375,463,464],{"class":389}," Params\n",[375,466,468],{"class":377,"line":467},2,[375,469,471],{"emptyLinePlaceholder":470},true,"\n",[375,473,475,478,481,484,488,490,493,495],{"class":377,"line":474},3,[375,476,477],{"class":385},"type",[375,479,480],{"class":389}," Params",[375,482,483],{"class":385}," struct{",[375,485,487],{"class":486},"sTEyZ"," Page",[375,489,437],{"class":385},[375,491,492],{"class":486}," PerPage ",[375,494,447],{"class":446},[375,496,497],{"class":385}," }\n",[375,499,501,503,506,509,512,514,517,520,523],{"class":377,"line":500},4,[375,502,423],{"class":385},[375,504,505],{"class":385}," (",[375,507,508],{"class":433},"p ",[375,510,511],{"class":389},"Params",[375,513,461],{"class":385},[375,515,516],{"class":426}," Limit",[375,518,519],{"class":385},"()",[375,521,522],{"class":446}," int",[375,524,526],{"class":525},"sHwdD","  \u002F\u002F SQL LIMIT\n",[375,528,530,532,534,536,538,540,543,545,547],{"class":377,"line":529},5,[375,531,423],{"class":385},[375,533,505],{"class":385},[375,535,508],{"class":433},[375,537,511],{"class":389},[375,539,461],{"class":385},[375,541,542],{"class":426}," Offset",[375,544,519],{"class":385},[375,546,522],{"class":446},[375,548,549],{"class":525}," \u002F\u002F SQL OFFSET: (Page-1)*PerPage\n",[366,551,553],{"className":368,"code":552,"language":370,"meta":371,"style":371},"p := paginate.Normalize(req.Params.Page, req.Params.PerPage) \u002F\u002F defaults: page 1, perPage 20\nrows, err := r.db.Query(ctx,\n    `SELECT ... ORDER BY created_at DESC LIMIT $1 OFFSET $2`, p.Limit(), p.Offset())\n",[352,554,555,603,636],{"__ignoreMap":371},[375,556,557,559,562,565,568,570,572,575,577,579,581,584,586,589,591,593,595,598,600],{"class":377,"line":378},[375,558,508],{"class":486},[375,560,561],{"class":385},":=",[375,563,564],{"class":486}," paginate",[375,566,567],{"class":385},".",[375,569,412],{"class":426},[375,571,430],{"class":385},[375,573,574],{"class":486},"req",[375,576,567],{"class":385},[375,578,511],{"class":486},[375,580,567],{"class":385},[375,582,583],{"class":486},"Page",[375,585,437],{"class":385},[375,587,588],{"class":486}," req",[375,590,567],{"class":385},[375,592,511],{"class":486},[375,594,567],{"class":385},[375,596,597],{"class":486},"PerPage",[375,599,461],{"class":385},[375,601,602],{"class":525}," \u002F\u002F defaults: page 1, perPage 20\n",[375,604,605,608,610,613,615,618,620,623,625,628,630,633],{"class":377,"line":467},[375,606,607],{"class":486},"rows",[375,609,437],{"class":385},[375,611,612],{"class":486}," err ",[375,614,561],{"class":385},[375,616,617],{"class":486}," r",[375,619,567],{"class":385},[375,621,622],{"class":486},"db",[375,624,567],{"class":385},[375,626,627],{"class":426},"Query",[375,629,430],{"class":385},[375,631,632],{"class":486},"ctx",[375,634,635],{"class":385},",\n",[375,637,638,641,645,648,650,653,655,658,661,663,665,668],{"class":377,"line":474},[375,639,640],{"class":385},"    `",[375,642,644],{"class":643},"sfazB","SELECT ... ORDER BY created_at DESC LIMIT $1 OFFSET $2",[375,646,647],{"class":385},"`",[375,649,437],{"class":385},[375,651,652],{"class":486}," p",[375,654,567],{"class":385},[375,656,657],{"class":426},"Limit",[375,659,660],{"class":385},"(),",[375,662,652],{"class":486},[375,664,567],{"class":385},[375,666,667],{"class":426},"Offset",[375,669,670],{"class":385},"())\n",[348,672,673],{},"The bounds are tunable with options:",[366,675,677],{"className":368,"code":676,"language":370,"meta":371,"style":371},"p := paginate.Normalize(page, perPage,\n    paginate.WithDefaultPerPage(15), \u002F\u002F when the request omits perPage (default: 20)\n    paginate.WithMaxPerPage(50),     \u002F\u002F hard cap, whatever the client asks for (default: 100)\n    paginate.WithMaxPage(10_000))    \u002F\u002F cap on the page number (default: 1,000,000)\n",[352,678,679,701,723,742],{"__ignoreMap":371},[375,680,681,683,685,687,689,691,693,695,697,699],{"class":377,"line":378},[375,682,508],{"class":486},[375,684,561],{"class":385},[375,686,564],{"class":486},[375,688,567],{"class":385},[375,690,412],{"class":426},[375,692,430],{"class":385},[375,694,434],{"class":486},[375,696,437],{"class":385},[375,698,440],{"class":486},[375,700,635],{"class":385},[375,702,703,706,708,711,713,717,720],{"class":377,"line":467},[375,704,705],{"class":486},"    paginate",[375,707,567],{"class":385},[375,709,710],{"class":426},"WithDefaultPerPage",[375,712,430],{"class":385},[375,714,716],{"class":715},"sbssI","15",[375,718,719],{"class":385},"),",[375,721,722],{"class":525}," \u002F\u002F when the request omits perPage (default: 20)\n",[375,724,725,727,729,732,734,737,739],{"class":377,"line":474},[375,726,705],{"class":486},[375,728,567],{"class":385},[375,730,731],{"class":426},"WithMaxPerPage",[375,733,430],{"class":385},[375,735,736],{"class":715},"50",[375,738,719],{"class":385},[375,740,741],{"class":525},"     \u002F\u002F hard cap, whatever the client asks for (default: 100)\n",[375,743,744,746,748,751,753,756,759,762,765],{"class":377,"line":500},[375,745,705],{"class":486},[375,747,567],{"class":385},[375,749,750],{"class":426},"WithMaxPage",[375,752,430],{"class":385},[375,754,755],{"class":715},"10",[375,757,758],{"class":385},"_",[375,760,761],{"class":715},"000",[375,763,764],{"class":385},"))",[375,766,767],{"class":525},"    \u002F\u002F cap on the page number (default: 1,000,000)\n",[348,769,770,772,773,776,777,780,781,784,785,788,789,791,792,794],{},[352,771,750],{}," is not cosmetic: ",[352,774,775],{},"Offset()"," is the product ",[352,778,779],{},"(page-1)*perPage",". Without a cap, a hostile ",[352,782,783],{},"?page="," value could provoke an int overflow or a pathological ",[352,786,787],{},"OFFSET",". After ",[352,790,412],{},", ",[352,793,775],{}," is guaranteed safe.",[348,796,797,798,801],{},"Results are wrapped for the response by ",[352,799,800],{},"Page[T]",":",[366,803,805],{"className":368,"code":804,"language":370,"meta":371,"style":371},"func NewPage[T any](items []T, total int64, p Params) Page[T]\n\ntype Page[T any] struct {\n    Items   []T\n    Total   int64\n    Page    int\n    PerPage int\n}\n",[352,806,807,859,863,884,895,903,912,920],{"__ignoreMap":371},[375,808,809,811,814,817,820,823,826,829,832,834,836,839,842,844,846,848,850,852,854,856],{"class":377,"line":378},[375,810,423],{"class":385},[375,812,813],{"class":426}," NewPage",[375,815,816],{"class":385},"[",[375,818,819],{"class":433},"T",[375,821,822],{"class":389}," any",[375,824,825],{"class":385},"](",[375,827,828],{"class":433},"items",[375,830,831],{"class":385}," []",[375,833,819],{"class":389},[375,835,437],{"class":385},[375,837,838],{"class":433}," total",[375,840,841],{"class":446}," int64",[375,843,437],{"class":385},[375,845,652],{"class":433},[375,847,480],{"class":389},[375,849,461],{"class":385},[375,851,487],{"class":389},[375,853,816],{"class":385},[375,855,819],{"class":389},[375,857,858],{"class":385},"]\n",[375,860,861],{"class":377,"line":467},[375,862,471],{"emptyLinePlaceholder":470},[375,864,865,867,869,871,873,875,878,881],{"class":377,"line":474},[375,866,477],{"class":385},[375,868,487],{"class":389},[375,870,816],{"class":385},[375,872,819],{"class":433},[375,874,822],{"class":389},[375,876,877],{"class":385},"]",[375,879,880],{"class":385}," struct",[375,882,883],{"class":385}," {\n",[375,885,886,889,892],{"class":377,"line":500},[375,887,888],{"class":486},"    Items   ",[375,890,891],{"class":385},"[]",[375,893,894],{"class":389},"T\n",[375,896,897,900],{"class":377,"line":529},[375,898,899],{"class":486},"    Total   ",[375,901,902],{"class":446},"int64\n",[375,904,906,909],{"class":377,"line":905},6,[375,907,908],{"class":486},"    Page    ",[375,910,911],{"class":446},"int\n",[375,913,915,918],{"class":377,"line":914},7,[375,916,917],{"class":486},"    PerPage ",[375,919,911],{"class":446},[375,921,923],{"class":377,"line":922},8,[375,924,925],{"class":385},"}\n",[348,927,928,930,931,934,935,938],{},[352,929,800],{}," mirrors the scaffolded ",[352,932,933],{},"shared\u002Fpaginator.tsp"," ",[352,936,937],{},"Paginated\u003CT>"," model, so mapping onto the generated response model is mechanical.",[395,940,942],{"id":941},"cursor-keyset-pagination","Cursor (keyset) pagination",[348,944,945,946,949,950,953,954,957,958,961,962,965],{},"Offset pagination has two structural problems on large or frequently mutated tables: ",[352,947,948],{},"OFFSET n"," computes and ",[357,951,952],{},"throws away"," the first ",[352,955,956],{},"n"," rows on every request (O(n), page 400 means combing through 8000 rows), and under concurrent inserts the pages ",[357,959,960],{},"drift",": the same row shows up on two pages, or on none. Cursor pagination solves both: the client receives an opaque cursor encoding the sort key of the last row it saw, and the next page continues from there with an indexed seek (",[352,963,964],{},"WHERE (created_at, id) \u003C (...)","), at near-constant cost, however deep you page.",[366,967,969],{"className":368,"code":968,"language":370,"meta":371,"style":371},"func NormalizeCursor(cursor *string, limit *int, sort string, opts ...Option) (CursorParams, error)\n\ntype CursorParams struct{ Limit int \u002F* + decoded keys *\u002F }\nfunc (p CursorParams) HasCursor() bool          \u002F\u002F was a cursor supplied, i.e. is this a page after the first\nfunc (p CursorParams) DecodeKey(dests ...any) error \u002F\u002F read out the key values, Scan-style\nfunc (p CursorParams) FetchLimit() int          \u002F\u002F Limit+1, how we detect whether another page follows\nfunc (p CursorParams) NextCursor(keys ...any) (string, error)\n\nfunc NewCursorPage[T any](rows []T, p CursorParams, key func(last T) []any) (CursorPage[T], error)\n\ntype CursorPage[T any] struct {\n    Items      []T\n    NextCursor string \u002F\u002F \"\" on the last page\n    HasMore    bool\n}\n\nvar ErrInvalidCursor \u002F\u002F errs definition: HTTP 400, code: paginate_invalid_cursor\n",[352,970,971,1028,1032,1051,1074,1106,1128,1164,1168,1234,1239,1259,1269,1280,1289,1294,1299],{"__ignoreMap":371},[375,972,973,975,978,980,983,985,988,990,993,995,997,999,1002,1005,1007,1009,1011,1013,1015,1017,1020,1022,1025],{"class":377,"line":378},[375,974,423],{"class":385},[375,976,977],{"class":426}," NormalizeCursor",[375,979,430],{"class":385},[375,981,982],{"class":433},"cursor",[375,984,443],{"class":385},[375,986,987],{"class":446},"string",[375,989,437],{"class":385},[375,991,992],{"class":433}," limit",[375,994,443],{"class":385},[375,996,447],{"class":446},[375,998,437],{"class":385},[375,1000,1001],{"class":433}," sort",[375,1003,1004],{"class":446}," string",[375,1006,437],{"class":385},[375,1008,452],{"class":433},[375,1010,455],{"class":385},[375,1012,458],{"class":389},[375,1014,461],{"class":385},[375,1016,505],{"class":385},[375,1018,1019],{"class":389},"CursorParams",[375,1021,437],{"class":385},[375,1023,1024],{"class":446}," error",[375,1026,1027],{"class":385},")\n",[375,1029,1030],{"class":377,"line":467},[375,1031,471],{"emptyLinePlaceholder":470},[375,1033,1034,1036,1039,1041,1044,1046,1049],{"class":377,"line":474},[375,1035,477],{"class":385},[375,1037,1038],{"class":389}," CursorParams",[375,1040,483],{"class":385},[375,1042,1043],{"class":486}," Limit ",[375,1045,447],{"class":446},[375,1047,1048],{"class":525}," \u002F* + decoded keys *\u002F",[375,1050,497],{"class":385},[375,1052,1053,1055,1057,1059,1061,1063,1066,1068,1071],{"class":377,"line":500},[375,1054,423],{"class":385},[375,1056,505],{"class":385},[375,1058,508],{"class":433},[375,1060,1019],{"class":389},[375,1062,461],{"class":385},[375,1064,1065],{"class":426}," HasCursor",[375,1067,519],{"class":385},[375,1069,1070],{"class":446}," bool",[375,1072,1073],{"class":525},"          \u002F\u002F was a cursor supplied, i.e. is this a page after the first\n",[375,1075,1076,1078,1080,1082,1084,1086,1089,1091,1094,1096,1099,1101,1103],{"class":377,"line":529},[375,1077,423],{"class":385},[375,1079,505],{"class":385},[375,1081,508],{"class":433},[375,1083,1019],{"class":389},[375,1085,461],{"class":385},[375,1087,1088],{"class":426}," DecodeKey",[375,1090,430],{"class":385},[375,1092,1093],{"class":433},"dests",[375,1095,455],{"class":385},[375,1097,1098],{"class":389},"any",[375,1100,461],{"class":385},[375,1102,1024],{"class":446},[375,1104,1105],{"class":525}," \u002F\u002F read out the key values, Scan-style\n",[375,1107,1108,1110,1112,1114,1116,1118,1121,1123,1125],{"class":377,"line":905},[375,1109,423],{"class":385},[375,1111,505],{"class":385},[375,1113,508],{"class":433},[375,1115,1019],{"class":389},[375,1117,461],{"class":385},[375,1119,1120],{"class":426}," FetchLimit",[375,1122,519],{"class":385},[375,1124,522],{"class":446},[375,1126,1127],{"class":525},"          \u002F\u002F Limit+1, how we detect whether another page follows\n",[375,1129,1130,1132,1134,1136,1138,1140,1143,1145,1148,1150,1152,1154,1156,1158,1160,1162],{"class":377,"line":914},[375,1131,423],{"class":385},[375,1133,505],{"class":385},[375,1135,508],{"class":433},[375,1137,1019],{"class":389},[375,1139,461],{"class":385},[375,1141,1142],{"class":426}," NextCursor",[375,1144,430],{"class":385},[375,1146,1147],{"class":433},"keys",[375,1149,455],{"class":385},[375,1151,1098],{"class":389},[375,1153,461],{"class":385},[375,1155,505],{"class":385},[375,1157,987],{"class":446},[375,1159,437],{"class":385},[375,1161,1024],{"class":446},[375,1163,1027],{"class":385},[375,1165,1166],{"class":377,"line":922},[375,1167,471],{"emptyLinePlaceholder":470},[375,1169,1171,1173,1176,1178,1180,1182,1184,1186,1188,1190,1192,1194,1196,1198,1201,1204,1207,1210,1212,1214,1216,1218,1220,1223,1225,1227,1230,1232],{"class":377,"line":1170},9,[375,1172,423],{"class":385},[375,1174,1175],{"class":426}," NewCursorPage",[375,1177,816],{"class":385},[375,1179,819],{"class":433},[375,1181,822],{"class":389},[375,1183,825],{"class":385},[375,1185,607],{"class":433},[375,1187,831],{"class":385},[375,1189,819],{"class":389},[375,1191,437],{"class":385},[375,1193,652],{"class":433},[375,1195,1038],{"class":389},[375,1197,437],{"class":385},[375,1199,1200],{"class":433}," key",[375,1202,1203],{"class":385}," func(",[375,1205,1206],{"class":433},"last",[375,1208,1209],{"class":389}," T",[375,1211,461],{"class":385},[375,1213,831],{"class":385},[375,1215,1098],{"class":389},[375,1217,461],{"class":385},[375,1219,505],{"class":385},[375,1221,1222],{"class":389},"CursorPage",[375,1224,816],{"class":385},[375,1226,819],{"class":389},[375,1228,1229],{"class":385},"],",[375,1231,1024],{"class":446},[375,1233,1027],{"class":385},[375,1235,1237],{"class":377,"line":1236},10,[375,1238,471],{"emptyLinePlaceholder":470},[375,1240,1242,1244,1247,1249,1251,1253,1255,1257],{"class":377,"line":1241},11,[375,1243,477],{"class":385},[375,1245,1246],{"class":389}," CursorPage",[375,1248,816],{"class":385},[375,1250,819],{"class":433},[375,1252,822],{"class":389},[375,1254,877],{"class":385},[375,1256,880],{"class":385},[375,1258,883],{"class":385},[375,1260,1262,1265,1267],{"class":377,"line":1261},12,[375,1263,1264],{"class":486},"    Items      ",[375,1266,891],{"class":385},[375,1268,894],{"class":389},[375,1270,1272,1275,1277],{"class":377,"line":1271},13,[375,1273,1274],{"class":486},"    NextCursor ",[375,1276,987],{"class":446},[375,1278,1279],{"class":525}," \u002F\u002F \"\" on the last page\n",[375,1281,1283,1286],{"class":377,"line":1282},14,[375,1284,1285],{"class":486},"    HasMore    ",[375,1287,1288],{"class":446},"bool\n",[375,1290,1292],{"class":377,"line":1291},15,[375,1293,925],{"class":385},[375,1295,1297],{"class":377,"line":1296},16,[375,1298,471],{"emptyLinePlaceholder":470},[375,1300,1302,1305,1308],{"class":377,"line":1301},17,[375,1303,1304],{"class":385},"var",[375,1306,1307],{"class":486}," ErrInvalidCursor ",[375,1309,1310],{"class":525},"\u002F\u002F errs definition: HTTP 400, code: paginate_invalid_cursor\n",[348,1312,1313,1316,1317,1320,1321,1323,1324,1326,1327,505,1329,1331,1332,1335,1336,1339,1340,1343],{},[352,1314,1315],{},"NormalizeCursor"," clamps ",[352,1318,1319],{},"limit"," with the same ",[352,1322,710],{},"\u002F",[352,1325,731],{}," options as ",[352,1328,412],{},[352,1330,750],{}," is ignored, there is no page number here). The ",[352,1333,1334],{},"sort"," argument must be ",[357,1337,1338],{},"exactly"," the ",[352,1341,1342],{},"ORDER BY"," expression the query uses: its fingerprint is stamped into issued cursors, and a cursor issued for a different sort is later rejected.",[395,1345,1347],{"id":1346},"the-shop-product-listing-end-to-end","The shop product listing, end to end",[348,1349,1350,1351,1354,1355,1358],{},"The full path of ",[352,1352,1353],{},"GET \u002Fapi\u002Fv1\u002Fshop\u002Fproducts?cursor=...&limit=..."," in the ",[402,1356,1357],{"href":41},"sample app",", handler → service → repository → response:",[366,1360,1363],{"className":368,"code":1361,"filename":1362,"language":370,"meta":371,"style":371},"func (h *Handler) ListProducts(ctx context.Context, req gen.ListProductsRequest) (gen.ListProductsResponse, error) {\n    page, err := h.svc.ListProducts(ctx, req.Params.Cursor, req.Params.Limit)\n    if err != nil {\n        return nil, service.ErrListProducts.Wrap(err, \"api: list products\")\n    }\n    return mapper.ToProductList(page), nil \u002F\u002F paginate.CursorPage[Product] → gen.CursorPageProduct\n}\n","internal\u002Fmodules\u002Fshop\u002Fsurfaces\u002Fapi\u002Fhttp\u002Fhandler.go",[352,1364,1365,1429,1484,1499,1537,1542,1566],{"__ignoreMap":371},[375,1366,1367,1369,1371,1374,1377,1380,1382,1385,1387,1389,1392,1394,1397,1399,1401,1404,1406,1409,1411,1413,1416,1418,1421,1423,1425,1427],{"class":377,"line":378},[375,1368,423],{"class":385},[375,1370,505],{"class":385},[375,1372,1373],{"class":433},"h ",[375,1375,1376],{"class":385},"*",[375,1378,1379],{"class":389},"Handler",[375,1381,461],{"class":385},[375,1383,1384],{"class":426}," ListProducts",[375,1386,430],{"class":385},[375,1388,632],{"class":433},[375,1390,1391],{"class":389}," context",[375,1393,567],{"class":385},[375,1395,1396],{"class":389},"Context",[375,1398,437],{"class":385},[375,1400,588],{"class":433},[375,1402,1403],{"class":389}," gen",[375,1405,567],{"class":385},[375,1407,1408],{"class":389},"ListProductsRequest",[375,1410,461],{"class":385},[375,1412,505],{"class":385},[375,1414,1415],{"class":389},"gen",[375,1417,567],{"class":385},[375,1419,1420],{"class":389},"ListProductsResponse",[375,1422,437],{"class":385},[375,1424,1024],{"class":446},[375,1426,461],{"class":385},[375,1428,883],{"class":385},[375,1430,1431,1434,1436,1438,1440,1443,1445,1448,1450,1453,1455,1457,1459,1461,1463,1465,1467,1470,1472,1474,1476,1478,1480,1482],{"class":377,"line":467},[375,1432,1433],{"class":486},"    page",[375,1435,437],{"class":385},[375,1437,612],{"class":486},[375,1439,561],{"class":385},[375,1441,1442],{"class":486}," h",[375,1444,567],{"class":385},[375,1446,1447],{"class":486},"svc",[375,1449,567],{"class":385},[375,1451,1452],{"class":426},"ListProducts",[375,1454,430],{"class":385},[375,1456,632],{"class":486},[375,1458,437],{"class":385},[375,1460,588],{"class":486},[375,1462,567],{"class":385},[375,1464,511],{"class":486},[375,1466,567],{"class":385},[375,1468,1469],{"class":486},"Cursor",[375,1471,437],{"class":385},[375,1473,588],{"class":486},[375,1475,567],{"class":385},[375,1477,511],{"class":486},[375,1479,567],{"class":385},[375,1481,657],{"class":486},[375,1483,1027],{"class":385},[375,1485,1486,1489,1491,1494,1497],{"class":377,"line":474},[375,1487,1488],{"class":381},"    if",[375,1490,612],{"class":486},[375,1492,1493],{"class":385},"!=",[375,1495,1496],{"class":385}," nil",[375,1498,883],{"class":385},[375,1500,1501,1504,1507,1510,1512,1515,1517,1520,1522,1525,1527,1529,1532,1535],{"class":377,"line":500},[375,1502,1503],{"class":381},"        return",[375,1505,1506],{"class":385}," nil,",[375,1508,1509],{"class":486}," service",[375,1511,567],{"class":385},[375,1513,1514],{"class":486},"ErrListProducts",[375,1516,567],{"class":385},[375,1518,1519],{"class":426},"Wrap",[375,1521,430],{"class":385},[375,1523,1524],{"class":486},"err",[375,1526,437],{"class":385},[375,1528,386],{"class":385},[375,1530,1531],{"class":643},"api: list products",[375,1533,1534],{"class":385},"\"",[375,1536,1027],{"class":385},[375,1538,1539],{"class":377,"line":529},[375,1540,1541],{"class":385},"    }\n",[375,1543,1544,1547,1550,1552,1555,1557,1559,1561,1563],{"class":377,"line":905},[375,1545,1546],{"class":381},"    return",[375,1548,1549],{"class":486}," mapper",[375,1551,567],{"class":385},[375,1553,1554],{"class":426},"ToProductList",[375,1556,430],{"class":385},[375,1558,434],{"class":486},[375,1560,719],{"class":385},[375,1562,1496],{"class":385},[375,1564,1565],{"class":525}," \u002F\u002F paginate.CursorPage[Product] → gen.CursorPageProduct\n",[375,1567,1568],{"class":377,"line":914},[375,1569,925],{"class":385},[366,1571,1574],{"className":368,"code":1572,"filename":1573,"language":370,"meta":371,"style":371},"const productSort = \"created_at DESC, id DESC\" \u002F\u002F must match the repository's ORDER BY\n\nfunc (s *Service) ListProducts(ctx context.Context, cursor *string, limit *int) (paginate.CursorPage[Product], error) {\n    p, err := paginate.NormalizeCursor(cursor, limit, productSort)\n    if err != nil {\n        return paginate.CursorPage[Product]{}, err \u002F\u002F ErrInvalidCursor → HTTP 400\n    }\n    return s.products.List(ctx, p)\n}\n","internal\u002Fmodules\u002Fshop\u002Fsurfaces\u002Fapi\u002Fservice\u002Fservice.go",[352,1575,1576,1597,1601,1669,1701,1713,1735,1739,1766],{"__ignoreMap":371},[375,1577,1578,1581,1584,1587,1589,1592,1594],{"class":377,"line":378},[375,1579,1580],{"class":385},"const",[375,1582,1583],{"class":486}," productSort ",[375,1585,1586],{"class":385},"=",[375,1588,386],{"class":385},[375,1590,1591],{"class":643},"created_at DESC, id DESC",[375,1593,1534],{"class":385},[375,1595,1596],{"class":525}," \u002F\u002F must match the repository's ORDER BY\n",[375,1598,1599],{"class":377,"line":467},[375,1600,471],{"emptyLinePlaceholder":470},[375,1602,1603,1605,1607,1610,1612,1615,1617,1619,1621,1623,1625,1627,1629,1631,1634,1636,1638,1640,1642,1644,1646,1648,1650,1652,1654,1656,1658,1661,1663,1665,1667],{"class":377,"line":474},[375,1604,423],{"class":385},[375,1606,505],{"class":385},[375,1608,1609],{"class":433},"s ",[375,1611,1376],{"class":385},[375,1613,1614],{"class":389},"Service",[375,1616,461],{"class":385},[375,1618,1384],{"class":426},[375,1620,430],{"class":385},[375,1622,632],{"class":433},[375,1624,1391],{"class":389},[375,1626,567],{"class":385},[375,1628,1396],{"class":389},[375,1630,437],{"class":385},[375,1632,1633],{"class":433}," cursor",[375,1635,443],{"class":385},[375,1637,987],{"class":446},[375,1639,437],{"class":385},[375,1641,992],{"class":433},[375,1643,443],{"class":385},[375,1645,447],{"class":446},[375,1647,461],{"class":385},[375,1649,505],{"class":385},[375,1651,354],{"class":389},[375,1653,567],{"class":385},[375,1655,1222],{"class":389},[375,1657,816],{"class":385},[375,1659,1660],{"class":389},"Product",[375,1662,1229],{"class":385},[375,1664,1024],{"class":446},[375,1666,461],{"class":385},[375,1668,883],{"class":385},[375,1670,1671,1674,1676,1678,1680,1682,1684,1686,1688,1690,1692,1694,1696,1699],{"class":377,"line":500},[375,1672,1673],{"class":486},"    p",[375,1675,437],{"class":385},[375,1677,612],{"class":486},[375,1679,561],{"class":385},[375,1681,564],{"class":486},[375,1683,567],{"class":385},[375,1685,1315],{"class":426},[375,1687,430],{"class":385},[375,1689,982],{"class":486},[375,1691,437],{"class":385},[375,1693,992],{"class":486},[375,1695,437],{"class":385},[375,1697,1698],{"class":486}," productSort",[375,1700,1027],{"class":385},[375,1702,1703,1705,1707,1709,1711],{"class":377,"line":529},[375,1704,1488],{"class":381},[375,1706,612],{"class":486},[375,1708,1493],{"class":385},[375,1710,1496],{"class":385},[375,1712,883],{"class":385},[375,1714,1715,1717,1719,1721,1723,1725,1727,1730,1732],{"class":377,"line":905},[375,1716,1503],{"class":381},[375,1718,564],{"class":389},[375,1720,567],{"class":385},[375,1722,1222],{"class":389},[375,1724,816],{"class":385},[375,1726,1660],{"class":389},[375,1728,1729],{"class":385},"]{},",[375,1731,612],{"class":486},[375,1733,1734],{"class":525},"\u002F\u002F ErrInvalidCursor → HTTP 400\n",[375,1736,1737],{"class":377,"line":914},[375,1738,1541],{"class":385},[375,1740,1741,1743,1746,1748,1751,1753,1756,1758,1760,1762,1764],{"class":377,"line":922},[375,1742,1546],{"class":381},[375,1744,1745],{"class":486}," s",[375,1747,567],{"class":385},[375,1749,1750],{"class":486},"products",[375,1752,567],{"class":385},[375,1754,1755],{"class":426},"List",[375,1757,430],{"class":385},[375,1759,632],{"class":486},[375,1761,437],{"class":385},[375,1763,652],{"class":486},[375,1765,1027],{"class":385},[375,1767,1768],{"class":377,"line":1170},[375,1769,925],{"class":385},[366,1771,1774],{"className":368,"code":1772,"filename":1773,"language":370,"meta":371,"style":371},"func (r *Products) List(ctx context.Context, p paginate.CursorParams) (paginate.CursorPage[Product], error) {\n    query := `SELECT id, name, price_cents, created_at FROM products`\n    args := []any{p.FetchLimit()}\n\n    if p.HasCursor() {\n        var createdAt time.Time\n        var id string\n        if err := p.DecodeKey(&createdAt, &id); err != nil {\n            return paginate.CursorPage[Product]{}, err\n        }\n        query += ` WHERE (created_at, id) \u003C ($2, $3)`\n        args = append(args, createdAt, id)\n    }\n    query += ` ORDER BY created_at DESC, id DESC LIMIT $1`\n\n    rows, err := r.db.Query(ctx, query, args...)\n    if err != nil {\n        return paginate.CursorPage[Product]{}, errs.Wrap(err, \"products: list\")\n    }\n    products, err := pgx.CollectRows(rows, pgx.RowToStructByName[Product])\n    if err != nil {\n        return paginate.CursorPage[Product]{}, errs.Wrap(err, \"products: scan\")\n    }\n\n    return paginate.NewCursorPage(products, p, func(last Product) []any {\n        return []any{last.CreatedAt, last.ID}\n    })\n}\n","internal\u002Fmodules\u002Fshop\u002Frepository\u002Fproducts.go",[352,1775,1776,1837,1853,1877,1881,1896,1912,1922,1963,1983,1988,2003,2030,2034,2047,2051,2089,2101,2140,2145,2185,2198,2236,2241,2246,2283,2313,2319],{"__ignoreMap":371},[375,1777,1778,1780,1782,1785,1787,1790,1792,1795,1797,1799,1801,1803,1805,1807,1809,1811,1813,1815,1817,1819,1821,1823,1825,1827,1829,1831,1833,1835],{"class":377,"line":378},[375,1779,423],{"class":385},[375,1781,505],{"class":385},[375,1783,1784],{"class":433},"r ",[375,1786,1376],{"class":385},[375,1788,1789],{"class":389},"Products",[375,1791,461],{"class":385},[375,1793,1794],{"class":426}," List",[375,1796,430],{"class":385},[375,1798,632],{"class":433},[375,1800,1391],{"class":389},[375,1802,567],{"class":385},[375,1804,1396],{"class":389},[375,1806,437],{"class":385},[375,1808,652],{"class":433},[375,1810,564],{"class":389},[375,1812,567],{"class":385},[375,1814,1019],{"class":389},[375,1816,461],{"class":385},[375,1818,505],{"class":385},[375,1820,354],{"class":389},[375,1822,567],{"class":385},[375,1824,1222],{"class":389},[375,1826,816],{"class":385},[375,1828,1660],{"class":389},[375,1830,1229],{"class":385},[375,1832,1024],{"class":446},[375,1834,461],{"class":385},[375,1836,883],{"class":385},[375,1838,1839,1842,1844,1847,1850],{"class":377,"line":467},[375,1840,1841],{"class":486},"    query ",[375,1843,561],{"class":385},[375,1845,1846],{"class":385}," `",[375,1848,1849],{"class":643},"SELECT id, name, price_cents, created_at FROM products",[375,1851,1852],{"class":385},"`\n",[375,1854,1855,1858,1860,1862,1864,1867,1869,1871,1874],{"class":377,"line":474},[375,1856,1857],{"class":486},"    args ",[375,1859,561],{"class":385},[375,1861,831],{"class":385},[375,1863,1098],{"class":389},[375,1865,1866],{"class":385},"{",[375,1868,348],{"class":486},[375,1870,567],{"class":385},[375,1872,1873],{"class":426},"FetchLimit",[375,1875,1876],{"class":385},"()}\n",[375,1878,1879],{"class":377,"line":500},[375,1880,471],{"emptyLinePlaceholder":470},[375,1882,1883,1885,1887,1889,1892,1894],{"class":377,"line":529},[375,1884,1488],{"class":381},[375,1886,652],{"class":486},[375,1888,567],{"class":385},[375,1890,1891],{"class":426},"HasCursor",[375,1893,519],{"class":385},[375,1895,883],{"class":385},[375,1897,1898,1901,1904,1907,1909],{"class":377,"line":905},[375,1899,1900],{"class":385},"        var",[375,1902,1903],{"class":486}," createdAt ",[375,1905,1906],{"class":389},"time",[375,1908,567],{"class":385},[375,1910,1911],{"class":389},"Time\n",[375,1913,1914,1916,1919],{"class":377,"line":914},[375,1915,1900],{"class":385},[375,1917,1918],{"class":486}," id ",[375,1920,1921],{"class":446},"string\n",[375,1923,1924,1927,1929,1931,1933,1935,1938,1941,1944,1946,1949,1952,1955,1957,1959,1961],{"class":377,"line":922},[375,1925,1926],{"class":381},"        if",[375,1928,612],{"class":486},[375,1930,561],{"class":385},[375,1932,652],{"class":486},[375,1934,567],{"class":385},[375,1936,1937],{"class":426},"DecodeKey",[375,1939,1940],{"class":385},"(&",[375,1942,1943],{"class":486},"createdAt",[375,1945,437],{"class":385},[375,1947,1948],{"class":385}," &",[375,1950,1951],{"class":486},"id",[375,1953,1954],{"class":385},");",[375,1956,612],{"class":486},[375,1958,1493],{"class":385},[375,1960,1496],{"class":385},[375,1962,883],{"class":385},[375,1964,1965,1968,1970,1972,1974,1976,1978,1980],{"class":377,"line":1170},[375,1966,1967],{"class":381},"            return",[375,1969,564],{"class":389},[375,1971,567],{"class":385},[375,1973,1222],{"class":389},[375,1975,816],{"class":385},[375,1977,1660],{"class":389},[375,1979,1729],{"class":385},[375,1981,1982],{"class":486}," err\n",[375,1984,1985],{"class":377,"line":1236},[375,1986,1987],{"class":385},"        }\n",[375,1989,1990,1993,1996,1998,2001],{"class":377,"line":1241},[375,1991,1992],{"class":486},"        query ",[375,1994,1995],{"class":385},"+=",[375,1997,1846],{"class":385},[375,1999,2000],{"class":643}," WHERE (created_at, id) \u003C ($2, $3)",[375,2002,1852],{"class":385},[375,2004,2005,2008,2010,2013,2015,2018,2020,2023,2025,2028],{"class":377,"line":1261},[375,2006,2007],{"class":486},"        args ",[375,2009,1586],{"class":385},[375,2011,2012],{"class":426}," append",[375,2014,430],{"class":385},[375,2016,2017],{"class":486},"args",[375,2019,437],{"class":385},[375,2021,2022],{"class":486}," createdAt",[375,2024,437],{"class":385},[375,2026,2027],{"class":486}," id",[375,2029,1027],{"class":385},[375,2031,2032],{"class":377,"line":1271},[375,2033,1541],{"class":385},[375,2035,2036,2038,2040,2042,2045],{"class":377,"line":1282},[375,2037,1841],{"class":486},[375,2039,1995],{"class":385},[375,2041,1846],{"class":385},[375,2043,2044],{"class":643}," ORDER BY created_at DESC, id DESC LIMIT $1",[375,2046,1852],{"class":385},[375,2048,2049],{"class":377,"line":1291},[375,2050,471],{"emptyLinePlaceholder":470},[375,2052,2053,2056,2058,2060,2062,2064,2066,2068,2070,2072,2074,2076,2078,2081,2083,2086],{"class":377,"line":1296},[375,2054,2055],{"class":486},"    rows",[375,2057,437],{"class":385},[375,2059,612],{"class":486},[375,2061,561],{"class":385},[375,2063,617],{"class":486},[375,2065,567],{"class":385},[375,2067,622],{"class":486},[375,2069,567],{"class":385},[375,2071,627],{"class":426},[375,2073,430],{"class":385},[375,2075,632],{"class":486},[375,2077,437],{"class":385},[375,2079,2080],{"class":486}," query",[375,2082,437],{"class":385},[375,2084,2085],{"class":486}," args",[375,2087,2088],{"class":385},"...)\n",[375,2090,2091,2093,2095,2097,2099],{"class":377,"line":1301},[375,2092,1488],{"class":381},[375,2094,612],{"class":486},[375,2096,1493],{"class":385},[375,2098,1496],{"class":385},[375,2100,883],{"class":385},[375,2102,2104,2106,2108,2110,2112,2114,2116,2118,2121,2123,2125,2127,2129,2131,2133,2136,2138],{"class":377,"line":2103},18,[375,2105,1503],{"class":381},[375,2107,564],{"class":389},[375,2109,567],{"class":385},[375,2111,1222],{"class":389},[375,2113,816],{"class":385},[375,2115,1660],{"class":389},[375,2117,1729],{"class":385},[375,2119,2120],{"class":486}," errs",[375,2122,567],{"class":385},[375,2124,1519],{"class":426},[375,2126,430],{"class":385},[375,2128,1524],{"class":486},[375,2130,437],{"class":385},[375,2132,386],{"class":385},[375,2134,2135],{"class":643},"products: list",[375,2137,1534],{"class":385},[375,2139,1027],{"class":385},[375,2141,2143],{"class":377,"line":2142},19,[375,2144,1541],{"class":385},[375,2146,2148,2151,2153,2155,2157,2160,2162,2165,2167,2169,2171,2173,2175,2178,2180,2182],{"class":377,"line":2147},20,[375,2149,2150],{"class":486},"    products",[375,2152,437],{"class":385},[375,2154,612],{"class":486},[375,2156,561],{"class":385},[375,2158,2159],{"class":486}," pgx",[375,2161,567],{"class":385},[375,2163,2164],{"class":426},"CollectRows",[375,2166,430],{"class":385},[375,2168,607],{"class":486},[375,2170,437],{"class":385},[375,2172,2159],{"class":486},[375,2174,567],{"class":385},[375,2176,2177],{"class":486},"RowToStructByName",[375,2179,816],{"class":385},[375,2181,1660],{"class":486},[375,2183,2184],{"class":385},"])\n",[375,2186,2188,2190,2192,2194,2196],{"class":377,"line":2187},21,[375,2189,1488],{"class":381},[375,2191,612],{"class":486},[375,2193,1493],{"class":385},[375,2195,1496],{"class":385},[375,2197,883],{"class":385},[375,2199,2201,2203,2205,2207,2209,2211,2213,2215,2217,2219,2221,2223,2225,2227,2229,2232,2234],{"class":377,"line":2200},22,[375,2202,1503],{"class":381},[375,2204,564],{"class":389},[375,2206,567],{"class":385},[375,2208,1222],{"class":389},[375,2210,816],{"class":385},[375,2212,1660],{"class":389},[375,2214,1729],{"class":385},[375,2216,2120],{"class":486},[375,2218,567],{"class":385},[375,2220,1519],{"class":426},[375,2222,430],{"class":385},[375,2224,1524],{"class":486},[375,2226,437],{"class":385},[375,2228,386],{"class":385},[375,2230,2231],{"class":643},"products: scan",[375,2233,1534],{"class":385},[375,2235,1027],{"class":385},[375,2237,2239],{"class":377,"line":2238},23,[375,2240,1541],{"class":385},[375,2242,2244],{"class":377,"line":2243},24,[375,2245,471],{"emptyLinePlaceholder":470},[375,2247,2249,2251,2253,2255,2258,2260,2262,2264,2266,2268,2270,2272,2275,2277,2279,2281],{"class":377,"line":2248},25,[375,2250,1546],{"class":381},[375,2252,564],{"class":486},[375,2254,567],{"class":385},[375,2256,2257],{"class":426},"NewCursorPage",[375,2259,430],{"class":385},[375,2261,1750],{"class":486},[375,2263,437],{"class":385},[375,2265,652],{"class":486},[375,2267,437],{"class":385},[375,2269,1203],{"class":385},[375,2271,1206],{"class":433},[375,2273,2274],{"class":389}," Product",[375,2276,461],{"class":385},[375,2278,831],{"class":385},[375,2280,1098],{"class":389},[375,2282,883],{"class":385},[375,2284,2286,2288,2290,2292,2294,2296,2298,2301,2303,2306,2308,2311],{"class":377,"line":2285},26,[375,2287,1503],{"class":381},[375,2289,831],{"class":385},[375,2291,1098],{"class":389},[375,2293,1866],{"class":385},[375,2295,1206],{"class":486},[375,2297,567],{"class":385},[375,2299,2300],{"class":486},"CreatedAt",[375,2302,437],{"class":385},[375,2304,2305],{"class":486}," last",[375,2307,567],{"class":385},[375,2309,2310],{"class":486},"ID",[375,2312,925],{"class":385},[375,2314,2316],{"class":377,"line":2315},27,[375,2317,2318],{"class":385},"    })\n",[375,2320,2322],{"class":377,"line":2321},28,[375,2323,925],{"class":385},[348,2325,2326,2327,2330,2331,2334,2335,2338,2339,2341,2342,2344,2345,2348,2349,2352,2353,2356,2357,2360,2361,2364,2365,2367],{},"The key to the mechanics is the ",[357,2328,2329],{},"+1 row",": the query requests ",[352,2332,2333],{},"FetchLimit()"," (= ",[352,2336,2337],{},"Limit+1","), and ",[352,2340,2257],{}," cuts the page down to ",[352,2343,657],{}," items. If the extra row was present, ",[352,2346,2347],{},"HasMore = true",", and ",[352,2350,2351],{},"NextCursor"," is built from the key of the ",[357,2354,2355],{},"last actually returned"," item: no separate ",[352,2358,2359],{},"COUNT"," query. The ",[352,2362,2363],{},"key"," function must return exactly the values, in exactly the order, used by the ",[352,2366,1342],{}," \u002F seek predicate.",[395,2369,2371],{"id":2370},"requirements-of-the-seek-predicate","Requirements of the seek predicate",[348,2373,2374,2375,2378],{},"The Postgres row-value comparison (",[352,2376,2377],{},"(created_at, id) \u003C ($2, $3)",") is only correct and fast when:",[2380,2381,2382,2396,2412,2422],"ul",{},[2383,2384,2385,2388,2389,1323,2392,2395],"li",{},[357,2386,2387],{},"the sort direction is uniform"," across all key columns: mixed ",[352,2390,2391],{},"ASC",[352,2393,2394],{},"DESC"," cannot be expressed with a tuple comparison;",[2383,2397,2398,2404,2405,2408,2409,2411],{},[357,2399,2400,2401],{},"every key column is ",[352,2402,2403],{},"NOT NULL",": a ",[352,2406,2407],{},"NULL"," tuple element makes the whole comparison ",[352,2410,2407],{},", silently dropping the row from every page;",[2383,2413,2414,2417,2418,2421],{},[357,2415,2416],{},"the last key is a unique tiebreaker",": typically the primary key, so a non-unique leading column (e.g. ",[352,2419,2420],{},"created_at",") cannot cause skipped or duplicated rows at page boundaries;",[2383,2423,2424,2429,2430,2433],{},[357,2425,2426,2427],{},"a composite index matching the ",[352,2428,1342],{}," exists (in the shop: ",[352,2431,2432],{},"CREATE INDEX ON products (created_at DESC, id DESC)","), so the seek stays an index scan.",[395,2435,2437],{"id":2436},"the-opaque-cursor","The opaque cursor",[348,2439,2440,2441,2443,2444,2447,2448,2451,2452,2455],{},"Towards the client the cursor is an opaque string: a base64url-encoded, versioned JSON envelope carrying the key values and a fingerprint of the sort spec. ",[352,2442,1315],{}," rejects with ",[352,2445,2446],{},"ErrInvalidCursor"," (HTTP 400, code ",[352,2449,2450],{},"paginate_invalid_cursor",") any cursor that fails to decode, was made for a different version, or ",[357,2453,2454],{},"was issued for a different sort order",". It never silently falls back to the first page, because that would hide the client's bug. Two consequences:",[2380,2457,2458,2461],{},[2383,2459,2460],{},"changing an endpoint's default sort invalidates outstanding cursors, by design;",[2383,2462,2463],{},"the cursor is not encrypted: its key values (timestamp, id) are decodable. Don't put anything in it the client must not see.",[2465,2466,2467,2468,2471,2472,2475,2476,934,2478,2481],"note",{},"Cursor pagination in this version only moves forward: there is no ",[352,2469,2470],{},"before"," parameter for paging backwards. ",[352,2473,2474],{},"CursorPage[T]"," mirrors the ",[352,2477,933],{},[352,2479,2480],{},"CursorPage\u003CT>"," model.",[395,2483,2485],{"id":2484},"which-one-when","Which one, when",[2487,2488,2489,2508],"table",{},[2490,2491,2492],"thead",{},[2493,2494,2495,2498,2503],"tr",{},[2496,2497],"th",{},[2496,2499,2500,2501,461],{},"Offset (",[352,2502,412],{},[2496,2504,2505,2506,461],{},"Cursor (",[352,2507,1315],{},[2509,2510,2511,2526,2537,2548,2559],"tbody",{},[2493,2512,2513,2517,2523],{},[2514,2515,2516],"td",{},"page-numbered UI (\"page 3 of 12\")",[2514,2518,2519,2520,461],{},"✔ (provides ",[352,2521,2522],{},"Total",[2514,2524,2525],{},"✘ (no page numbers, no total count)",[2493,2527,2528,2531,2534],{},[2514,2529,2530],{},"infinite scroll \u002F \"load more\" button",[2514,2532,2533],{},"works, but drift-prone",[2514,2535,2536],{},"✔ (its native case)",[2493,2538,2539,2542,2545],{},[2514,2540,2541],{},"large table, deep paging",[2514,2543,2544],{},"O(n), degrades",[2514,2546,2547],{},"✔ (near-constant)",[2493,2549,2550,2553,2556],{},[2514,2551,2552],{},"stable pages under concurrent inserts",[2514,2554,2555],{},"✘ (drifts)",[2514,2557,2558],{},"✔",[2493,2560,2561,2564,2566],{},[2514,2562,2563],{},"jumping to an arbitrary page",[2514,2565,2558],{},[2514,2567,2568],{},"✘ (sequential only)",[348,2570,2571],{},"Rule of thumb from the shop: the public product listing (large, growing, infinite scroll) uses cursors; an admin-side list of a few hundred rows is more comfortable with page numbers on offset.",[395,2573,2575],{"id":2574},"the-sql-is-yours","The SQL is yours",[348,2577,2578,2579,2581,2582,2585,2586,2589,2590,2585,2593,2596],{},"An important property: ",[352,2580,354],{}," doesn't hide pagination behind a query builder: the SQL is yours, and the package only provides the parameter discipline and the page mechanics (",[352,2583,2584],{},"paginate.Normalize"," \u002F ",[352,2587,2588],{},"paginate.NormalizeCursor"," for input, ",[352,2591,2592],{},"paginate.NewPage",[352,2594,2595],{},"paginate.NewCursorPage"," for the response). In exchange, the seek predicate and the index are in your hands, which is exactly where it matters once you've reached for cursor pagination at all.",[395,2598,2600],{"id":2599},"patterns-used","Patterns used",[2380,2602,2603,2612],{},[2383,2604,2605,2608,2609,567],{},[357,2606,2607],{},"Opaque, tamper-resistant cursor"," (base64url + sha256 sort fingerprint): ",[402,2610,92],{"href":2611},"\u002Fen\u002Fconcepts\u002Fdesign-patterns#opaque-tamper-resistant-cursor",[2383,2613,2614,505,2617,791,2619,2621,2622,2625,2626,2629],{},[357,2615,2616],{},"Generic result wrappers",[352,2618,800],{},[352,2620,2474],{},"): ",[402,2623,92],{"href":2624},"\u002Fen\u002Fconcepts\u002Fdesign-patterns#functional-options"," (the ",[352,2627,2628],{},"With*"," options for page bounds).",[2631,2632,2633],"style",{},"html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"title":371,"searchDepth":467,"depth":467,"links":2635},[2636,2637,2638,2639,2640,2641,2642,2643],{"id":397,"depth":467,"text":398},{"id":941,"depth":467,"text":942},{"id":1346,"depth":467,"text":1347},{"id":2370,"depth":467,"text":2371},{"id":2436,"depth":467,"text":2437},{"id":2484,"depth":467,"text":2485},{"id":2574,"depth":467,"text":2575},{"id":2599,"depth":467,"text":2600},"Offset and cursor (keyset) pagination with the paginate package, normalized parameters, typed page wrappers, opaque cursors.","md",null,{},{"icon":131},{"title":128,"description":2644},"C0uOMn5vE4oqE43QausDHTK4IdMP7l8Wb4wepED_HRg",[2652,2654],{"title":123,"path":124,"stem":125,"description":2653,"icon":126,"children":-1},"Struct-tag-based request validation with go-playground\u002Fvalidator, at bind time on Fiber, as strict middleware on chi, with 422 problem responses.",{"title":139,"path":140,"stem":141,"description":2655,"icon":134,"children":-1},"The dbx package, the database-agnostic contract your code depends on, and the two implementations you choose by import.",1784668707725]