[{"data":1,"prerenderedAt":5311},["ShallowReactive",2],{"content:\u002F10-databases\u002F07-go-postgres-pgx-sqlc-squirrel":3},{"title":4,"description":5,"path":6,"body":7},"Go и PostgreSQL: pgx, sqlc и Squirrel","В Go доступ к PostgreSQL чаще строят вокруг явного SQL:","\u002F10-databases\u002F07-go-postgres-pgx-sqlc-squirrel",{"type":8,"value":9,"toc":5294},"minimark",[10,14,17,48,51,61,64,69,174,183,185,189,192,672,682,699,706,924,991,1050,1056,1070,1080,1082,1086,1099,1409,1412,1457,1473,1607,1609,1613,1616,1990,2001,2057,2064,2159,2261,2268,2270,2274,2328,2508,2682,2684,2688,2691,2717,2838,2858,2869,2871,2873,2878,2884,2887,2893,3014,3112,3115,3279,3288,3661,3674,3734,3750,3752,3756,3759,3964,3977,3985,4117,4120,4149,4151,4155,4242,4245,4334,4337,4343,4409,4412,4418,4421,4486,4519,4524,4573,4575,4579,4582,4588,4591,4597,4600,4622,4625,4628,4631,4666,4668,4672,4719,4721,4725,4785,4787,4791,4830,4992,5159,5161,5165,5217,5219,5223,5290],[11,12,4],"h1",{"id":13},"go-и-postgresql-pgx-sqlc-и-squirrel",[15,16,5],"p",{},[18,19,20,28,39,45],"ul",{},[21,22,23,27],"li",{},[24,25,26],"code",{},"database\u002Fsql"," плюс драйвер;",[21,29,30,31,34,35,38],{},"нативный ",[24,32,33],{},"pgx","\u002F",[24,36,37],{},"pgxpool",";",[21,40,41,44],{},[24,42,43],{},"sqlc"," для генерации типобезопасного кода из SQL;",[21,46,47],{},"Squirrel или другой builder для редких динамических запросов.",[15,49,50],{},"ORM тоже бывают полезны, но в сервисах с важными транзакциями, блокировками, индексами и понятным latency budget явный SQL обычно проще проверять и оптимизировать.",[52,53,59],"pre",{"className":54,"code":56,"language":57,"meta":58},[55],"language-text","HTTP handler \u002F worker\n        |\n        v\nUse case \u002F service\n        |\n        v\nRepository \u002F Store \u002F Unit of Work\n        |\n        v\npgxpool \u002F sqlc \u002F Squirrel\n        |\n        v\nPostgreSQL\n","text","",[24,60,56],{"__ignoreMap":58},[62,63],"hr",{},[65,66,68],"h2",{"id":67},"карта-выбора","Карта выбора",[70,71,72,89],"table",{},[73,74,75],"thead",{},[76,77,78,83,86],"tr",{},[79,80,82],"th",{"align":81},"left","Задача",[79,84,85],{"align":81},"Инструмент",[79,87,88],{"align":81},"Почему",[90,91,92,105,128,144,163],"tbody",{},[76,93,94,98,102],{},[95,96,97],"td",{"align":81},"Простые учебные примеры, portable SQL",[95,99,100],{"align":81},[24,101,26],{},[95,103,104],{"align":81},"Стандартная библиотека, знакомый API.",[76,106,107,110,114],{},[95,108,109],{"align":81},"Production-сервис только на PostgreSQL",[95,111,112],{"align":81},[24,113,37],{},[95,115,116,117,120,121,120,124,127],{"align":81},"Нативные типы, ",[24,118,119],{},"CopyFrom",", ",[24,122,123],{},"Batch",[24,125,126],{},"PgError",", tracing hooks.",[76,129,130,133,141],{},[95,131,132],{"align":81},"Много стабильных SQL-запросов",[95,134,135,137,138],{"align":81},[24,136,43],{}," + ",[24,139,140],{},"pgx\u002Fv5",[95,142,143],{"align":81},"SQL остаётся явным, Go-типы генерируются.",[76,145,146,149,152],{},[95,147,148],{"align":81},"Динамические фильтры поиска",[95,150,151],{"align":81},"Squirrel точечно",[95,153,154,155,158,159,162],{"align":81},"Удобно собирать ",[24,156,157],{},"WHERE"," и ",[24,160,161],{},"IN"," из условий.",[76,164,165,168,171],{},[95,166,167],{"align":81},"Lazy loading, identity map, сложный graph",[95,169,170],{"align":81},"ORM осторожно",[95,172,173],{"align":81},"Может ускорить CRUD, но скрывает SQL и round-trips.",[15,175,176,158,179,182],{},[24,177,178],{},"*sql.DB",[24,180,181],{},"*pgxpool.Pool"," - это не одно соединение, а пул. Создавайте пул один раз на приложение и переиспользуйте.",[62,184],{},[65,186,188],{"id":187},"pgxpool-подключение-и-context","pgxpool: подключение и context",[15,190,191],{},"Минимальный production-friendly конструктор:",[52,193,198],{"className":194,"code":195,"language":196,"meta":197,"style":58},"language-go shiki shiki-themes github-dark","package postgres\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"time\"\n\n    \"github.com\u002Fjackc\u002Fpgx\u002Fv5\u002Fpgxpool\"\n)\n\nfunc NewPool(ctx context.Context, dsn string) (*pgxpool.Pool, error) {\n    cfg, err := pgxpool.ParseConfig(dsn)\n    if err != nil {\n        return nil, fmt.Errorf(\"parse postgres config: %w\", err)\n    }\n\n    cfg.MaxConns = 20 \u002F\u002F validate from env\u002Fconfig, do not copy blindly\n    cfg.MinConns = 2\n    cfg.MaxConnLifetime = time.Hour\n    cfg.MaxConnIdleTime = 15 * time.Minute\n    cfg.HealthCheckPeriod = time.Minute\n\n    pool, err := pgxpool.NewWithConfig(ctx, cfg)\n    if err != nil {\n        return nil, fmt.Errorf(\"create postgres pool: %w\", err)\n    }\n\n    pingCtx, cancel := context.WithTimeout(ctx, 2*time.Second)\n    defer cancel()\n\n    if err := pool.Ping(pingCtx); err != nil {\n        pool.Close()\n        return nil, fmt.Errorf(\"ping postgres: %w\", err)\n    }\n    return pool, nil\n}\n","go","no-run",[24,199,200,213,220,230,243,253,263,268,278,284,289,343,361,380,408,414,419,435,446,457,474,484,489,505,518,540,545,550,575,587,592,616,627,649,654,666],{"__ignoreMap":58},[201,202,205,209],"span",{"class":203,"line":204},"line",1,[201,206,208],{"class":207},"snl16","package",[201,210,212],{"class":211},"svObZ"," postgres\n",[201,214,216],{"class":203,"line":215},2,[201,217,219],{"emptyLinePlaceholder":218},true,"\n",[201,221,223,226],{"class":203,"line":222},3,[201,224,225],{"class":207},"import",[201,227,229],{"class":228},"s95oV"," (\n",[201,231,233,237,240],{"class":203,"line":232},4,[201,234,236],{"class":235},"sU2Wk","    \"",[201,238,239],{"class":211},"context",[201,241,242],{"class":235},"\"\n",[201,244,246,248,251],{"class":203,"line":245},5,[201,247,236],{"class":235},[201,249,250],{"class":211},"fmt",[201,252,242],{"class":235},[201,254,256,258,261],{"class":203,"line":255},6,[201,257,236],{"class":235},[201,259,260],{"class":211},"time",[201,262,242],{"class":235},[201,264,266],{"class":203,"line":265},7,[201,267,219],{"emptyLinePlaceholder":218},[201,269,271,273,276],{"class":203,"line":270},8,[201,272,236],{"class":235},[201,274,275],{"class":211},"github.com\u002Fjackc\u002Fpgx\u002Fv5\u002Fpgxpool",[201,277,242],{"class":235},[201,279,281],{"class":203,"line":280},9,[201,282,283],{"class":228},")\n",[201,285,287],{"class":203,"line":286},10,[201,288,219],{"emptyLinePlaceholder":218},[201,290,292,295,298,301,305,308,311,314,316,319,322,325,328,330,332,335,337,340],{"class":203,"line":291},11,[201,293,294],{"class":207},"func",[201,296,297],{"class":211}," NewPool",[201,299,300],{"class":228},"(",[201,302,304],{"class":303},"s9osk","ctx",[201,306,307],{"class":211}," context",[201,309,310],{"class":228},".",[201,312,313],{"class":211},"Context",[201,315,120],{"class":228},[201,317,318],{"class":303},"dsn",[201,320,321],{"class":207}," string",[201,323,324],{"class":228},") (",[201,326,327],{"class":207},"*",[201,329,37],{"class":211},[201,331,310],{"class":228},[201,333,334],{"class":211},"Pool",[201,336,120],{"class":228},[201,338,339],{"class":207},"error",[201,341,342],{"class":228},") {\n",[201,344,346,349,352,355,358],{"class":203,"line":345},12,[201,347,348],{"class":228},"    cfg, err ",[201,350,351],{"class":207},":=",[201,353,354],{"class":228}," pgxpool.",[201,356,357],{"class":211},"ParseConfig",[201,359,360],{"class":228},"(dsn)\n",[201,362,364,367,370,373,377],{"class":203,"line":363},13,[201,365,366],{"class":207},"    if",[201,368,369],{"class":228}," err ",[201,371,372],{"class":207},"!=",[201,374,376],{"class":375},"sDLfK"," nil",[201,378,379],{"class":228}," {\n",[201,381,383,386,388,391,394,396,399,402,405],{"class":203,"line":382},14,[201,384,385],{"class":207},"        return",[201,387,376],{"class":375},[201,389,390],{"class":228},", fmt.",[201,392,393],{"class":211},"Errorf",[201,395,300],{"class":228},[201,397,398],{"class":235},"\"parse postgres config: ",[201,400,401],{"class":375},"%w",[201,403,404],{"class":235},"\"",[201,406,407],{"class":228},", err)\n",[201,409,411],{"class":203,"line":410},15,[201,412,413],{"class":228},"    }\n",[201,415,417],{"class":203,"line":416},16,[201,418,219],{"emptyLinePlaceholder":218},[201,420,422,425,428,431],{"class":203,"line":421},17,[201,423,424],{"class":228},"    cfg.MaxConns ",[201,426,427],{"class":207},"=",[201,429,430],{"class":375}," 20",[201,432,434],{"class":433},"sAwPA"," \u002F\u002F validate from env\u002Fconfig, do not copy blindly\n",[201,436,438,441,443],{"class":203,"line":437},18,[201,439,440],{"class":228},"    cfg.MinConns ",[201,442,427],{"class":207},[201,444,445],{"class":375}," 2\n",[201,447,449,452,454],{"class":203,"line":448},19,[201,450,451],{"class":228},"    cfg.MaxConnLifetime ",[201,453,427],{"class":207},[201,455,456],{"class":228}," time.Hour\n",[201,458,460,463,465,468,471],{"class":203,"line":459},20,[201,461,462],{"class":228},"    cfg.MaxConnIdleTime ",[201,464,427],{"class":207},[201,466,467],{"class":375}," 15",[201,469,470],{"class":207}," *",[201,472,473],{"class":228}," time.Minute\n",[201,475,477,480,482],{"class":203,"line":476},21,[201,478,479],{"class":228},"    cfg.HealthCheckPeriod ",[201,481,427],{"class":207},[201,483,473],{"class":228},[201,485,487],{"class":203,"line":486},22,[201,488,219],{"emptyLinePlaceholder":218},[201,490,492,495,497,499,502],{"class":203,"line":491},23,[201,493,494],{"class":228},"    pool, err ",[201,496,351],{"class":207},[201,498,354],{"class":228},[201,500,501],{"class":211},"NewWithConfig",[201,503,504],{"class":228},"(ctx, cfg)\n",[201,506,508,510,512,514,516],{"class":203,"line":507},24,[201,509,366],{"class":207},[201,511,369],{"class":228},[201,513,372],{"class":207},[201,515,376],{"class":375},[201,517,379],{"class":228},[201,519,521,523,525,527,529,531,534,536,538],{"class":203,"line":520},25,[201,522,385],{"class":207},[201,524,376],{"class":375},[201,526,390],{"class":228},[201,528,393],{"class":211},[201,530,300],{"class":228},[201,532,533],{"class":235},"\"create postgres pool: ",[201,535,401],{"class":375},[201,537,404],{"class":235},[201,539,407],{"class":228},[201,541,543],{"class":203,"line":542},26,[201,544,413],{"class":228},[201,546,548],{"class":203,"line":547},27,[201,549,219],{"emptyLinePlaceholder":218},[201,551,553,556,558,561,564,567,570,572],{"class":203,"line":552},28,[201,554,555],{"class":228},"    pingCtx, cancel ",[201,557,351],{"class":207},[201,559,560],{"class":228}," context.",[201,562,563],{"class":211},"WithTimeout",[201,565,566],{"class":228},"(ctx, ",[201,568,569],{"class":375},"2",[201,571,327],{"class":207},[201,573,574],{"class":228},"time.Second)\n",[201,576,578,581,584],{"class":203,"line":577},29,[201,579,580],{"class":207},"    defer",[201,582,583],{"class":211}," cancel",[201,585,586],{"class":228},"()\n",[201,588,590],{"class":203,"line":589},30,[201,591,219],{"emptyLinePlaceholder":218},[201,593,595,597,599,601,604,607,610,612,614],{"class":203,"line":594},31,[201,596,366],{"class":207},[201,598,369],{"class":228},[201,600,351],{"class":207},[201,602,603],{"class":228}," pool.",[201,605,606],{"class":211},"Ping",[201,608,609],{"class":228},"(pingCtx); err ",[201,611,372],{"class":207},[201,613,376],{"class":375},[201,615,379],{"class":228},[201,617,619,622,625],{"class":203,"line":618},32,[201,620,621],{"class":228},"        pool.",[201,623,624],{"class":211},"Close",[201,626,586],{"class":228},[201,628,630,632,634,636,638,640,643,645,647],{"class":203,"line":629},33,[201,631,385],{"class":207},[201,633,376],{"class":375},[201,635,390],{"class":228},[201,637,393],{"class":211},[201,639,300],{"class":228},[201,641,642],{"class":235},"\"ping postgres: ",[201,644,401],{"class":375},[201,646,404],{"class":235},[201,648,407],{"class":228},[201,650,652],{"class":203,"line":651},34,[201,653,413],{"class":228},[201,655,657,660,663],{"class":203,"line":656},35,[201,658,659],{"class":207},"    return",[201,661,662],{"class":228}," pool, ",[201,664,665],{"class":375},"nil\n",[201,667,669],{"class":203,"line":668},36,[201,670,671],{"class":228},"}\n",[15,673,674,675,677,678,681],{},"Почему сразу делаем ",[24,676,606],{},": ",[24,679,680],{},"pgxpool.NewWithConfig"," создаёт пул быстро и не обязан открыть все соединения. Приложение лучше уронить на старте, если БД недоступна.",[15,683,684,687,688,120,691,694,695,698],{},[24,685,686],{},"MaxConns"," - не \"сколько хочется одному pod\". Его считают на весь fleet: HTTP replicas, workers, cron jobs, migration jobs, PgBouncer\u002Fserver limits и reserved connections для on-call. Значение из env надо валидировать на старте: ",[24,689,690],{},"> 0",[24,692,693],{},"min \u003C= max",", timeout в разумном диапазоне, ",[24,696,697],{},"sslmode"," соответствует окружению, DSN не пустой и не логируется.",[15,700,701,702,705],{},"Каждая операция с БД принимает ",[24,703,704],{},"context.Context",":",[52,707,709],{"className":194,"code":708,"language":196,"meta":197,"style":58},"func (s *Store) GetUser(ctx context.Context, id int64) (User, error) {\n    ctx, cancel := context.WithTimeout(ctx, 300*time.Millisecond)\n    defer cancel()\n\n    var user User\n    err := s.pool.QueryRow(ctx, `\n        SELECT id, email, name, created_at\n        FROM users\n        WHERE id = $1\n    `, id).Scan(&user.ID, &user.Email, &user.Name, &user.CreatedAt)\n    if err != nil {\n        return User{}, fmt.Errorf(\"select user: %w\", err)\n    }\n    return user, nil\n}\n",[24,710,711,761,782,790,794,805,823,828,833,838,872,884,907,911,920],{"__ignoreMap":58},[201,712,713,715,718,721,723,726,729,732,734,736,738,740,742,744,747,750,752,755,757,759],{"class":203,"line":204},[201,714,294],{"class":207},[201,716,717],{"class":228}," (",[201,719,720],{"class":303},"s ",[201,722,327],{"class":207},[201,724,725],{"class":211},"Store",[201,727,728],{"class":228},") ",[201,730,731],{"class":211},"GetUser",[201,733,300],{"class":228},[201,735,304],{"class":303},[201,737,307],{"class":211},[201,739,310],{"class":228},[201,741,313],{"class":211},[201,743,120],{"class":228},[201,745,746],{"class":303},"id",[201,748,749],{"class":207}," int64",[201,751,324],{"class":228},[201,753,754],{"class":211},"User",[201,756,120],{"class":228},[201,758,339],{"class":207},[201,760,342],{"class":228},[201,762,763,766,768,770,772,774,777,779],{"class":203,"line":215},[201,764,765],{"class":228},"    ctx, cancel ",[201,767,351],{"class":207},[201,769,560],{"class":228},[201,771,563],{"class":211},[201,773,566],{"class":228},[201,775,776],{"class":375},"300",[201,778,327],{"class":207},[201,780,781],{"class":228},"time.Millisecond)\n",[201,783,784,786,788],{"class":203,"line":222},[201,785,580],{"class":207},[201,787,583],{"class":211},[201,789,586],{"class":228},[201,791,792],{"class":203,"line":232},[201,793,219],{"emptyLinePlaceholder":218},[201,795,796,799,802],{"class":203,"line":245},[201,797,798],{"class":207},"    var",[201,800,801],{"class":228}," user ",[201,803,804],{"class":211},"User\n",[201,806,807,810,812,815,818,820],{"class":203,"line":255},[201,808,809],{"class":228},"    err ",[201,811,351],{"class":207},[201,813,814],{"class":228}," s.pool.",[201,816,817],{"class":211},"QueryRow",[201,819,566],{"class":228},[201,821,822],{"class":235},"`\n",[201,824,825],{"class":203,"line":265},[201,826,827],{"class":235},"        SELECT id, email, name, created_at\n",[201,829,830],{"class":203,"line":270},[201,831,832],{"class":235},"        FROM users\n",[201,834,835],{"class":203,"line":280},[201,836,837],{"class":235},"        WHERE id = $1\n",[201,839,840,843,846,849,851,854,857,859,862,864,867,869],{"class":203,"line":286},[201,841,842],{"class":235},"    `",[201,844,845],{"class":228},", id).",[201,847,848],{"class":211},"Scan",[201,850,300],{"class":228},[201,852,853],{"class":207},"&",[201,855,856],{"class":228},"user.ID, ",[201,858,853],{"class":207},[201,860,861],{"class":228},"user.Email, ",[201,863,853],{"class":207},[201,865,866],{"class":228},"user.Name, ",[201,868,853],{"class":207},[201,870,871],{"class":228},"user.CreatedAt)\n",[201,873,874,876,878,880,882],{"class":203,"line":291},[201,875,366],{"class":207},[201,877,369],{"class":228},[201,879,372],{"class":207},[201,881,376],{"class":375},[201,883,379],{"class":228},[201,885,886,888,891,894,896,898,901,903,905],{"class":203,"line":345},[201,887,385],{"class":207},[201,889,890],{"class":211}," User",[201,892,893],{"class":228},"{}, fmt.",[201,895,393],{"class":211},[201,897,300],{"class":228},[201,899,900],{"class":235},"\"select user: ",[201,902,401],{"class":375},[201,904,404],{"class":235},[201,906,407],{"class":228},[201,908,909],{"class":203,"line":363},[201,910,413],{"class":228},[201,912,913,915,918],{"class":203,"line":382},[201,914,659],{"class":207},[201,916,917],{"class":228}," user, ",[201,919,665],{"class":375},[201,921,922],{"class":203,"line":410},[201,923,671],{"class":228},[70,925,926,939],{},[73,927,928],{},[76,929,930,933,936],{},[79,931,932],{"align":81},"Настройка",[79,934,935],{"align":81},"Где работает",[79,937,938],{"align":81},"Зачем",[90,940,941,954,967,979],{},[76,942,943,948,951],{},[95,944,945],{"align":81},[24,946,947],{},"context.WithTimeout",[95,949,950],{"align":81},"В Go-клиенте",[95,952,953],{"align":81},"Отменить ожидание со стороны приложения.",[76,955,956,961,964],{},[95,957,958],{"align":81},[24,959,960],{},"statement_timeout",[95,962,963],{"align":81},"В PostgreSQL",[95,965,966],{"align":81},"Ограничить выполнение запроса на сервере.",[76,968,969,974,976],{},[95,970,971],{"align":81},[24,972,973],{},"lock_timeout",[95,975,963],{"align":81},[95,977,978],{"align":81},"Не ждать lock слишком долго.",[76,980,981,986,988],{},[95,982,983],{"align":81},[24,984,985],{},"idle_in_transaction_session_timeout",[95,987,963],{"align":81},[95,989,990],{"align":81},"Убить забытые idle-транзакции.",[52,992,996],{"className":993,"code":994,"language":995,"meta":58,"style":58},"language-sql shiki shiki-themes github-dark","SET LOCAL statement_timeout = '500ms';\nSET LOCAL lock_timeout = '100ms';\nSET LOCAL idle_in_transaction_session_timeout = '5s';\n","sql",[24,997,998,1017,1034],{"__ignoreMap":58},[201,999,1000,1003,1006,1009,1011,1014],{"class":203,"line":204},[201,1001,1002],{"class":207},"SET",[201,1004,1005],{"class":207}," LOCAL",[201,1007,1008],{"class":228}," statement_timeout ",[201,1010,427],{"class":207},[201,1012,1013],{"class":235}," '500ms'",[201,1015,1016],{"class":228},";\n",[201,1018,1019,1021,1023,1026,1029,1032],{"class":203,"line":215},[201,1020,1002],{"class":207},[201,1022,1005],{"class":207},[201,1024,1025],{"class":207}," lock_timeout",[201,1027,1028],{"class":207}," =",[201,1030,1031],{"class":235}," '100ms'",[201,1033,1016],{"class":228},[201,1035,1036,1038,1040,1043,1045,1048],{"class":203,"line":222},[201,1037,1002],{"class":207},[201,1039,1005],{"class":207},[201,1041,1042],{"class":228}," idle_in_transaction_session_timeout ",[201,1044,427],{"class":207},[201,1046,1047],{"class":235}," '5s'",[201,1049,1016],{"class":228},[15,1051,1052,1053,310],{},"Context не заменяет transaction management: отмена context не делает за вас явный ",[24,1054,1055],{},"Rollback",[15,1057,1058,1059,1061,1062,1065,1066,1069],{},"Если ",[24,1060,239],{}," отменился во время statement внутри transaction, transaction часто уже нельзя безопасно продолжать: PostgreSQL может держать её в aborted state до ",[24,1063,1064],{},"ROLLBACK",". Не пытайтесь \"дописать остаток\" после ",[24,1067,1068],{},"context deadline exceeded","; завершите transaction, верните ошибку и дайте верхнему уровню решить retry\u002Fidempotency.",[15,1071,1072,158,1074,1076,1077,1079],{},[24,1073,960],{},[24,1075,947],{}," решают разные задачи. Context ограничивает ожидание клиента и освобождает goroutine, а ",[24,1078,960],{}," страхует сервер от слишком долгого выполнения даже если клиент завис или потерял связь. В production обычно есть оба: короткий request context, per-operation budget и server-side limits через session\u002Ftransaction settings.",[62,1081],{},[65,1083,1085],{"id":1084},"query-scan-и-nullable-типы","Query, Scan и nullable-типы",[15,1087,1088,1090,1091,1094,1095,1098],{},[24,1089,817],{}," используйте для одной строки, ",[24,1092,1093],{},"Query"," - для списка. С ",[24,1096,1097],{},"Rows"," обязательно закрывайте cursor и проверяйте ошибку итерации.",[52,1100,1102],{"className":194,"code":1101,"language":196,"meta":197,"style":58},"func (s *Store) ListCourses(ctx context.Context) ([]Course, error) {\n    rows, err := s.pool.Query(ctx, `\n        SELECT id, slug, title, description\n        FROM courses\n        ORDER BY id\n    `)\n    if err != nil {\n        return nil, fmt.Errorf(\"query courses: %w\", err)\n    }\n    defer rows.Close()\n\n    var courses []Course\n    for rows.Next() {\n        var c Course\n        if err := rows.Scan(&c.ID, &c.Slug, &c.Title, &c.Description); err != nil {\n            return nil, fmt.Errorf(\"scan course: %w\", err)\n        }\n        courses = append(courses, c)\n    }\n    if err := rows.Err(); err != nil {\n        return nil, fmt.Errorf(\"iterate courses: %w\", err)\n    }\n    return courses, nil\n}\n",[24,1103,1104,1143,1158,1163,1168,1173,1179,1191,1212,1216,1227,1231,1241,1254,1264,1305,1327,1332,1345,1349,1371,1392,1396,1405],{"__ignoreMap":58},[201,1105,1106,1108,1110,1112,1114,1116,1118,1121,1123,1125,1127,1129,1131,1134,1137,1139,1141],{"class":203,"line":204},[201,1107,294],{"class":207},[201,1109,717],{"class":228},[201,1111,720],{"class":303},[201,1113,327],{"class":207},[201,1115,725],{"class":211},[201,1117,728],{"class":228},[201,1119,1120],{"class":211},"ListCourses",[201,1122,300],{"class":228},[201,1124,304],{"class":303},[201,1126,307],{"class":211},[201,1128,310],{"class":228},[201,1130,313],{"class":211},[201,1132,1133],{"class":228},") ([]",[201,1135,1136],{"class":211},"Course",[201,1138,120],{"class":228},[201,1140,339],{"class":207},[201,1142,342],{"class":228},[201,1144,1145,1148,1150,1152,1154,1156],{"class":203,"line":215},[201,1146,1147],{"class":228},"    rows, err ",[201,1149,351],{"class":207},[201,1151,814],{"class":228},[201,1153,1093],{"class":211},[201,1155,566],{"class":228},[201,1157,822],{"class":235},[201,1159,1160],{"class":203,"line":222},[201,1161,1162],{"class":235},"        SELECT id, slug, title, description\n",[201,1164,1165],{"class":203,"line":232},[201,1166,1167],{"class":235},"        FROM courses\n",[201,1169,1170],{"class":203,"line":245},[201,1171,1172],{"class":235},"        ORDER BY id\n",[201,1174,1175,1177],{"class":203,"line":255},[201,1176,842],{"class":235},[201,1178,283],{"class":228},[201,1180,1181,1183,1185,1187,1189],{"class":203,"line":265},[201,1182,366],{"class":207},[201,1184,369],{"class":228},[201,1186,372],{"class":207},[201,1188,376],{"class":375},[201,1190,379],{"class":228},[201,1192,1193,1195,1197,1199,1201,1203,1206,1208,1210],{"class":203,"line":270},[201,1194,385],{"class":207},[201,1196,376],{"class":375},[201,1198,390],{"class":228},[201,1200,393],{"class":211},[201,1202,300],{"class":228},[201,1204,1205],{"class":235},"\"query courses: ",[201,1207,401],{"class":375},[201,1209,404],{"class":235},[201,1211,407],{"class":228},[201,1213,1214],{"class":203,"line":280},[201,1215,413],{"class":228},[201,1217,1218,1220,1223,1225],{"class":203,"line":286},[201,1219,580],{"class":207},[201,1221,1222],{"class":228}," rows.",[201,1224,624],{"class":211},[201,1226,586],{"class":228},[201,1228,1229],{"class":203,"line":291},[201,1230,219],{"emptyLinePlaceholder":218},[201,1232,1233,1235,1238],{"class":203,"line":345},[201,1234,798],{"class":207},[201,1236,1237],{"class":228}," courses []",[201,1239,1240],{"class":211},"Course\n",[201,1242,1243,1246,1248,1251],{"class":203,"line":363},[201,1244,1245],{"class":207},"    for",[201,1247,1222],{"class":228},[201,1249,1250],{"class":211},"Next",[201,1252,1253],{"class":228},"() {\n",[201,1255,1256,1259,1262],{"class":203,"line":382},[201,1257,1258],{"class":207},"        var",[201,1260,1261],{"class":228}," c ",[201,1263,1240],{"class":211},[201,1265,1266,1269,1271,1273,1275,1277,1279,1281,1284,1286,1289,1291,1294,1296,1299,1301,1303],{"class":203,"line":410},[201,1267,1268],{"class":207},"        if",[201,1270,369],{"class":228},[201,1272,351],{"class":207},[201,1274,1222],{"class":228},[201,1276,848],{"class":211},[201,1278,300],{"class":228},[201,1280,853],{"class":207},[201,1282,1283],{"class":228},"c.ID, ",[201,1285,853],{"class":207},[201,1287,1288],{"class":228},"c.Slug, ",[201,1290,853],{"class":207},[201,1292,1293],{"class":228},"c.Title, ",[201,1295,853],{"class":207},[201,1297,1298],{"class":228},"c.Description); err ",[201,1300,372],{"class":207},[201,1302,376],{"class":375},[201,1304,379],{"class":228},[201,1306,1307,1310,1312,1314,1316,1318,1321,1323,1325],{"class":203,"line":416},[201,1308,1309],{"class":207},"            return",[201,1311,376],{"class":375},[201,1313,390],{"class":228},[201,1315,393],{"class":211},[201,1317,300],{"class":228},[201,1319,1320],{"class":235},"\"scan course: ",[201,1322,401],{"class":375},[201,1324,404],{"class":235},[201,1326,407],{"class":228},[201,1328,1329],{"class":203,"line":421},[201,1330,1331],{"class":228},"        }\n",[201,1333,1334,1337,1339,1342],{"class":203,"line":437},[201,1335,1336],{"class":228},"        courses ",[201,1338,427],{"class":207},[201,1340,1341],{"class":211}," append",[201,1343,1344],{"class":228},"(courses, c)\n",[201,1346,1347],{"class":203,"line":448},[201,1348,413],{"class":228},[201,1350,1351,1353,1355,1357,1359,1362,1365,1367,1369],{"class":203,"line":459},[201,1352,366],{"class":207},[201,1354,369],{"class":228},[201,1356,351],{"class":207},[201,1358,1222],{"class":228},[201,1360,1361],{"class":211},"Err",[201,1363,1364],{"class":228},"(); err ",[201,1366,372],{"class":207},[201,1368,376],{"class":375},[201,1370,379],{"class":228},[201,1372,1373,1375,1377,1379,1381,1383,1386,1388,1390],{"class":203,"line":476},[201,1374,385],{"class":207},[201,1376,376],{"class":375},[201,1378,390],{"class":228},[201,1380,393],{"class":211},[201,1382,300],{"class":228},[201,1384,1385],{"class":235},"\"iterate courses: ",[201,1387,401],{"class":375},[201,1389,404],{"class":235},[201,1391,407],{"class":228},[201,1393,1394],{"class":203,"line":486},[201,1395,413],{"class":228},[201,1397,1398,1400,1403],{"class":203,"line":491},[201,1399,659],{"class":207},[201,1401,1402],{"class":228}," courses, ",[201,1404,665],{"class":375},[201,1406,1407],{"class":203,"line":507},[201,1408,671],{"class":228},[15,1410,1411],{},"Типичные ошибки:",[18,1413,1414,1421,1427,1434,1443],{},[21,1415,1416,1417,1420],{},"забыли ",[24,1418,1419],{},"rows.Close()"," и держите connection занятым;",[21,1422,1423,1424,38],{},"не проверили ",[24,1425,1426],{},"rows.Err()",[21,1428,1429,1430,1433],{},"используете ",[24,1431,1432],{},"SELECT *",", а порядок колонок меняется;",[21,1435,1436,1437,34,1440,38],{},"сканируете nullable column в обычный ",[24,1438,1439],{},"string",[24,1441,1442],{},"time.Time",[21,1444,1445,1446,1449,1450,1453,1454,310],{},"превращаете ",[24,1447,1448],{},"pgx.ErrNoRows"," в ",[24,1451,1452],{},"500",", хотя это доменная ситуация ",[24,1455,1456],{},"not found",[15,1458,1459,1460,1463,1464,120,1467,1470,1471,310],{},"SQL ",[24,1461,1462],{},"NULL"," не равен zero value Go. Для nullable-полей используйте ",[24,1465,1466],{},"pgtype.*",[24,1468,1469],{},"sql.Null*",", pointers в DTO или overrides в ",[24,1472,43],{},[52,1474,1476],{"className":194,"code":1475,"language":196,"meta":197,"style":58},"type User struct {\n    ID        int64\n    Email     string\n    Name      string\n    AvatarURL pgtype.Text\n    DeletedAt pgtype.Timestamptz\n}\n\ntype UserDTO struct {\n    ID        int64      `json:\"id\"`\n    Email     string     `json:\"email\"`\n    AvatarURL *string    `json:\"avatar_url,omitempty\"`\n    DeletedAt *time.Time `json:\"deleted_at,omitempty\"`\n}\n",[24,1477,1478,1490,1498,1506,1513,1526,1538,1542,1546,1557,1567,1576,1587,1603],{"__ignoreMap":58},[201,1479,1480,1483,1485,1488],{"class":203,"line":204},[201,1481,1482],{"class":207},"type",[201,1484,890],{"class":211},[201,1486,1487],{"class":207}," struct",[201,1489,379],{"class":228},[201,1491,1492,1495],{"class":203,"line":215},[201,1493,1494],{"class":228},"    ID        ",[201,1496,1497],{"class":207},"int64\n",[201,1499,1500,1503],{"class":203,"line":222},[201,1501,1502],{"class":228},"    Email     ",[201,1504,1505],{"class":207},"string\n",[201,1507,1508,1511],{"class":203,"line":232},[201,1509,1510],{"class":228},"    Name      ",[201,1512,1505],{"class":207},[201,1514,1515,1518,1521,1523],{"class":203,"line":245},[201,1516,1517],{"class":228},"    AvatarURL ",[201,1519,1520],{"class":211},"pgtype",[201,1522,310],{"class":228},[201,1524,1525],{"class":211},"Text\n",[201,1527,1528,1531,1533,1535],{"class":203,"line":255},[201,1529,1530],{"class":228},"    DeletedAt ",[201,1532,1520],{"class":211},[201,1534,310],{"class":228},[201,1536,1537],{"class":211},"Timestamptz\n",[201,1539,1540],{"class":203,"line":265},[201,1541,671],{"class":228},[201,1543,1544],{"class":203,"line":270},[201,1545,219],{"emptyLinePlaceholder":218},[201,1547,1548,1550,1553,1555],{"class":203,"line":280},[201,1549,1482],{"class":207},[201,1551,1552],{"class":211}," UserDTO",[201,1554,1487],{"class":207},[201,1556,379],{"class":228},[201,1558,1559,1561,1564],{"class":203,"line":286},[201,1560,1494],{"class":228},[201,1562,1563],{"class":207},"int64",[201,1565,1566],{"class":235},"      `json:\"id\"`\n",[201,1568,1569,1571,1573],{"class":203,"line":291},[201,1570,1502],{"class":228},[201,1572,1439],{"class":207},[201,1574,1575],{"class":235},"     `json:\"email\"`\n",[201,1577,1578,1580,1582,1584],{"class":203,"line":345},[201,1579,1517],{"class":228},[201,1581,327],{"class":207},[201,1583,1439],{"class":207},[201,1585,1586],{"class":235},"    `json:\"avatar_url,omitempty\"`\n",[201,1588,1589,1591,1593,1595,1597,1600],{"class":203,"line":363},[201,1590,1530],{"class":228},[201,1592,327],{"class":207},[201,1594,260],{"class":211},[201,1596,310],{"class":228},[201,1598,1599],{"class":211},"Time",[201,1601,1602],{"class":235}," `json:\"deleted_at,omitempty\"`\n",[201,1604,1605],{"class":203,"line":382},[201,1606,671],{"class":228},[62,1608],{},[65,1610,1612],{"id":1611},"транзакции-ошибки-и-retry","Транзакции, ошибки и retry",[15,1614,1615],{},"Транзакция нужна, когда несколько операций должны быть атомарны.",[52,1617,1619],{"className":194,"code":1618,"language":196,"meta":197,"style":58},"func (s *Store) EnrollUser(ctx context.Context, userID, courseID int64) error {\n    tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.ReadCommitted})\n    if err != nil {\n        return fmt.Errorf(\"begin tx: %w\", err)\n    }\n    defer func() {\n        _ = tx.Rollback(ctx)\n    }()\n\n    tag, err := tx.Exec(ctx, `\n        INSERT INTO enrollments (user_id, course_id)\n        VALUES ($1, $2)\n        ON CONFLICT DO NOTHING\n    `, userID, courseID)\n    if err != nil {\n        return fmt.Errorf(\"insert enrollment: %w\", err)\n    }\n\n    if tag.RowsAffected() == 1 {\n        if _, err := tx.Exec(ctx, `\n            UPDATE course_stats\n            SET students_count = students_count + 1\n            WHERE course_id = $1\n        `, courseID); err != nil {\n            return fmt.Errorf(\"update course stats: %w\", err)\n        }\n    }\n\n    if err := tx.Commit(ctx); err != nil {\n        return fmt.Errorf(\"commit tx: %w\", err)\n    }\n    return nil\n}\n",[24,1620,1621,1666,1690,1702,1722,1726,1735,1750,1755,1759,1775,1780,1785,1790,1797,1809,1828,1832,1836,1857,1874,1879,1884,1889,1903,1922,1926,1930,1934,1956,1975,1979,1986],{"__ignoreMap":58},[201,1622,1623,1625,1627,1629,1631,1633,1635,1638,1640,1642,1644,1646,1648,1650,1653,1655,1658,1660,1662,1664],{"class":203,"line":204},[201,1624,294],{"class":207},[201,1626,717],{"class":228},[201,1628,720],{"class":303},[201,1630,327],{"class":207},[201,1632,725],{"class":211},[201,1634,728],{"class":228},[201,1636,1637],{"class":211},"EnrollUser",[201,1639,300],{"class":228},[201,1641,304],{"class":303},[201,1643,307],{"class":211},[201,1645,310],{"class":228},[201,1647,313],{"class":211},[201,1649,120],{"class":228},[201,1651,1652],{"class":303},"userID",[201,1654,120],{"class":228},[201,1656,1657],{"class":303},"courseID",[201,1659,749],{"class":207},[201,1661,728],{"class":228},[201,1663,339],{"class":207},[201,1665,379],{"class":228},[201,1667,1668,1671,1673,1675,1678,1680,1682,1684,1687],{"class":203,"line":215},[201,1669,1670],{"class":228},"    tx, err ",[201,1672,351],{"class":207},[201,1674,814],{"class":228},[201,1676,1677],{"class":211},"BeginTx",[201,1679,566],{"class":228},[201,1681,33],{"class":211},[201,1683,310],{"class":228},[201,1685,1686],{"class":211},"TxOptions",[201,1688,1689],{"class":228},"{IsoLevel: pgx.ReadCommitted})\n",[201,1691,1692,1694,1696,1698,1700],{"class":203,"line":222},[201,1693,366],{"class":207},[201,1695,369],{"class":228},[201,1697,372],{"class":207},[201,1699,376],{"class":375},[201,1701,379],{"class":228},[201,1703,1704,1706,1709,1711,1713,1716,1718,1720],{"class":203,"line":232},[201,1705,385],{"class":207},[201,1707,1708],{"class":228}," fmt.",[201,1710,393],{"class":211},[201,1712,300],{"class":228},[201,1714,1715],{"class":235},"\"begin tx: ",[201,1717,401],{"class":375},[201,1719,404],{"class":235},[201,1721,407],{"class":228},[201,1723,1724],{"class":203,"line":245},[201,1725,413],{"class":228},[201,1727,1728,1730,1733],{"class":203,"line":255},[201,1729,580],{"class":207},[201,1731,1732],{"class":207}," func",[201,1734,1253],{"class":228},[201,1736,1737,1740,1742,1745,1747],{"class":203,"line":265},[201,1738,1739],{"class":228},"        _ ",[201,1741,427],{"class":207},[201,1743,1744],{"class":228}," tx.",[201,1746,1055],{"class":211},[201,1748,1749],{"class":228},"(ctx)\n",[201,1751,1752],{"class":203,"line":270},[201,1753,1754],{"class":228},"    }()\n",[201,1756,1757],{"class":203,"line":280},[201,1758,219],{"emptyLinePlaceholder":218},[201,1760,1761,1764,1766,1768,1771,1773],{"class":203,"line":286},[201,1762,1763],{"class":228},"    tag, err ",[201,1765,351],{"class":207},[201,1767,1744],{"class":228},[201,1769,1770],{"class":211},"Exec",[201,1772,566],{"class":228},[201,1774,822],{"class":235},[201,1776,1777],{"class":203,"line":291},[201,1778,1779],{"class":235},"        INSERT INTO enrollments (user_id, course_id)\n",[201,1781,1782],{"class":203,"line":345},[201,1783,1784],{"class":235},"        VALUES ($1, $2)\n",[201,1786,1787],{"class":203,"line":363},[201,1788,1789],{"class":235},"        ON CONFLICT DO NOTHING\n",[201,1791,1792,1794],{"class":203,"line":382},[201,1793,842],{"class":235},[201,1795,1796],{"class":228},", userID, courseID)\n",[201,1798,1799,1801,1803,1805,1807],{"class":203,"line":410},[201,1800,366],{"class":207},[201,1802,369],{"class":228},[201,1804,372],{"class":207},[201,1806,376],{"class":375},[201,1808,379],{"class":228},[201,1810,1811,1813,1815,1817,1819,1822,1824,1826],{"class":203,"line":416},[201,1812,385],{"class":207},[201,1814,1708],{"class":228},[201,1816,393],{"class":211},[201,1818,300],{"class":228},[201,1820,1821],{"class":235},"\"insert enrollment: ",[201,1823,401],{"class":375},[201,1825,404],{"class":235},[201,1827,407],{"class":228},[201,1829,1830],{"class":203,"line":421},[201,1831,413],{"class":228},[201,1833,1834],{"class":203,"line":437},[201,1835,219],{"emptyLinePlaceholder":218},[201,1837,1838,1840,1843,1846,1849,1852,1855],{"class":203,"line":448},[201,1839,366],{"class":207},[201,1841,1842],{"class":228}," tag.",[201,1844,1845],{"class":211},"RowsAffected",[201,1847,1848],{"class":228},"() ",[201,1850,1851],{"class":207},"==",[201,1853,1854],{"class":375}," 1",[201,1856,379],{"class":228},[201,1858,1859,1861,1864,1866,1868,1870,1872],{"class":203,"line":459},[201,1860,1268],{"class":207},[201,1862,1863],{"class":228}," _, err ",[201,1865,351],{"class":207},[201,1867,1744],{"class":228},[201,1869,1770],{"class":211},[201,1871,566],{"class":228},[201,1873,822],{"class":235},[201,1875,1876],{"class":203,"line":476},[201,1877,1878],{"class":235},"            UPDATE course_stats\n",[201,1880,1881],{"class":203,"line":486},[201,1882,1883],{"class":235},"            SET students_count = students_count + 1\n",[201,1885,1886],{"class":203,"line":491},[201,1887,1888],{"class":235},"            WHERE course_id = $1\n",[201,1890,1891,1894,1897,1899,1901],{"class":203,"line":507},[201,1892,1893],{"class":235},"        `",[201,1895,1896],{"class":228},", courseID); err ",[201,1898,372],{"class":207},[201,1900,376],{"class":375},[201,1902,379],{"class":228},[201,1904,1905,1907,1909,1911,1913,1916,1918,1920],{"class":203,"line":520},[201,1906,1309],{"class":207},[201,1908,1708],{"class":228},[201,1910,393],{"class":211},[201,1912,300],{"class":228},[201,1914,1915],{"class":235},"\"update course stats: ",[201,1917,401],{"class":375},[201,1919,404],{"class":235},[201,1921,407],{"class":228},[201,1923,1924],{"class":203,"line":542},[201,1925,1331],{"class":228},[201,1927,1928],{"class":203,"line":547},[201,1929,413],{"class":228},[201,1931,1932],{"class":203,"line":552},[201,1933,219],{"emptyLinePlaceholder":218},[201,1935,1936,1938,1940,1942,1944,1947,1950,1952,1954],{"class":203,"line":577},[201,1937,366],{"class":207},[201,1939,369],{"class":228},[201,1941,351],{"class":207},[201,1943,1744],{"class":228},[201,1945,1946],{"class":211},"Commit",[201,1948,1949],{"class":228},"(ctx); err ",[201,1951,372],{"class":207},[201,1953,376],{"class":375},[201,1955,379],{"class":228},[201,1957,1958,1960,1962,1964,1966,1969,1971,1973],{"class":203,"line":589},[201,1959,385],{"class":207},[201,1961,1708],{"class":228},[201,1963,393],{"class":211},[201,1965,300],{"class":228},[201,1967,1968],{"class":235},"\"commit tx: ",[201,1970,401],{"class":375},[201,1972,404],{"class":235},[201,1974,407],{"class":228},[201,1976,1977],{"class":203,"line":594},[201,1978,413],{"class":228},[201,1980,1981,1983],{"class":203,"line":618},[201,1982,659],{"class":207},[201,1984,1985],{"class":375}," nil\n",[201,1987,1988],{"class":203,"line":629},[201,1989,671],{"class":228},[15,1991,1992,1994,1995,1997,1998,310],{},[24,1993,1055],{}," после успешного ",[24,1996,1946],{}," вернёт ошибку, но это нормально, поэтому её часто игнорируют в ",[24,1999,2000],{},"defer",[70,2002,2003,2016],{},[73,2004,2005],{},[76,2006,2007,2010,2013],{},[79,2008,2009],{"align":81},"Уровень",[79,2011,2012],{"align":81},"Как ведёт себя в PostgreSQL",[79,2014,2015],{"align":81},"Когда применять",[90,2017,2018,2031,2044],{},[76,2019,2020,2025,2028],{},[95,2021,2022],{"align":81},[24,2023,2024],{},"Read Committed",[95,2026,2027],{"align":81},"Каждый statement видит snapshot на начало statement.",[95,2029,2030],{"align":81},"Большинство CRUD-сценариев.",[76,2032,2033,2038,2041],{},[95,2034,2035],{"align":81},[24,2036,2037],{},"Repeatable Read",[95,2039,2040],{"align":81},"Транзакция видит snapshot на начало первой операции.",[95,2042,2043],{"align":81},"Отчёты и проверки со стабильной картиной.",[76,2045,2046,2051,2054],{},[95,2047,2048],{"align":81},[24,2049,2050],{},"Serializable",[95,2052,2053],{"align":81},"PostgreSQL предотвращает anomalies, иногда откатывая транзакцию.",[95,2055,2056],{"align":81},"Сложные конкурентные инварианты, где приемлем retry.",[15,2058,2059,2060,2063],{},"Структурированные ошибки PostgreSQL доставайте через ",[24,2061,2062],{},"pgconn.PgError",", а не через парсинг текста:",[70,2065,2066,2079],{},[73,2067,2068],{},[76,2069,2070,2073,2076],{},[79,2071,2072],{"align":81},"SQLSTATE",[79,2074,2075],{"align":81},"Meaning",[79,2077,2078],{"align":81},"Что делать",[90,2080,2081,2094,2107,2120,2133,2146],{},[76,2082,2083,2088,2091],{},[95,2084,2085],{"align":81},[24,2086,2087],{},"23505",[95,2089,2090],{"align":81},"unique violation",[95,2092,2093],{"align":81},"Вернуть conflict или обработать idempotency.",[76,2095,2096,2101,2104],{},[95,2097,2098],{"align":81},[24,2099,2100],{},"23503",[95,2102,2103],{"align":81},"foreign key violation",[95,2105,2106],{"align":81},"Обычно validation\u002Fdomain error.",[76,2108,2109,2114,2117],{},[95,2110,2111],{"align":81},[24,2112,2113],{},"23502",[95,2115,2116],{"align":81},"not null violation",[95,2118,2119],{"align":81},"Баг в коде или миграции.",[76,2121,2122,2127,2130],{},[95,2123,2124],{"align":81},[24,2125,2126],{},"40001",[95,2128,2129],{"align":81},"serialization failure",[95,2131,2132],{"align":81},"Повторить всю транзакцию.",[76,2134,2135,2140,2143],{},[95,2136,2137],{"align":81},[24,2138,2139],{},"40P01",[95,2141,2142],{"align":81},"deadlock detected",[95,2144,2145],{"align":81},"Повторить с backoff и проверить порядок locks.",[76,2147,2148,2153,2156],{},[95,2149,2150],{"align":81},[24,2151,2152],{},"57014",[95,2154,2155],{"align":81},"query canceled",[95,2157,2158],{"align":81},"Проверить timeout\u002Fcancellation.",[52,2160,2162],{"className":194,"code":2161,"language":196,"meta":197,"style":58},"func isRetryablePostgresError(err error) bool {\n    var pgErr *pgconn.PgError\n    if !errors.As(err, &pgErr) {\n        return false\n    }\n    return pgErr.Code == \"40001\" || pgErr.Code == \"40P01\"\n}\n",[24,2163,2164,2186,2203,2224,2231,2235,2257],{"__ignoreMap":58},[201,2165,2166,2168,2171,2173,2176,2179,2181,2184],{"class":203,"line":204},[201,2167,294],{"class":207},[201,2169,2170],{"class":211}," isRetryablePostgresError",[201,2172,300],{"class":228},[201,2174,2175],{"class":303},"err",[201,2177,2178],{"class":207}," error",[201,2180,728],{"class":228},[201,2182,2183],{"class":207},"bool",[201,2185,379],{"class":228},[201,2187,2188,2190,2193,2195,2198,2200],{"class":203,"line":215},[201,2189,798],{"class":207},[201,2191,2192],{"class":228}," pgErr ",[201,2194,327],{"class":207},[201,2196,2197],{"class":211},"pgconn",[201,2199,310],{"class":228},[201,2201,2202],{"class":211},"PgError\n",[201,2204,2205,2207,2210,2213,2216,2219,2221],{"class":203,"line":222},[201,2206,366],{"class":207},[201,2208,2209],{"class":207}," !",[201,2211,2212],{"class":228},"errors.",[201,2214,2215],{"class":211},"As",[201,2217,2218],{"class":228},"(err, ",[201,2220,853],{"class":207},[201,2222,2223],{"class":228},"pgErr) {\n",[201,2225,2226,2228],{"class":203,"line":232},[201,2227,385],{"class":207},[201,2229,2230],{"class":375}," false\n",[201,2232,2233],{"class":203,"line":245},[201,2234,413],{"class":228},[201,2236,2237,2239,2242,2244,2247,2250,2252,2254],{"class":203,"line":255},[201,2238,659],{"class":207},[201,2240,2241],{"class":228}," pgErr.Code ",[201,2243,1851],{"class":207},[201,2245,2246],{"class":235}," \"40001\"",[201,2248,2249],{"class":207}," ||",[201,2251,2241],{"class":228},[201,2253,1851],{"class":207},[201,2255,2256],{"class":235}," \"40P01\"\n",[201,2258,2259],{"class":203,"line":265},[201,2260,671],{"class":228},[15,2262,2263,2264,2267],{},"Retry безопасен только если повторяется весь бизнес-сценарий внутри незакоммиченной транзакции или операция сделана идемпотентной. Повторять один последний ",[24,2265,2266],{},"UPDATE"," обычно недостаточно.",[62,2269],{},[65,2271,2273],{"id":2272},"производительные-операции-pgx","Производительные операции pgx",[70,2275,2276,2289],{},[73,2277,2278],{},[76,2279,2280,2283,2286],{},[79,2281,2282],{"align":81},"Возможность",[79,2284,2285],{"align":81},"Для чего",[79,2287,2288],{"align":81},"Главная осторожность",[90,2290,2291,2302,2316],{},[76,2292,2293,2296,2299],{},[95,2294,2295],{"align":81},"Statement cache \u002F prepared statements",[95,2297,2298],{"align":81},"Повторяющиеся запросы с параметрами.",[95,2300,2301],{"align":81},"Prepared statement живёт на connection; PgBouncer transaction pooling может мешать.",[76,2303,2304,2308,2311],{},[95,2305,2306],{"align":81},[24,2307,123],{},[95,2309,2310],{"align":81},"Сократить network round-trips для группы запросов.",[95,2312,2313,2315],{"align":81},[24,2314,123],{}," сам не делает операции атомарными.",[76,2317,2318,2322,2325],{},[95,2319,2320],{"align":81},[24,2321,119],{},[95,2323,2324],{"align":81},"Bulk insert: импорт, backfill, batch jobs.",[95,2326,2327],{"align":81},"Не подходит для одной-двух строк и сложной per-row логики.",[52,2329,2331],{"className":194,"code":2330,"language":196,"meta":197,"style":58},"batch := &pgx.Batch{}\nbatch.Queue(`INSERT INTO audit_log (user_id, action) VALUES ($1, $2)`, userID, \"login\")\nbatch.Queue(`UPDATE users SET last_login_at = now() WHERE id = $1`, userID)\n\nbr := pool.SendBatch(ctx, batch)\ndefer br.Close()\n\nif _, err := br.Exec(); err != nil {\n    return fmt.Errorf(\"insert audit log: %w\", err)\n}\nif _, err := br.Exec(); err != nil {\n    return fmt.Errorf(\"update last login: %w\", err)\n}\n",[24,2332,2333,2352,2373,2387,2391,2406,2417,2421,2442,2461,2465,2485,2504],{"__ignoreMap":58},[201,2334,2335,2338,2340,2343,2345,2347,2349],{"class":203,"line":204},[201,2336,2337],{"class":228},"batch ",[201,2339,351],{"class":207},[201,2341,2342],{"class":207}," &",[201,2344,33],{"class":211},[201,2346,310],{"class":228},[201,2348,123],{"class":211},[201,2350,2351],{"class":228},"{}\n",[201,2353,2354,2357,2360,2362,2365,2368,2371],{"class":203,"line":215},[201,2355,2356],{"class":228},"batch.",[201,2358,2359],{"class":211},"Queue",[201,2361,300],{"class":228},[201,2363,2364],{"class":235},"`INSERT INTO audit_log (user_id, action) VALUES ($1, $2)`",[201,2366,2367],{"class":228},", userID, ",[201,2369,2370],{"class":235},"\"login\"",[201,2372,283],{"class":228},[201,2374,2375,2377,2379,2381,2384],{"class":203,"line":222},[201,2376,2356],{"class":228},[201,2378,2359],{"class":211},[201,2380,300],{"class":228},[201,2382,2383],{"class":235},"`UPDATE users SET last_login_at = now() WHERE id = $1`",[201,2385,2386],{"class":228},", userID)\n",[201,2388,2389],{"class":203,"line":232},[201,2390,219],{"emptyLinePlaceholder":218},[201,2392,2393,2396,2398,2400,2403],{"class":203,"line":245},[201,2394,2395],{"class":228},"br ",[201,2397,351],{"class":207},[201,2399,603],{"class":228},[201,2401,2402],{"class":211},"SendBatch",[201,2404,2405],{"class":228},"(ctx, batch)\n",[201,2407,2408,2410,2413,2415],{"class":203,"line":255},[201,2409,2000],{"class":207},[201,2411,2412],{"class":228}," br.",[201,2414,624],{"class":211},[201,2416,586],{"class":228},[201,2418,2419],{"class":203,"line":265},[201,2420,219],{"emptyLinePlaceholder":218},[201,2422,2423,2426,2428,2430,2432,2434,2436,2438,2440],{"class":203,"line":270},[201,2424,2425],{"class":207},"if",[201,2427,1863],{"class":228},[201,2429,351],{"class":207},[201,2431,2412],{"class":228},[201,2433,1770],{"class":211},[201,2435,1364],{"class":228},[201,2437,372],{"class":207},[201,2439,376],{"class":375},[201,2441,379],{"class":228},[201,2443,2444,2446,2448,2450,2452,2455,2457,2459],{"class":203,"line":280},[201,2445,659],{"class":207},[201,2447,1708],{"class":228},[201,2449,393],{"class":211},[201,2451,300],{"class":228},[201,2453,2454],{"class":235},"\"insert audit log: ",[201,2456,401],{"class":375},[201,2458,404],{"class":235},[201,2460,407],{"class":228},[201,2462,2463],{"class":203,"line":286},[201,2464,671],{"class":228},[201,2466,2467,2469,2471,2473,2475,2477,2479,2481,2483],{"class":203,"line":291},[201,2468,2425],{"class":207},[201,2470,1863],{"class":228},[201,2472,351],{"class":207},[201,2474,2412],{"class":228},[201,2476,1770],{"class":211},[201,2478,1364],{"class":228},[201,2480,372],{"class":207},[201,2482,376],{"class":375},[201,2484,379],{"class":228},[201,2486,2487,2489,2491,2493,2495,2498,2500,2502],{"class":203,"line":345},[201,2488,659],{"class":207},[201,2490,1708],{"class":228},[201,2492,393],{"class":211},[201,2494,300],{"class":228},[201,2496,2497],{"class":235},"\"update last login: ",[201,2499,401],{"class":375},[201,2501,404],{"class":235},[201,2503,407],{"class":228},[201,2505,2506],{"class":203,"line":363},[201,2507,671],{"class":228},[52,2509,2511],{"className":194,"code":2510,"language":196,"meta":197,"style":58},"count, err := pool.CopyFrom(\n    ctx,\n    pgx.Identifier{\"courses\"},\n    []string{\"slug\", \"title\"},\n    pgx.CopyFromRows([][]any{\n        {\"intro-to-go\", \"Введение в Go\"},\n        {\"postgres-in-go\", \"PostgreSQL в Go\"},\n    }),\n)\nif err != nil {\n    return fmt.Errorf(\"copy courses: %w\", err)\n}\nlog.Printf(\"inserted %d rows\", count)\n",[24,2512,2513,2527,2532,2551,2570,2587,2602,2616,2621,2625,2637,2656,2660],{"__ignoreMap":58},[201,2514,2515,2518,2520,2522,2524],{"class":203,"line":204},[201,2516,2517],{"class":228},"count, err ",[201,2519,351],{"class":207},[201,2521,603],{"class":228},[201,2523,119],{"class":211},[201,2525,2526],{"class":228},"(\n",[201,2528,2529],{"class":203,"line":215},[201,2530,2531],{"class":228},"    ctx,\n",[201,2533,2534,2537,2539,2542,2545,2548],{"class":203,"line":222},[201,2535,2536],{"class":211},"    pgx",[201,2538,310],{"class":228},[201,2540,2541],{"class":211},"Identifier",[201,2543,2544],{"class":228},"{",[201,2546,2547],{"class":235},"\"courses\"",[201,2549,2550],{"class":228},"},\n",[201,2552,2553,2556,2558,2560,2563,2565,2568],{"class":203,"line":232},[201,2554,2555],{"class":228},"    []",[201,2557,1439],{"class":207},[201,2559,2544],{"class":228},[201,2561,2562],{"class":235},"\"slug\"",[201,2564,120],{"class":228},[201,2566,2567],{"class":235},"\"title\"",[201,2569,2550],{"class":228},[201,2571,2572,2575,2578,2581,2584],{"class":203,"line":245},[201,2573,2574],{"class":228},"    pgx.",[201,2576,2577],{"class":211},"CopyFromRows",[201,2579,2580],{"class":228},"([][]",[201,2582,2583],{"class":211},"any",[201,2585,2586],{"class":228},"{\n",[201,2588,2589,2592,2595,2597,2600],{"class":203,"line":255},[201,2590,2591],{"class":228},"        {",[201,2593,2594],{"class":235},"\"intro-to-go\"",[201,2596,120],{"class":228},[201,2598,2599],{"class":235},"\"Введение в Go\"",[201,2601,2550],{"class":228},[201,2603,2604,2606,2609,2611,2614],{"class":203,"line":265},[201,2605,2591],{"class":228},[201,2607,2608],{"class":235},"\"postgres-in-go\"",[201,2610,120],{"class":228},[201,2612,2613],{"class":235},"\"PostgreSQL в Go\"",[201,2615,2550],{"class":228},[201,2617,2618],{"class":203,"line":270},[201,2619,2620],{"class":228},"    }),\n",[201,2622,2623],{"class":203,"line":280},[201,2624,283],{"class":228},[201,2626,2627,2629,2631,2633,2635],{"class":203,"line":286},[201,2628,2425],{"class":207},[201,2630,369],{"class":228},[201,2632,372],{"class":207},[201,2634,376],{"class":375},[201,2636,379],{"class":228},[201,2638,2639,2641,2643,2645,2647,2650,2652,2654],{"class":203,"line":291},[201,2640,659],{"class":207},[201,2642,1708],{"class":228},[201,2644,393],{"class":211},[201,2646,300],{"class":228},[201,2648,2649],{"class":235},"\"copy courses: ",[201,2651,401],{"class":375},[201,2653,404],{"class":235},[201,2655,407],{"class":228},[201,2657,2658],{"class":203,"line":345},[201,2659,671],{"class":228},[201,2661,2662,2665,2668,2670,2673,2676,2679],{"class":203,"line":363},[201,2663,2664],{"class":228},"log.",[201,2666,2667],{"class":211},"Printf",[201,2669,300],{"class":228},[201,2671,2672],{"class":235},"\"inserted ",[201,2674,2675],{"class":375},"%d",[201,2677,2678],{"class":235}," rows\"",[201,2680,2681],{"class":228},", count)\n",[62,2683],{},[65,2685,2687],{"id":2686},"наблюдаемость","Наблюдаемость",[15,2689,2690],{},"БД почти всегда заметна в latency budget. Минимальный набор:",[18,2692,2693,2696,2699,2702,2705,2708,2711,2714],{},[21,2694,2695],{},"duration metrics по операциям;",[21,2697,2698],{},"error counters по SQLSTATE;",[21,2700,2701],{},"pool stats;",[21,2703,2704],{},"slow query logs;",[21,2706,2707],{},"tracing spans;",[21,2709,2710],{},"request id \u002F stable tenant\u002Fuser identifier только если это разрешено privacy policy;",[21,2712,2713],{},"operation name вместо сырого SQL в logs\u002Ftraces;",[21,2715,2716],{},"alerts на connection usage, lock waits, deadlocks.",[52,2718,2720],{"className":194,"code":2719,"language":196,"meta":197,"style":58},"stat := pool.Stat()\n\nmetrics.PostgresConnsTotal.Set(float64(stat.TotalConns()))\nmetrics.PostgresConnsAcquired.Set(float64(stat.AcquiredConns()))\nmetrics.PostgresAcquireCount.Add(float64(stat.AcquireCount()))\nmetrics.PostgresAcquireDuration.Add(stat.AcquireDuration().Seconds())\nmetrics.PostgresCanceledAcquireCount.Add(float64(stat.CanceledAcquireCount()))\n",[24,2721,2722,2736,2740,2762,2780,2799,2820],{"__ignoreMap":58},[201,2723,2724,2727,2729,2731,2734],{"class":203,"line":204},[201,2725,2726],{"class":228},"stat ",[201,2728,351],{"class":207},[201,2730,603],{"class":228},[201,2732,2733],{"class":211},"Stat",[201,2735,586],{"class":228},[201,2737,2738],{"class":203,"line":215},[201,2739,219],{"emptyLinePlaceholder":218},[201,2741,2742,2745,2748,2750,2753,2756,2759],{"class":203,"line":222},[201,2743,2744],{"class":228},"metrics.PostgresConnsTotal.",[201,2746,2747],{"class":211},"Set",[201,2749,300],{"class":228},[201,2751,2752],{"class":207},"float64",[201,2754,2755],{"class":228},"(stat.",[201,2757,2758],{"class":211},"TotalConns",[201,2760,2761],{"class":228},"()))\n",[201,2763,2764,2767,2769,2771,2773,2775,2778],{"class":203,"line":232},[201,2765,2766],{"class":228},"metrics.PostgresConnsAcquired.",[201,2768,2747],{"class":211},[201,2770,300],{"class":228},[201,2772,2752],{"class":207},[201,2774,2755],{"class":228},[201,2776,2777],{"class":211},"AcquiredConns",[201,2779,2761],{"class":228},[201,2781,2782,2785,2788,2790,2792,2794,2797],{"class":203,"line":245},[201,2783,2784],{"class":228},"metrics.PostgresAcquireCount.",[201,2786,2787],{"class":211},"Add",[201,2789,300],{"class":228},[201,2791,2752],{"class":207},[201,2793,2755],{"class":228},[201,2795,2796],{"class":211},"AcquireCount",[201,2798,2761],{"class":228},[201,2800,2801,2804,2806,2808,2811,2814,2817],{"class":203,"line":255},[201,2802,2803],{"class":228},"metrics.PostgresAcquireDuration.",[201,2805,2787],{"class":211},[201,2807,2755],{"class":228},[201,2809,2810],{"class":211},"AcquireDuration",[201,2812,2813],{"class":228},"().",[201,2815,2816],{"class":211},"Seconds",[201,2818,2819],{"class":228},"())\n",[201,2821,2822,2825,2827,2829,2831,2833,2836],{"class":203,"line":265},[201,2823,2824],{"class":228},"metrics.PostgresCanceledAcquireCount.",[201,2826,2787],{"class":211},[201,2828,300],{"class":228},[201,2830,2752],{"class":207},[201,2832,2755],{"class":228},[201,2834,2835],{"class":211},"CanceledAcquireCount",[201,2837,2761],{"class":228},[15,2839,2840,2841,120,2844,120,2847,120,2850,120,2852,120,2855,310],{},"Не логируйте полный SQL с личными данными и токенами. Обычно достаточно ",[24,2842,2843],{},"operation name",[24,2845,2846],{},"duration",[24,2848,2849],{},"rows affected",[24,2851,2072],{},[24,2853,2854],{},"retry attempt",[24,2856,2857],{},"trace id",[15,2859,2860,2861,2864,2865,2868],{},"Для tracing задавайте имена span вроде ",[24,2862,2863],{},"db.rates.read_latest",", а не ",[24,2866,2867],{},"SELECT ... WHERE user_email = ...",". Args, DSN, bearer tokens, card\u002Faccount identifiers и raw payloads должны быть redacted до попадания в logger\u002Ftracer. В debug-режиме можно включать query text только для локального окружения или через защищённый sampling с scrubber'ом.",[62,2870],{},[65,2872,43],{"id":43},[15,2874,2875,2877],{},[24,2876,43],{}," генерирует Go-код из schema и queries. Это не ORM: SQL остаётся вашим SQL, а ошибки в колонках и параметрах ловятся раньше runtime.",[52,2879,2882],{"className":2880,"code":2881,"language":57,"meta":58},[55],"schema.sql + queries.sql + sqlc.yaml\n        |\n        v\nsqlc generate\n        |\n        v\ninternal\u002Fdb\u002F*.go\n",[24,2883,2881],{"__ignoreMap":58},[15,2885,2886],{},"Пример структуры:",[52,2888,2891],{"className":2889,"code":2890,"language":57,"meta":58},[55],"backend\u002F\n  sqlc.yaml\n  internal\u002Fdb\u002F\n    migrations\u002F\n      001_init.sql\n    query\u002F\n      users.sql\n    generated\u002F\n      db.go\n      models.go\n      users.sql.go\n",[24,2892,2890],{"__ignoreMap":58},[52,2894,2898],{"className":2895,"code":2896,"language":2897,"meta":58,"style":58},"language-yaml shiki shiki-themes github-dark","version: \"2\"\nsql:\n  - engine: \"postgresql\"\n    schema: \"internal\u002Fdb\u002Fmigrations\"\n    queries: \"internal\u002Fdb\u002Fquery\"\n    gen:\n      go:\n        package: \"db\"\n        out: \"internal\u002Fdb\u002Fgenerated\"\n        sql_package: \"pgx\u002Fv5\"\n        emit_json_tags: true\n        emit_empty_slices: true\n","yaml",[24,2899,2900,2911,2918,2931,2941,2951,2958,2965,2975,2985,2995,3005],{"__ignoreMap":58},[201,2901,2902,2906,2908],{"class":203,"line":204},[201,2903,2905],{"class":2904},"s4JwU","version",[201,2907,677],{"class":228},[201,2909,2910],{"class":235},"\"2\"\n",[201,2912,2913,2915],{"class":203,"line":215},[201,2914,995],{"class":2904},[201,2916,2917],{"class":228},":\n",[201,2919,2920,2923,2926,2928],{"class":203,"line":222},[201,2921,2922],{"class":228},"  - ",[201,2924,2925],{"class":2904},"engine",[201,2927,677],{"class":228},[201,2929,2930],{"class":235},"\"postgresql\"\n",[201,2932,2933,2936,2938],{"class":203,"line":232},[201,2934,2935],{"class":2904},"    schema",[201,2937,677],{"class":228},[201,2939,2940],{"class":235},"\"internal\u002Fdb\u002Fmigrations\"\n",[201,2942,2943,2946,2948],{"class":203,"line":245},[201,2944,2945],{"class":2904},"    queries",[201,2947,677],{"class":228},[201,2949,2950],{"class":235},"\"internal\u002Fdb\u002Fquery\"\n",[201,2952,2953,2956],{"class":203,"line":255},[201,2954,2955],{"class":2904},"    gen",[201,2957,2917],{"class":228},[201,2959,2960,2963],{"class":203,"line":265},[201,2961,2962],{"class":2904},"      go",[201,2964,2917],{"class":228},[201,2966,2967,2970,2972],{"class":203,"line":270},[201,2968,2969],{"class":2904},"        package",[201,2971,677],{"class":228},[201,2973,2974],{"class":235},"\"db\"\n",[201,2976,2977,2980,2982],{"class":203,"line":280},[201,2978,2979],{"class":2904},"        out",[201,2981,677],{"class":228},[201,2983,2984],{"class":235},"\"internal\u002Fdb\u002Fgenerated\"\n",[201,2986,2987,2990,2992],{"class":203,"line":286},[201,2988,2989],{"class":2904},"        sql_package",[201,2991,677],{"class":228},[201,2993,2994],{"class":235},"\"pgx\u002Fv5\"\n",[201,2996,2997,3000,3002],{"class":203,"line":291},[201,2998,2999],{"class":2904},"        emit_json_tags",[201,3001,677],{"class":228},[201,3003,3004],{"class":375},"true\n",[201,3006,3007,3010,3012],{"class":203,"line":345},[201,3008,3009],{"class":2904},"        emit_empty_slices",[201,3011,677],{"class":228},[201,3013,3004],{"class":375},[52,3015,3017],{"className":993,"code":3016,"language":995,"meta":58,"style":58},"-- name: GetUserByID :one\nSELECT id, email, name, avatar_url, created_at\nFROM users\nWHERE id = $1;\n\n-- name: ListUsers :many\nSELECT id, email, name, avatar_url, created_at\nFROM users\nORDER BY id\nLIMIT $1 OFFSET $2;\n",[24,3018,3019,3024,3038,3046,3063,3067,3072,3082,3088,3096],{"__ignoreMap":58},[201,3020,3021],{"class":203,"line":204},[201,3022,3023],{"class":433},"-- name: GetUserByID :one\n",[201,3025,3026,3029,3032,3035],{"class":203,"line":215},[201,3027,3028],{"class":207},"SELECT",[201,3030,3031],{"class":228}," id, email, ",[201,3033,3034],{"class":207},"name",[201,3036,3037],{"class":228},", avatar_url, created_at\n",[201,3039,3040,3043],{"class":203,"line":222},[201,3041,3042],{"class":207},"FROM",[201,3044,3045],{"class":228}," users\n",[201,3047,3048,3050,3053,3055,3058,3061],{"class":203,"line":232},[201,3049,157],{"class":207},[201,3051,3052],{"class":228}," id ",[201,3054,427],{"class":207},[201,3056,3057],{"class":228}," $",[201,3059,3060],{"class":375},"1",[201,3062,1016],{"class":228},[201,3064,3065],{"class":203,"line":245},[201,3066,219],{"emptyLinePlaceholder":218},[201,3068,3069],{"class":203,"line":255},[201,3070,3071],{"class":433},"-- name: ListUsers :many\n",[201,3073,3074,3076,3078,3080],{"class":203,"line":265},[201,3075,3028],{"class":207},[201,3077,3031],{"class":228},[201,3079,3034],{"class":207},[201,3081,3037],{"class":228},[201,3083,3084,3086],{"class":203,"line":270},[201,3085,3042],{"class":207},[201,3087,3045],{"class":228},[201,3089,3090,3093],{"class":203,"line":280},[201,3091,3092],{"class":207},"ORDER BY",[201,3094,3095],{"class":228}," id\n",[201,3097,3098,3101,3103,3105,3108,3110],{"class":203,"line":286},[201,3099,3100],{"class":207},"LIMIT",[201,3102,3057],{"class":228},[201,3104,3060],{"class":375},[201,3106,3107],{"class":228}," OFFSET $",[201,3109,569],{"class":375},[201,3111,1016],{"class":228},[15,3113,3114],{},"Обычно generated models держат отдельно от domain models:",[52,3116,3118],{"className":194,"code":3117,"language":196,"meta":197,"style":58},"func (s *Store) GetUser(ctx context.Context, id int64) (User, error) {\n    row, err := s.q.GetUserByID(ctx, id)\n    if errors.Is(err, pgx.ErrNoRows) {\n        return User{}, ErrUserNotFound\n    }\n    if err != nil {\n        return User{}, fmt.Errorf(\"get user by id: %w\", err)\n    }\n\n    return User{\n        ID:    row.ID,\n        Email: row.Email,\n        Name:  row.Name,\n    }, nil\n}\n",[24,3119,3120,3162,3178,3191,3200,3204,3216,3237,3241,3245,3253,3258,3263,3268,3275],{"__ignoreMap":58},[201,3121,3122,3124,3126,3128,3130,3132,3134,3136,3138,3140,3142,3144,3146,3148,3150,3152,3154,3156,3158,3160],{"class":203,"line":204},[201,3123,294],{"class":207},[201,3125,717],{"class":228},[201,3127,720],{"class":303},[201,3129,327],{"class":207},[201,3131,725],{"class":211},[201,3133,728],{"class":228},[201,3135,731],{"class":211},[201,3137,300],{"class":228},[201,3139,304],{"class":303},[201,3141,307],{"class":211},[201,3143,310],{"class":228},[201,3145,313],{"class":211},[201,3147,120],{"class":228},[201,3149,746],{"class":303},[201,3151,749],{"class":207},[201,3153,324],{"class":228},[201,3155,754],{"class":211},[201,3157,120],{"class":228},[201,3159,339],{"class":207},[201,3161,342],{"class":228},[201,3163,3164,3167,3169,3172,3175],{"class":203,"line":215},[201,3165,3166],{"class":228},"    row, err ",[201,3168,351],{"class":207},[201,3170,3171],{"class":228}," s.q.",[201,3173,3174],{"class":211},"GetUserByID",[201,3176,3177],{"class":228},"(ctx, id)\n",[201,3179,3180,3182,3185,3188],{"class":203,"line":222},[201,3181,366],{"class":207},[201,3183,3184],{"class":228}," errors.",[201,3186,3187],{"class":211},"Is",[201,3189,3190],{"class":228},"(err, pgx.ErrNoRows) {\n",[201,3192,3193,3195,3197],{"class":203,"line":232},[201,3194,385],{"class":207},[201,3196,890],{"class":211},[201,3198,3199],{"class":228},"{}, ErrUserNotFound\n",[201,3201,3202],{"class":203,"line":245},[201,3203,413],{"class":228},[201,3205,3206,3208,3210,3212,3214],{"class":203,"line":255},[201,3207,366],{"class":207},[201,3209,369],{"class":228},[201,3211,372],{"class":207},[201,3213,376],{"class":375},[201,3215,379],{"class":228},[201,3217,3218,3220,3222,3224,3226,3228,3231,3233,3235],{"class":203,"line":265},[201,3219,385],{"class":207},[201,3221,890],{"class":211},[201,3223,893],{"class":228},[201,3225,393],{"class":211},[201,3227,300],{"class":228},[201,3229,3230],{"class":235},"\"get user by id: ",[201,3232,401],{"class":375},[201,3234,404],{"class":235},[201,3236,407],{"class":228},[201,3238,3239],{"class":203,"line":270},[201,3240,413],{"class":228},[201,3242,3243],{"class":203,"line":280},[201,3244,219],{"emptyLinePlaceholder":218},[201,3246,3247,3249,3251],{"class":203,"line":286},[201,3248,659],{"class":207},[201,3250,890],{"class":211},[201,3252,2586],{"class":228},[201,3254,3255],{"class":203,"line":291},[201,3256,3257],{"class":228},"        ID:    row.ID,\n",[201,3259,3260],{"class":203,"line":345},[201,3261,3262],{"class":228},"        Email: row.Email,\n",[201,3264,3265],{"class":203,"line":363},[201,3266,3267],{"class":228},"        Name:  row.Name,\n",[201,3269,3270,3273],{"class":203,"line":382},[201,3271,3272],{"class":228},"    }, ",[201,3274,665],{"class":375},[201,3276,3277],{"class":203,"line":410},[201,3278,671],{"class":228},[15,3280,3281,3282,3284,3285,705],{},"Транзакционный ",[24,3283,43],{},"-код привязывают через ",[24,3286,3287],{},"WithTx",[52,3289,3291],{"className":194,"code":3290,"language":196,"meta":197,"style":58},"func (s *Store) CreateOrder(ctx context.Context, in CreateOrderInput) error {\n    tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{})\n    if err != nil {\n        return fmt.Errorf(\"begin tx: %w\", err)\n    }\n    defer func() {\n        _ = tx.Rollback(ctx)\n    }()\n\n    qtx := s.q.WithTx(tx)\n\n    order, err := qtx.CreateOrder(ctx, db.CreateOrderParams{\n        UserID: in.UserID,\n        Total:  in.Total,\n    })\n    if err != nil {\n        return fmt.Errorf(\"create order: %w\", err)\n    }\n\n    for _, item := range in.Items {\n        if err := qtx.CreateOrderItem(ctx, db.CreateOrderItemParams{\n            OrderID: order.ID,\n            Sku:     item.SKU,\n            Qty:     item.Quantity,\n        }); err != nil {\n            return fmt.Errorf(\"create order item: %w\", err)\n        }\n    }\n\n    if err := tx.Commit(ctx); err != nil {\n        return fmt.Errorf(\"commit tx: %w\", err)\n    }\n    return nil\n}\n",[24,3292,3293,3334,3355,3367,3385,3389,3397,3409,3413,3417,3431,3435,3459,3464,3469,3474,3486,3505,3509,3513,3528,3552,3557,3562,3567,3578,3597,3601,3605,3609,3629,3647,3651,3657],{"__ignoreMap":58},[201,3294,3295,3297,3299,3301,3303,3305,3307,3310,3312,3314,3316,3318,3320,3322,3325,3328,3330,3332],{"class":203,"line":204},[201,3296,294],{"class":207},[201,3298,717],{"class":228},[201,3300,720],{"class":303},[201,3302,327],{"class":207},[201,3304,725],{"class":211},[201,3306,728],{"class":228},[201,3308,3309],{"class":211},"CreateOrder",[201,3311,300],{"class":228},[201,3313,304],{"class":303},[201,3315,307],{"class":211},[201,3317,310],{"class":228},[201,3319,313],{"class":211},[201,3321,120],{"class":228},[201,3323,3324],{"class":303},"in",[201,3326,3327],{"class":211}," CreateOrderInput",[201,3329,728],{"class":228},[201,3331,339],{"class":207},[201,3333,379],{"class":228},[201,3335,3336,3338,3340,3342,3344,3346,3348,3350,3352],{"class":203,"line":215},[201,3337,1670],{"class":228},[201,3339,351],{"class":207},[201,3341,814],{"class":228},[201,3343,1677],{"class":211},[201,3345,566],{"class":228},[201,3347,33],{"class":211},[201,3349,310],{"class":228},[201,3351,1686],{"class":211},[201,3353,3354],{"class":228},"{})\n",[201,3356,3357,3359,3361,3363,3365],{"class":203,"line":222},[201,3358,366],{"class":207},[201,3360,369],{"class":228},[201,3362,372],{"class":207},[201,3364,376],{"class":375},[201,3366,379],{"class":228},[201,3368,3369,3371,3373,3375,3377,3379,3381,3383],{"class":203,"line":232},[201,3370,385],{"class":207},[201,3372,1708],{"class":228},[201,3374,393],{"class":211},[201,3376,300],{"class":228},[201,3378,1715],{"class":235},[201,3380,401],{"class":375},[201,3382,404],{"class":235},[201,3384,407],{"class":228},[201,3386,3387],{"class":203,"line":245},[201,3388,413],{"class":228},[201,3390,3391,3393,3395],{"class":203,"line":255},[201,3392,580],{"class":207},[201,3394,1732],{"class":207},[201,3396,1253],{"class":228},[201,3398,3399,3401,3403,3405,3407],{"class":203,"line":265},[201,3400,1739],{"class":228},[201,3402,427],{"class":207},[201,3404,1744],{"class":228},[201,3406,1055],{"class":211},[201,3408,1749],{"class":228},[201,3410,3411],{"class":203,"line":270},[201,3412,1754],{"class":228},[201,3414,3415],{"class":203,"line":280},[201,3416,219],{"emptyLinePlaceholder":218},[201,3418,3419,3422,3424,3426,3428],{"class":203,"line":286},[201,3420,3421],{"class":228},"    qtx ",[201,3423,351],{"class":207},[201,3425,3171],{"class":228},[201,3427,3287],{"class":211},[201,3429,3430],{"class":228},"(tx)\n",[201,3432,3433],{"class":203,"line":291},[201,3434,219],{"emptyLinePlaceholder":218},[201,3436,3437,3440,3442,3445,3447,3449,3452,3454,3457],{"class":203,"line":345},[201,3438,3439],{"class":228},"    order, err ",[201,3441,351],{"class":207},[201,3443,3444],{"class":228}," qtx.",[201,3446,3309],{"class":211},[201,3448,566],{"class":228},[201,3450,3451],{"class":211},"db",[201,3453,310],{"class":228},[201,3455,3456],{"class":211},"CreateOrderParams",[201,3458,2586],{"class":228},[201,3460,3461],{"class":203,"line":363},[201,3462,3463],{"class":228},"        UserID: in.UserID,\n",[201,3465,3466],{"class":203,"line":382},[201,3467,3468],{"class":228},"        Total:  in.Total,\n",[201,3470,3471],{"class":203,"line":410},[201,3472,3473],{"class":228},"    })\n",[201,3475,3476,3478,3480,3482,3484],{"class":203,"line":416},[201,3477,366],{"class":207},[201,3479,369],{"class":228},[201,3481,372],{"class":207},[201,3483,376],{"class":375},[201,3485,379],{"class":228},[201,3487,3488,3490,3492,3494,3496,3499,3501,3503],{"class":203,"line":421},[201,3489,385],{"class":207},[201,3491,1708],{"class":228},[201,3493,393],{"class":211},[201,3495,300],{"class":228},[201,3497,3498],{"class":235},"\"create order: ",[201,3500,401],{"class":375},[201,3502,404],{"class":235},[201,3504,407],{"class":228},[201,3506,3507],{"class":203,"line":437},[201,3508,413],{"class":228},[201,3510,3511],{"class":203,"line":448},[201,3512,219],{"emptyLinePlaceholder":218},[201,3514,3515,3517,3520,3522,3525],{"class":203,"line":459},[201,3516,1245],{"class":207},[201,3518,3519],{"class":228}," _, item ",[201,3521,351],{"class":207},[201,3523,3524],{"class":207}," range",[201,3526,3527],{"class":228}," in.Items {\n",[201,3529,3530,3532,3534,3536,3538,3541,3543,3545,3547,3550],{"class":203,"line":476},[201,3531,1268],{"class":207},[201,3533,369],{"class":228},[201,3535,351],{"class":207},[201,3537,3444],{"class":228},[201,3539,3540],{"class":211},"CreateOrderItem",[201,3542,566],{"class":228},[201,3544,3451],{"class":211},[201,3546,310],{"class":228},[201,3548,3549],{"class":211},"CreateOrderItemParams",[201,3551,2586],{"class":228},[201,3553,3554],{"class":203,"line":486},[201,3555,3556],{"class":228},"            OrderID: order.ID,\n",[201,3558,3559],{"class":203,"line":491},[201,3560,3561],{"class":228},"            Sku:     item.SKU,\n",[201,3563,3564],{"class":203,"line":507},[201,3565,3566],{"class":228},"            Qty:     item.Quantity,\n",[201,3568,3569,3572,3574,3576],{"class":203,"line":520},[201,3570,3571],{"class":228},"        }); err ",[201,3573,372],{"class":207},[201,3575,376],{"class":375},[201,3577,379],{"class":228},[201,3579,3580,3582,3584,3586,3588,3591,3593,3595],{"class":203,"line":542},[201,3581,1309],{"class":207},[201,3583,1708],{"class":228},[201,3585,393],{"class":211},[201,3587,300],{"class":228},[201,3589,3590],{"class":235},"\"create order item: ",[201,3592,401],{"class":375},[201,3594,404],{"class":235},[201,3596,407],{"class":228},[201,3598,3599],{"class":203,"line":547},[201,3600,1331],{"class":228},[201,3602,3603],{"class":203,"line":552},[201,3604,413],{"class":228},[201,3606,3607],{"class":203,"line":577},[201,3608,219],{"emptyLinePlaceholder":218},[201,3610,3611,3613,3615,3617,3619,3621,3623,3625,3627],{"class":203,"line":589},[201,3612,366],{"class":207},[201,3614,369],{"class":228},[201,3616,351],{"class":207},[201,3618,1744],{"class":228},[201,3620,1946],{"class":211},[201,3622,1949],{"class":228},[201,3624,372],{"class":207},[201,3626,376],{"class":375},[201,3628,379],{"class":228},[201,3630,3631,3633,3635,3637,3639,3641,3643,3645],{"class":203,"line":594},[201,3632,385],{"class":207},[201,3634,1708],{"class":228},[201,3636,393],{"class":211},[201,3638,300],{"class":228},[201,3640,1968],{"class":235},[201,3642,401],{"class":375},[201,3644,404],{"class":235},[201,3646,407],{"class":228},[201,3648,3649],{"class":203,"line":618},[201,3650,413],{"class":228},[201,3652,3653,3655],{"class":203,"line":629},[201,3654,659],{"class":207},[201,3656,1985],{"class":375},[201,3658,3659],{"class":203,"line":651},[201,3660,671],{"class":228},[15,3662,3663,3664,158,3667,3670,3671,3673],{},"Не смешивайте случайно ",[24,3665,3666],{},"s.q",[24,3668,3669],{},"qtx"," внутри одной транзакции: вызов через ",[24,3672,3666],{}," пойдёт через пул вне transaction boundary.",[70,3675,3676,3686],{},[73,3677,3678],{},[76,3679,3680,3683],{},[79,3681,3682],{"align":81},"Плюсы sqlc",[79,3684,3685],{"align":81},"Минусы sqlc",[90,3687,3688,3700,3710,3718,3726],{},[76,3689,3690,3697],{},[95,3691,3692,3693,3696],{"align":81},"SQL явно лежит в ",[24,3694,3695],{},".sql"," файлах.",[95,3698,3699],{"align":81},"Динамические запросы неудобны.",[76,3701,3702,3707],{},[95,3703,3704,3705,310],{"align":81},"Меньше ручного ",[24,3706,848],{},[95,3708,3709],{"align":81},"Generated code надо обновлять.",[76,3711,3712,3715],{},[95,3713,3714],{"align":81},"Типы параметров и результата генерируются.",[95,3716,3717],{"align":81},"Сложные SQL-типы требуют overrides.",[76,3719,3720,3723],{},[95,3721,3722],{"align":81},"Запросы легко review'ить.",[95,3724,3725],{"align":81},"Domain model всё равно лучше маппить руками.",[76,3727,3728,3731],{},[95,3729,3730],{"align":81},"Хорошо ложится в adapter layer Clean Architecture.",[95,3732,3733],{"align":81},"Для больших запросов params могут быть громоздкими.",[15,3735,3736,3738,3739,3741,3742,3745,3746,3749],{},[24,3737,43],{}," не заменяет миграции. Миграции меняют реальную БД, а ",[24,3740,43],{}," читает schema, чтобы понять типы. В CI полезно запускать ",[24,3743,3744],{},"sqlc generate"," или ",[24,3747,3748],{},"sqlc vet",", а integration tests - на реальной PostgreSQL.",[62,3751],{},[65,3753,3755],{"id":3754},"squirrel-и-query-builders","Squirrel и query builders",[15,3757,3758],{},"Squirrel помогает собирать SQL из частей:",[52,3760,3762],{"className":194,"code":3761,"language":196,"meta":197,"style":58},"import sq \"github.com\u002FMasterminds\u002Fsquirrel\"\n\npsql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)\n\nquery := psql.\n    Select(\"id\", \"email\", \"name\").\n    From(\"users\").\n    Where(sq.Eq{\"deleted_at\": nil}).\n    OrderBy(\"id DESC\").\n    Limit(20)\n\nsql, args, err := query.ToSql()\nif err != nil {\n    return err\n}\n\nrows, err := pool.Query(ctx, sql, args...)\n",[24,3763,3764,3778,3782,3798,3802,3812,3835,3847,3875,3887,3899,3903,3918,3930,3937,3941,3945],{"__ignoreMap":58},[201,3765,3766,3768,3771,3773,3776],{"class":203,"line":204},[201,3767,225],{"class":207},[201,3769,3770],{"class":228}," sq ",[201,3772,404],{"class":235},[201,3774,3775],{"class":211},"github.com\u002FMasterminds\u002Fsquirrel",[201,3777,242],{"class":235},[201,3779,3780],{"class":203,"line":215},[201,3781,219],{"emptyLinePlaceholder":218},[201,3783,3784,3787,3789,3792,3795],{"class":203,"line":222},[201,3785,3786],{"class":228},"psql ",[201,3788,351],{"class":207},[201,3790,3791],{"class":228}," sq.StatementBuilder.",[201,3793,3794],{"class":211},"PlaceholderFormat",[201,3796,3797],{"class":228},"(sq.Dollar)\n",[201,3799,3800],{"class":203,"line":232},[201,3801,219],{"emptyLinePlaceholder":218},[201,3803,3804,3807,3809],{"class":203,"line":245},[201,3805,3806],{"class":228},"query ",[201,3808,351],{"class":207},[201,3810,3811],{"class":228}," psql.\n",[201,3813,3814,3817,3819,3822,3824,3827,3829,3832],{"class":203,"line":255},[201,3815,3816],{"class":211},"    Select",[201,3818,300],{"class":228},[201,3820,3821],{"class":235},"\"id\"",[201,3823,120],{"class":228},[201,3825,3826],{"class":235},"\"email\"",[201,3828,120],{"class":228},[201,3830,3831],{"class":235},"\"name\"",[201,3833,3834],{"class":228},").\n",[201,3836,3837,3840,3842,3845],{"class":203,"line":265},[201,3838,3839],{"class":211},"    From",[201,3841,300],{"class":228},[201,3843,3844],{"class":235},"\"users\"",[201,3846,3834],{"class":228},[201,3848,3849,3852,3854,3857,3859,3862,3864,3867,3869,3872],{"class":203,"line":270},[201,3850,3851],{"class":211},"    Where",[201,3853,300],{"class":228},[201,3855,3856],{"class":211},"sq",[201,3858,310],{"class":228},[201,3860,3861],{"class":211},"Eq",[201,3863,2544],{"class":228},[201,3865,3866],{"class":235},"\"deleted_at\"",[201,3868,677],{"class":228},[201,3870,3871],{"class":375},"nil",[201,3873,3874],{"class":228},"}).\n",[201,3876,3877,3880,3882,3885],{"class":203,"line":280},[201,3878,3879],{"class":211},"    OrderBy",[201,3881,300],{"class":228},[201,3883,3884],{"class":235},"\"id DESC\"",[201,3886,3834],{"class":228},[201,3888,3889,3892,3894,3897],{"class":203,"line":286},[201,3890,3891],{"class":211},"    Limit",[201,3893,300],{"class":228},[201,3895,3896],{"class":375},"20",[201,3898,283],{"class":228},[201,3900,3901],{"class":203,"line":291},[201,3902,219],{"emptyLinePlaceholder":218},[201,3904,3905,3908,3910,3913,3916],{"class":203,"line":345},[201,3906,3907],{"class":228},"sql, args, err ",[201,3909,351],{"class":207},[201,3911,3912],{"class":228}," query.",[201,3914,3915],{"class":211},"ToSql",[201,3917,586],{"class":228},[201,3919,3920,3922,3924,3926,3928],{"class":203,"line":363},[201,3921,2425],{"class":207},[201,3923,369],{"class":228},[201,3925,372],{"class":207},[201,3927,376],{"class":375},[201,3929,379],{"class":228},[201,3931,3932,3934],{"class":203,"line":382},[201,3933,659],{"class":207},[201,3935,3936],{"class":228}," err\n",[201,3938,3939],{"class":203,"line":410},[201,3940,671],{"class":228},[201,3942,3943],{"class":203,"line":416},[201,3944,219],{"emptyLinePlaceholder":218},[201,3946,3947,3950,3952,3954,3956,3959,3962],{"class":203,"line":421},[201,3948,3949],{"class":228},"rows, err ",[201,3951,351],{"class":207},[201,3953,603],{"class":228},[201,3955,1093],{"class":211},[201,3957,3958],{"class":228},"(ctx, sql, args",[201,3960,3961],{"class":207},"...",[201,3963,283],{"class":228},[15,3965,3966,3967,3970,3971,120,3974,310],{},"Для PostgreSQL важно включить dollar placeholders, иначе builder будет генерировать ",[24,3968,3969],{},"?",", а PostgreSQL ожидает ",[24,3972,3973],{},"$1",[24,3975,3976],{},"$2",[15,3978,3979,3980,120,3982,3984],{},"Squirrel полезен для optional filters, dynamic ",[24,3981,157],{},[24,3983,161],{}," по slice, админских таблиц и report query builder. Он опасен там, где в SQL попадают identifiers из пользовательского ввода.",[52,3986,3988],{"className":194,"code":3987,"language":196,"meta":197,"style":58},"func orderBy(sort string) string {\n    switch sort {\n    case \"created_at\":\n        return \"created_at DESC\"\n    case \"email\":\n        return \"email ASC\"\n    default:\n        return \"id DESC\"\n    }\n}\n\nb = b.Where(\"email = ?\", email)\nb = b.OrderBy(orderBy(f.Sort))\n",[24,3989,3990,4010,4018,4028,4035,4044,4051,4058,4065,4069,4073,4077,4098],{"__ignoreMap":58},[201,3991,3992,3994,3997,3999,4002,4004,4006,4008],{"class":203,"line":204},[201,3993,294],{"class":207},[201,3995,3996],{"class":211}," orderBy",[201,3998,300],{"class":228},[201,4000,4001],{"class":303},"sort",[201,4003,321],{"class":207},[201,4005,728],{"class":228},[201,4007,1439],{"class":207},[201,4009,379],{"class":228},[201,4011,4012,4015],{"class":203,"line":215},[201,4013,4014],{"class":207},"    switch",[201,4016,4017],{"class":228}," sort {\n",[201,4019,4020,4023,4026],{"class":203,"line":222},[201,4021,4022],{"class":207},"    case",[201,4024,4025],{"class":235}," \"created_at\"",[201,4027,2917],{"class":228},[201,4029,4030,4032],{"class":203,"line":232},[201,4031,385],{"class":207},[201,4033,4034],{"class":235}," \"created_at DESC\"\n",[201,4036,4037,4039,4042],{"class":203,"line":245},[201,4038,4022],{"class":207},[201,4040,4041],{"class":235}," \"email\"",[201,4043,2917],{"class":228},[201,4045,4046,4048],{"class":203,"line":255},[201,4047,385],{"class":207},[201,4049,4050],{"class":235}," \"email ASC\"\n",[201,4052,4053,4056],{"class":203,"line":265},[201,4054,4055],{"class":207},"    default",[201,4057,2917],{"class":228},[201,4059,4060,4062],{"class":203,"line":270},[201,4061,385],{"class":207},[201,4063,4064],{"class":235}," \"id DESC\"\n",[201,4066,4067],{"class":203,"line":280},[201,4068,413],{"class":228},[201,4070,4071],{"class":203,"line":286},[201,4072,671],{"class":228},[201,4074,4075],{"class":203,"line":291},[201,4076,219],{"emptyLinePlaceholder":218},[201,4078,4079,4082,4084,4087,4090,4092,4095],{"class":203,"line":345},[201,4080,4081],{"class":228},"b ",[201,4083,427],{"class":207},[201,4085,4086],{"class":228}," b.",[201,4088,4089],{"class":211},"Where",[201,4091,300],{"class":228},[201,4093,4094],{"class":235},"\"email = ?\"",[201,4096,4097],{"class":228},", email)\n",[201,4099,4100,4102,4104,4106,4109,4111,4114],{"class":203,"line":363},[201,4101,4081],{"class":228},[201,4103,427],{"class":207},[201,4105,4086],{"class":228},[201,4107,4108],{"class":211},"OrderBy",[201,4110,300],{"class":228},[201,4112,4113],{"class":211},"orderBy",[201,4115,4116],{"class":228},"(f.Sort))\n",[15,4118,4119],{},"Правила защиты:",[18,4121,4122,4125,4137,4140,4146],{},[21,4123,4124],{},"values всегда через placeholders;",[21,4126,4127,4128,120,4130,120,4133,4136],{},"identifiers (",[24,4129,70],{},[24,4131,4132],{},"column",[24,4134,4135],{},"direction",") только через whitelist;",[21,4138,4139],{},"не принимайте raw SQL из HTTP request;",[21,4141,4142,4145],{},[24,4143,4144],{},"sq.Expr"," используйте только с placeholders и константным SQL;",[21,4147,4148],{},"args в логах могут содержать персональные данные.",[62,4150],{},[65,4152,4154],{"id":4153},"production-паттерны-доступа-к-данным","Production-паттерны доступа к данным",[70,4156,4157,4170],{},[73,4158,4159],{},[76,4160,4161,4164,4167],{},[79,4162,4163],{"align":81},"Паттерн",[79,4165,4166],{"align":81},"Когда нужен",[79,4168,4169],{"align":81},"Главный риск",[90,4171,4172,4183,4194,4205,4216,4227],{},[76,4173,4174,4177,4180],{},[95,4175,4176],{"align":81},"Repository \u002F Store",[95,4178,4179],{"align":81},"Скрыть storage details от use case.",[95,4181,4182],{"align":81},"Интерфейс превращается в CRUD-копию таблиц.",[76,4184,4185,4188,4191],{},[95,4186,4187],{"align":81},"Unit of Work",[95,4189,4190],{"align":81},"Несколько repositories в одной транзакции.",[95,4192,4193],{"align":81},"Неочевидная transaction boundary.",[76,4195,4196,4199,4202],{},[95,4197,4198],{"align":81},"Outbox",[95,4200,4201],{"align":81},"Атомарно связать запись в БД и публикацию события.",[95,4203,4204],{"align":81},"Нужен publisher, retry и дедупликация.",[76,4206,4207,4210,4213],{},[95,4208,4209],{"align":81},"Idempotency keys",[95,4211,4212],{"align":81},"Клиент или очередь могут повторить запрос.",[95,4214,4215],{"align":81},"Ключ без scope конфликтует между пользователями.",[76,4217,4218,4221,4224],{},[95,4219,4220],{"align":81},"Keyset pagination",[95,4222,4223],{"align":81},"Большие и живые списки.",[95,4225,4226],{"align":81},"Cursor без tie-breaker пропускает или повторяет строки.",[76,4228,4229,4232,4235],{},[95,4230,4231],{"align":81},"Anti N+1",[95,4233,4234],{"align":81},"Списки с дочерними сущностями.",[95,4236,4237,4238,4241],{"align":81},"Один большой ",[24,4239,4240],{},"JOIN"," тоже может раздуть результат.",[15,4243,4244],{},"Хороший repository описывает потребность use case, а не таблицу:",[52,4246,4248],{"className":194,"code":4247,"language":196,"meta":197,"style":58},"type BillingStore interface {\n    CreateInvoice(ctx context.Context, invoice Invoice) (Invoice, error)\n    MarkInvoicePaid(ctx context.Context, invoiceID int64, paymentID string) error\n}\n",[24,4249,4250,4262,4296,4330],{"__ignoreMap":58},[201,4251,4252,4254,4257,4260],{"class":203,"line":204},[201,4253,1482],{"class":207},[201,4255,4256],{"class":211}," BillingStore",[201,4258,4259],{"class":207}," interface",[201,4261,379],{"class":228},[201,4263,4264,4267,4269,4271,4273,4275,4277,4279,4282,4285,4287,4290,4292,4294],{"class":203,"line":215},[201,4265,4266],{"class":211},"    CreateInvoice",[201,4268,300],{"class":228},[201,4270,304],{"class":303},[201,4272,307],{"class":211},[201,4274,310],{"class":228},[201,4276,313],{"class":211},[201,4278,120],{"class":228},[201,4280,4281],{"class":303},"invoice",[201,4283,4284],{"class":211}," Invoice",[201,4286,324],{"class":228},[201,4288,4289],{"class":211},"Invoice",[201,4291,120],{"class":228},[201,4293,339],{"class":207},[201,4295,283],{"class":228},[201,4297,4298,4301,4303,4305,4307,4309,4311,4313,4316,4318,4320,4323,4325,4327],{"class":203,"line":222},[201,4299,4300],{"class":211},"    MarkInvoicePaid",[201,4302,300],{"class":228},[201,4304,304],{"class":303},[201,4306,307],{"class":211},[201,4308,310],{"class":228},[201,4310,313],{"class":211},[201,4312,120],{"class":228},[201,4314,4315],{"class":303},"invoiceID",[201,4317,749],{"class":207},[201,4319,120],{"class":228},[201,4321,4322],{"class":303},"paymentID",[201,4324,321],{"class":207},[201,4326,728],{"class":228},[201,4328,4329],{"class":207},"error\n",[201,4331,4332],{"class":203,"line":232},[201,4333,671],{"class":228},[15,4335,4336],{},"Outbox:",[52,4338,4341],{"className":4339,"code":4340,"language":57,"meta":58},[55],"Transaction:\n  INSERT orders\n  INSERT outbox_events\n\nBackground publisher:\n  SELECT unpublished events\n  publish to Kafka\n  mark as published\n",[24,4342,4340],{"__ignoreMap":58},[52,4344,4346],{"className":993,"code":4345,"language":995,"meta":58,"style":58},"SELECT id, topic, key, payload\nFROM outbox_events\nWHERE published_at IS NULL\nORDER BY created_at\nLIMIT 100\nFOR UPDATE SKIP LOCKED;\n",[24,4347,4348,4361,4368,4381,4388,4395],{"__ignoreMap":58},[201,4349,4350,4352,4355,4358],{"class":203,"line":204},[201,4351,3028],{"class":207},[201,4353,4354],{"class":228}," id, topic, ",[201,4356,4357],{"class":207},"key",[201,4359,4360],{"class":228},", payload\n",[201,4362,4363,4365],{"class":203,"line":215},[201,4364,3042],{"class":207},[201,4366,4367],{"class":228}," outbox_events\n",[201,4369,4370,4372,4375,4378],{"class":203,"line":222},[201,4371,157],{"class":207},[201,4373,4374],{"class":228}," published_at ",[201,4376,4377],{"class":207},"IS",[201,4379,4380],{"class":207}," NULL\n",[201,4382,4383,4385],{"class":203,"line":232},[201,4384,3092],{"class":207},[201,4386,4387],{"class":228}," created_at\n",[201,4389,4390,4392],{"class":203,"line":245},[201,4391,3100],{"class":207},[201,4393,4394],{"class":375}," 100\n",[201,4396,4397,4400,4403,4406],{"class":203,"line":255},[201,4398,4399],{"class":207},"FOR",[201,4401,4402],{"class":207}," UPDATE",[201,4404,4405],{"class":207}," SKIP",[201,4407,4408],{"class":228}," LOCKED;\n",[15,4410,4411],{},"Idempotency:",[52,4413,4416],{"className":4414,"code":4415,"language":57,"meta":58},[55],"POST \u002Fpayments Idempotency-Key: abc\n  |\n  v\nINSERT key abc\n  |\n  +-- conflict -> вернуть сохранённый результат или \"in progress\"\n  |\n  v\nвыполнить операцию\n  |\n  v\nсохранить response\n",[24,4417,4415],{"__ignoreMap":58},[15,4419,4420],{},"Keyset pagination:",[52,4422,4424],{"className":993,"code":4423,"language":995,"meta":58,"style":58},"SELECT id, created_at, title\nFROM courses\nWHERE (created_at, id) \u003C ($1, $2)\nORDER BY created_at DESC, id DESC\nLIMIT 20;\n",[24,4425,4426,4433,4440,4462,4478],{"__ignoreMap":58},[201,4427,4428,4430],{"class":203,"line":204},[201,4429,3028],{"class":207},[201,4431,4432],{"class":228}," id, created_at, title\n",[201,4434,4435,4437],{"class":203,"line":215},[201,4436,3042],{"class":207},[201,4438,4439],{"class":228}," courses\n",[201,4441,4442,4444,4447,4450,4453,4455,4458,4460],{"class":203,"line":222},[201,4443,157],{"class":207},[201,4445,4446],{"class":228}," (created_at, id) ",[201,4448,4449],{"class":207},"\u003C",[201,4451,4452],{"class":228}," ($",[201,4454,3060],{"class":375},[201,4456,4457],{"class":228},", $",[201,4459,569],{"class":375},[201,4461,283],{"class":228},[201,4463,4464,4466,4469,4472,4475],{"class":203,"line":232},[201,4465,3092],{"class":207},[201,4467,4468],{"class":228}," created_at ",[201,4470,4471],{"class":207},"DESC",[201,4473,4474],{"class":228},", id ",[201,4476,4477],{"class":207},"DESC\n",[201,4479,4480,4482,4484],{"class":203,"line":245},[201,4481,3100],{"class":207},[201,4483,430],{"class":375},[201,4485,1016],{"class":228},[52,4487,4489],{"className":993,"code":4488,"language":995,"meta":58,"style":58},"CREATE INDEX courses_created_at_id_idx\nON courses (created_at DESC, id DESC);\n",[24,4490,4491,4502],{"__ignoreMap":58},[201,4492,4493,4496,4499],{"class":203,"line":204},[201,4494,4495],{"class":207},"CREATE",[201,4497,4498],{"class":207}," INDEX",[201,4500,4501],{"class":211}," courses_created_at_id_idx\n",[201,4503,4504,4507,4510,4512,4514,4516],{"class":203,"line":215},[201,4505,4506],{"class":207},"ON",[201,4508,4509],{"class":228}," courses (created_at ",[201,4511,4471],{"class":207},[201,4513,4474],{"class":228},[201,4515,4471],{"class":207},[201,4517,4518],{"class":228},");\n",[15,4520,4521,4522,705],{},"N+1 лечится явным batch-запросом или ",[24,4523,4240],{},[52,4525,4527],{"className":993,"code":4526,"language":995,"meta":58,"style":58},"SELECT course_id, id, title\nFROM lessons\nWHERE course_id = ANY($1::bigint[])\nORDER BY course_id, position;\n",[24,4528,4529,4536,4543,4566],{"__ignoreMap":58},[201,4530,4531,4533],{"class":203,"line":204},[201,4532,3028],{"class":207},[201,4534,4535],{"class":228}," course_id, id, title\n",[201,4537,4538,4540],{"class":203,"line":215},[201,4539,3042],{"class":207},[201,4541,4542],{"class":228}," lessons\n",[201,4544,4545,4547,4550,4552,4555,4557,4560,4563],{"class":203,"line":222},[201,4546,157],{"class":207},[201,4548,4549],{"class":228}," course_id ",[201,4551,427],{"class":207},[201,4553,4554],{"class":228}," ANY($",[201,4556,3060],{"class":375},[201,4558,4559],{"class":228},"::",[201,4561,4562],{"class":207},"bigint",[201,4564,4565],{"class":228},"[])\n",[201,4567,4568,4570],{"class":203,"line":232},[201,4569,3092],{"class":207},[201,4571,4572],{"class":228}," course_id, position;\n",[62,4574],{},[65,4576,4578],{"id":4577},"пул-соединений-и-тесты","Пул соединений и тесты",[15,4580,4581],{},"Слишком маленький пул создаёт очередь внутри приложения. Слишком большой пул перегружает PostgreSQL и увеличивает context switching.",[52,4583,4586],{"className":4584,"code":4585,"language":57,"meta":58},[55],"max_conns_per_instance =\n  floor((postgres_app_connection_budget - reserved_for_ops - job_connections)\n        \u002F max_app_instances)\n",[24,4587,4585],{"__ignoreMap":58},[15,4589,4590],{},"Если PostgreSQL выдерживает 120 app connections, а у вас 6 replicas:",[52,4592,4595],{"className":4593,"code":4594,"language":57,"meta":58},[55],"120 \u002F 6 = 20 connections на instance\n",[24,4596,4594],{"__ignoreMap":58},[15,4598,4599],{},"Что смотреть:",[18,4601,4602,4610,4616,4619],{},[21,4603,4604,4606,4607,4609],{},[24,4605,2777],{}," близко к ",[24,4608,686],{}," - пул упирается;",[21,4611,4612,4613,4615],{},"растёт ",[24,4614,2810],{}," - goroutine ждут соединение;",[21,4617,4618],{},"много idle connections - пул завышен;",[21,4620,4621],{},"PostgreSQL CPU high и queries slow - проблема может быть в индексах, locks или плане запроса.",[15,4623,4624],{},"Fleet-aware sizing проверяют вместе с deployment config. Если autoscaling может поднять 20 replicas, формула должна использовать 20, а не текущее количество pod. Для workers и batch jobs задавайте отдельные лимиты, чтобы backfill не съел все connections HTTP-сервиса.",[15,4626,4627],{},"Mock repository полезен для use case tests, но SQL проверяйте на реальной PostgreSQL. SQLite не заменяет PostgreSQL: другой dialect, planner, locks и types.",[15,4629,4630],{},"Тестовый минимум:",[18,4632,4633,4636,4639,4642,4648,4651,4654,4657,4660,4663],{},[21,4634,4635],{},"migrations применяются с нуля;",[21,4637,4638],{},"constraints реально работают;",[21,4640,4641],{},"transaction rollback;",[21,4643,4644,4647],{},[24,4645,4646],{},"ErrNoRows"," маппится в domain error;",[21,4649,4650],{},"unique violation маппится в conflict;",[21,4652,4653],{},"pagination не пропускает строки;",[21,4655,4656],{},"outbox создаётся в той же транзакции;",[21,4658,4659],{},"retry работает на искусственном serialization\u002Fdeadlock сценарии;",[21,4661,4662],{},"config validation падает на пустом DSN, неверном pool size и небезопасном TLS policy для production;",[21,4664,4665],{},"pool sizing test\u002Fdocumentation показывает общий connection budget для replicas\u002Fworkers\u002Fjobs.",[62,4667],{},[65,4669,4671],{"id":4670},"что-запомнить","Что запомнить",[18,4673,4674,4680,4683,4686,4689,4696,4699,4704,4707,4710,4716],{},[21,4675,4676,4679],{},[24,4677,4678],{},"pgxpool.Pool"," создаётся один раз на приложение и закрывается при shutdown.",[21,4681,4682],{},"Размер pool считают на весь fleet и валидируют из config на старте.",[21,4684,4685],{},"Context отменяет ожидание и запрос, но transaction boundary всё равно надо завершать явно.",[21,4687,4688],{},"После canceled statement внутри transaction обычно нужен rollback, а не продолжение бизнес-операции.",[21,4690,4691,4692,4695],{},"Для PostgreSQL ошибок используйте ",[24,4693,4694],{},"PgError.Code",", а не текст сообщения.",[21,4697,4698],{},"Retry делайте только для всей транзакции и только для retryable ошибок.",[21,4700,4701,4703],{},[24,4702,43],{}," генерирует код из SQL, но не управляет миграциями.",[21,4705,4706],{},"Squirrel защищает values через placeholders, но identifiers требуют whitelist.",[21,4708,4709],{},"Logs\u002Ftraces должны показывать operation, latency и SQLSTATE, но не PII, DSN и raw args.",[21,4711,4712,4715],{},[24,4713,4714],{},"LIMIT\u002FOFFSET"," прост, но keyset pagination лучше для больших живых списков.",[21,4717,4718],{},"Integration tests для SQL должны идти на PostgreSQL.",[62,4720],{},[65,4722,4724],{"id":4723},"практика","Практика",[4726,4727,4728,4746,4755,4761,4767,4782],"ol",{},[21,4729,4730,4731,4734,4735,677,4737,120,4740,120,4743,310],{},"Возьмите простую таблицу ",[24,4732,4733],{},"courses"," и напишите repository на ",[24,4736,37],{},[24,4738,4739],{},"Create",[24,4741,4742],{},"GetBySlug",[24,4744,4745],{},"List",[21,4747,4748,4749,158,4751,4754],{},"Добавьте обработку ",[24,4750,1448],{},[24,4752,4753],{},"PgError.Code == \"23505\""," в доменные ошибки.",[21,4756,4757,4758,4760],{},"Перепишите те же queries через ",[24,4759,43],{},", оставив ручной mapping в domain model.",[21,4762,4763,4764,310],{},"Добавьте транзакционный сценарий: создать заказ и позиции заказа через ",[24,4765,4766],{},"qtx := q.WithTx(tx)",[21,4768,4769,4770,120,4773,120,4776,4779,4780,310],{},"Напишите dynamic search через Squirrel: ",[24,4771,4772],{},"email",[24,4774,4775],{},"created_after",[24,4777,4778],{},"ids",", whitelist для ",[24,4781,4001],{},[21,4783,4784],{},"Сделайте integration test на PostgreSQL: миграции, unique constraint, rollback.",[62,4786],{},[65,4788,4790],{"id":4789},"интерактивная-практика","Интерактивная практика",[4792,4793,4797,4803,4825],"quiz",{"answer":4794,"id":4795,"xp":4796},"4","db-go-pgx-q1","10",[15,4798,4799,4800,4802],{},"Где безопаснее обрабатывать динамический ",[24,4801,3092],{}," от пользователя?",[4804,4805,4806],"template",{"v-slot:options":58},[18,4807,4808,4811,4817,4822],{},[21,4809,4810],{},"Подставить строку пользователя прямо в SQL",[21,4812,4813,4814],{},"Экранировать через ",[24,4815,4816],{},"fmt.Sprintf(\"%q\", input)",[21,4818,4819,4820],{},"Передать имя колонки как ",[24,4821,3973],{},[21,4823,4824],{},"Выбрать SQL identifier из whitelist",[4804,4826,4827],{"v-slot:explanation":58},[15,4828,4829],{},"Параметры защищают значения, но не SQL identifiers. Для колонок, direction и raw fragments нужен whitelist.",[4831,4832,4836,4839,4982],"predict",{"answer":4833,"id":4834,"xp":4835},"not-found\\nconflict","db-go-pgx-p1","15",[15,4837,4838],{},"Что выведет программа?",[4804,4840,4841],{"v-slot:code":58},[52,4842,4844],{"className":194,"code":4843,"language":196,"meta":58,"style":58},"package main\n\nimport \"fmt\"\n\nfunc DomainError(code string) string {\n    if code == \"23505\" {\n        return \"conflict\"\n    }\n    return \"not-found\"\n}\n\nfunc main() {\n    fmt.Println(DomainError(\"no-rows\"))\n    fmt.Println(DomainError(\"23505\"))\n}\n",[24,4845,4846,4853,4857,4868,4872,4891,4905,4912,4916,4923,4927,4931,4940,4961,4978],{"__ignoreMap":58},[201,4847,4848,4850],{"class":203,"line":204},[201,4849,208],{"class":207},[201,4851,4852],{"class":211}," main\n",[201,4854,4855],{"class":203,"line":215},[201,4856,219],{"emptyLinePlaceholder":218},[201,4858,4859,4861,4864,4866],{"class":203,"line":222},[201,4860,225],{"class":207},[201,4862,4863],{"class":235}," \"",[201,4865,250],{"class":211},[201,4867,242],{"class":235},[201,4869,4870],{"class":203,"line":232},[201,4871,219],{"emptyLinePlaceholder":218},[201,4873,4874,4876,4879,4881,4883,4885,4887,4889],{"class":203,"line":245},[201,4875,294],{"class":207},[201,4877,4878],{"class":211}," DomainError",[201,4880,300],{"class":228},[201,4882,24],{"class":303},[201,4884,321],{"class":207},[201,4886,728],{"class":228},[201,4888,1439],{"class":207},[201,4890,379],{"class":228},[201,4892,4893,4895,4898,4900,4903],{"class":203,"line":255},[201,4894,366],{"class":207},[201,4896,4897],{"class":228}," code ",[201,4899,1851],{"class":207},[201,4901,4902],{"class":235}," \"23505\"",[201,4904,379],{"class":228},[201,4906,4907,4909],{"class":203,"line":265},[201,4908,385],{"class":207},[201,4910,4911],{"class":235}," \"conflict\"\n",[201,4913,4914],{"class":203,"line":270},[201,4915,413],{"class":228},[201,4917,4918,4920],{"class":203,"line":280},[201,4919,659],{"class":207},[201,4921,4922],{"class":235}," \"not-found\"\n",[201,4924,4925],{"class":203,"line":286},[201,4926,671],{"class":228},[201,4928,4929],{"class":203,"line":291},[201,4930,219],{"emptyLinePlaceholder":218},[201,4932,4933,4935,4938],{"class":203,"line":345},[201,4934,294],{"class":207},[201,4936,4937],{"class":211}," main",[201,4939,1253],{"class":228},[201,4941,4942,4945,4948,4950,4953,4955,4958],{"class":203,"line":363},[201,4943,4944],{"class":228},"    fmt.",[201,4946,4947],{"class":211},"Println",[201,4949,300],{"class":228},[201,4951,4952],{"class":211},"DomainError",[201,4954,300],{"class":228},[201,4956,4957],{"class":235},"\"no-rows\"",[201,4959,4960],{"class":228},"))\n",[201,4962,4963,4965,4967,4969,4971,4973,4976],{"class":203,"line":382},[201,4964,4944],{"class":228},[201,4966,4947],{"class":211},[201,4968,300],{"class":228},[201,4970,4952],{"class":211},[201,4972,300],{"class":228},[201,4974,4975],{"class":235},"\"23505\"",[201,4977,4960],{"class":228},[201,4979,4980],{"class":203,"line":410},[201,4981,671],{"class":228},[4804,4983,4984],{"v-slot:hint":58},[15,4985,4986,4988,4989,4991],{},[24,4987,2087],{}," - unique violation. ",[24,4990,4646],{}," обычно маппят в not found.",[4993,4994,4997,5015,5144],"code-task",{"expected":4995,"id":4996,"xp":3896},"rollback\\nretry-tx\\nwhitelist","db-go-pgx-ct1",[15,4998,4999,5000,5003,5004,5007,5008,5011,5012,310],{},"Реализуй ",[24,5001,5002],{},"RepositoryRule",": canceled statement внутри transaction требует ",[24,5005,5006],{},"rollback","; retryable SQLSTATE повторяет всю транзакцию как ",[24,5009,5010],{},"retry-tx","; динамическая сортировка требует ",[24,5013,5014],{},"whitelist",[4804,5016,5017],{"v-slot:template":58},[52,5018,5020],{"className":194,"code":5019,"language":196,"meta":58,"style":58},"package main\n\nimport \"fmt\"\n\nfunc RepositoryRule(signal string) string {\n    return \"todo\"\n}\n\nfunc main() {\n    fmt.Println(RepositoryRule(\"ctx-canceled-in-tx\"))\n    fmt.Println(RepositoryRule(\"40001\"))\n    fmt.Println(RepositoryRule(\"dynamic-sort\"))\n}\n",[24,5021,5022,5028,5032,5042,5046,5066,5073,5077,5081,5089,5106,5123,5140],{"__ignoreMap":58},[201,5023,5024,5026],{"class":203,"line":204},[201,5025,208],{"class":207},[201,5027,4852],{"class":211},[201,5029,5030],{"class":203,"line":215},[201,5031,219],{"emptyLinePlaceholder":218},[201,5033,5034,5036,5038,5040],{"class":203,"line":222},[201,5035,225],{"class":207},[201,5037,4863],{"class":235},[201,5039,250],{"class":211},[201,5041,242],{"class":235},[201,5043,5044],{"class":203,"line":232},[201,5045,219],{"emptyLinePlaceholder":218},[201,5047,5048,5050,5053,5055,5058,5060,5062,5064],{"class":203,"line":245},[201,5049,294],{"class":207},[201,5051,5052],{"class":211}," RepositoryRule",[201,5054,300],{"class":228},[201,5056,5057],{"class":303},"signal",[201,5059,321],{"class":207},[201,5061,728],{"class":228},[201,5063,1439],{"class":207},[201,5065,379],{"class":228},[201,5067,5068,5070],{"class":203,"line":255},[201,5069,659],{"class":207},[201,5071,5072],{"class":235}," \"todo\"\n",[201,5074,5075],{"class":203,"line":265},[201,5076,671],{"class":228},[201,5078,5079],{"class":203,"line":270},[201,5080,219],{"emptyLinePlaceholder":218},[201,5082,5083,5085,5087],{"class":203,"line":280},[201,5084,294],{"class":207},[201,5086,4937],{"class":211},[201,5088,1253],{"class":228},[201,5090,5091,5093,5095,5097,5099,5101,5104],{"class":203,"line":286},[201,5092,4944],{"class":228},[201,5094,4947],{"class":211},[201,5096,300],{"class":228},[201,5098,5002],{"class":211},[201,5100,300],{"class":228},[201,5102,5103],{"class":235},"\"ctx-canceled-in-tx\"",[201,5105,4960],{"class":228},[201,5107,5108,5110,5112,5114,5116,5118,5121],{"class":203,"line":291},[201,5109,4944],{"class":228},[201,5111,4947],{"class":211},[201,5113,300],{"class":228},[201,5115,5002],{"class":211},[201,5117,300],{"class":228},[201,5119,5120],{"class":235},"\"40001\"",[201,5122,4960],{"class":228},[201,5124,5125,5127,5129,5131,5133,5135,5138],{"class":203,"line":345},[201,5126,4944],{"class":228},[201,5128,4947],{"class":211},[201,5130,300],{"class":228},[201,5132,5002],{"class":211},[201,5134,300],{"class":228},[201,5136,5137],{"class":235},"\"dynamic-sort\"",[201,5139,4960],{"class":228},[201,5141,5142],{"class":203,"line":363},[201,5143,671],{"class":228},[4804,5145,5146],{"v-slot:hints":58},[18,5147,5148,5151,5156],{},[21,5149,5150],{},"Context timeout не закрывает бизнес-границу сам по себе.",[21,5152,5153,5155],{},[24,5154,2126],{}," нельзя чинить повтором одного statement.",[21,5157,5158],{},"Squirrel параметризует values, но identifiers всё равно выбирайте сами.",[62,5160],{},[65,5162,5164],{"id":5163},"что-спросят-на-собеседовании","Что спросят на собеседовании",[18,5166,5167,5175,5183,5188,5193,5196,5199,5205,5208,5214],{},[21,5168,5169,5170,5172,5173,3969],{},"Чем ",[24,5171,26],{}," отличается от нативного ",[24,5174,33],{},[21,5176,5177,5178,158,5180,5182],{},"Почему ",[24,5179,178],{},[24,5181,181],{}," не надо создавать на каждый request?",[21,5184,5185,5186,3969],{},"Что случится, если не вызвать ",[24,5187,1419],{},[21,5189,5190,5191,3969],{},"Почему context timeout не заменяет ",[24,5192,1055],{},[21,5194,5195],{},"Какие SQLSTATE стоит маппить в приложении?",[21,5197,5198],{},"Почему retry должен повторять всю транзакцию?",[21,5200,5201,5202,5204],{},"Что делает ",[24,5203,43],{},", а чего он не делает?",[21,5206,5207],{},"Где Squirrel помогает, а где создаёт риск SQL injection?",[21,5209,5210,5211,3969],{},"Почему keyset pagination стабильнее ",[24,5212,5213],{},"OFFSET",[21,5215,5216],{},"Зачем нужен outbox pattern?",[62,5218],{},[65,5220,5222],{"id":5221},"источники","Источники",[18,5224,5225,5234,5241,5248,5255,5262,5269,5276,5283],{},[21,5226,5227],{},[5228,5229,5233],"a",{"href":5230,"rel":5231},"https:\u002F\u002Fgo.dev\u002Fdoc\u002Fdatabase\u002F",[5232],"nofollow","Go: Accessing relational databases",[21,5235,5236],{},[5228,5237,5240],{"href":5238,"rel":5239},"https:\u002F\u002Fgo.dev\u002Fdoc\u002Fdatabase\u002Fexecute-transactions",[5232],"Go: Executing transactions",[21,5242,5243],{},[5228,5244,5247],{"href":5245,"rel":5246},"https:\u002F\u002Fpkg.go.dev\u002Fgithub.com\u002Fjackc\u002Fpgx\u002Fv5",[5232],"pgx package documentation",[21,5249,5250],{},[5228,5251,5254],{"href":5252,"rel":5253},"https:\u002F\u002Fpkg.go.dev\u002Fgithub.com\u002Fjackc\u002Fpgx\u002Fv5\u002Fpgxpool",[5232],"pgxpool package documentation",[21,5256,5257],{},[5228,5258,5261],{"href":5259,"rel":5260},"https:\u002F\u002Fpkg.go.dev\u002Fgithub.com\u002Fjackc\u002Fpgx\u002Fv5\u002Ftracelog",[5232],"pgx tracelog package documentation",[21,5263,5264],{},[5228,5265,5268],{"href":5266,"rel":5267},"https:\u002F\u002Fdocs.sqlc.dev\u002F",[5232],"sqlc documentation",[21,5270,5271],{},[5228,5272,5275],{"href":5273,"rel":5274},"https:\u002F\u002Fgithub.com\u002FMasterminds\u002Fsquirrel",[5232],"Squirrel README",[21,5277,5278],{},[5228,5279,5282],{"href":5280,"rel":5281},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Ftransaction-iso.html",[5232],"PostgreSQL Transaction Isolation",[21,5284,5285],{},[5228,5286,5289],{"href":5287,"rel":5288},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fmvcc-serialization-failure-handling.html",[5232],"PostgreSQL Serialization Failure Handling",[5291,5292,5293],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}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 .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}",{"title":58,"searchDepth":215,"depth":215,"links":5295},[5296,5297,5298,5299,5300,5301,5302,5303,5304,5305,5306,5307,5308,5309,5310],{"id":67,"depth":215,"text":68},{"id":187,"depth":215,"text":188},{"id":1084,"depth":215,"text":1085},{"id":1611,"depth":215,"text":1612},{"id":2272,"depth":215,"text":2273},{"id":2686,"depth":215,"text":2687},{"id":43,"depth":215,"text":43},{"id":3754,"depth":215,"text":3755},{"id":4153,"depth":215,"text":4154},{"id":4577,"depth":215,"text":4578},{"id":4670,"depth":215,"text":4671},{"id":4723,"depth":215,"text":4724},{"id":4789,"depth":215,"text":4790},{"id":5163,"depth":215,"text":5164},{"id":5221,"depth":215,"text":5222},1781022066732]