[{"data":1,"prerenderedAt":5510},["ShallowReactive",2],{"page-\u002Fsql\u002F22-stored-procedures-and-functions":3},{"id":4,"title":5,"body":6,"description":5503,"extension":5504,"meta":5505,"navigation":578,"path":5506,"seo":5507,"stem":5508,"__hash__":5509},"content\u002Fsql\u002F22-stored-procedures-and-functions.md","22 — Stored Procedures & Functions",{"type":7,"value":8,"toc":5479},"minimark",[9,13,43,48,176,180,421,425,980,984,1211,1215,1647,1651,1657,1724,2019,2024,2181,2185,2301,2305,2533,2563,2567,2964,2968,3125,3129,3209,3213,3216,3846,3850,4175,4179,4357,4361,4666,4670,4797,4801,4954,4958,5144,5148,5154,5258,5475],[10,11,5],"h1",{"id":12},"_22-stored-procedures-functions",[14,15,16,17,21,22,25,26,30,31,34,35,38,39,42],"p",{},"PostgreSQL has two server-side routine types: ",[18,19,20],"strong",{},"functions"," (callable from expressions, return scalars\u002Fsets\u002Ftables, can't control transactions) and ",[18,23,24],{},"procedures"," (callable only via ",[27,28,29],"code",{},"CALL",", can ",[27,32,33],{},"COMMIT","\u002F",[27,36,37],{},"ROLLBACK"," internally). Both are usually written in ",[18,40,41],{},"PL\u002FpgSQL"," — a block-structured procedural language with variables, conditionals, loops, and exception handling.",[44,45,47],"h2",{"id":46},"function-vs-procedure-the-decision-matrix","FUNCTION vs PROCEDURE — the decision matrix",[49,50,51,67],"table",{},[52,53,54],"thead",{},[55,56,57,61,64],"tr",{},[58,59,60],"th",{},"Feature",[58,62,63],{},"Function",[58,65,66],{},"Procedure",[68,69,70,93,112,136,150,165],"tbody",{},[55,71,72,76,87],{},[73,74,75],"td",{},"Called from",[73,77,78,79,82,83,86],{},"Any expression (",[27,80,81],{},"SELECT my_func(...)",", in ",[27,84,85],{},"WHERE",", in index)",[73,88,89,90],{},"Only ",[27,91,92],{},"CALL my_proc(...)",[55,94,95,98,105],{},[73,96,97],{},"Returns",[73,99,100,101,104],{},"Must return a value (scalar, set, ",[27,102,103],{},"TABLE(...)",")",[73,106,107,108,111],{},"No row return (can have ",[27,109,110],{},"INOUT"," args read back)",[55,113,114,117,128],{},[73,115,116],{},"Transactions",[73,118,119,120,123,124,34,126],{},"Runs in caller's transaction — ",[18,121,122],{},"cannot"," ",[27,125,33],{},[27,127,37],{},[73,129,130,131,34,133,135],{},"Can ",[27,132,33],{},[27,134,37],{}," mid-execution",[55,137,138,141,147],{},[73,139,140],{},"Use in index",[73,142,143,144,104],{},"Yes (if ",[27,145,146],{},"IMMUTABLE",[73,148,149],{},"No",[55,151,152,160,163],{},[73,153,154,155,34,157],{},"Use in ",[27,156,85],{},[27,158,159],{},"SELECT",[73,161,162],{},"Yes",[73,164,149],{},[55,166,167,170,173],{},[73,168,169],{},"Typical use",[73,171,172],{},"Computation, data retrieval, encapsulating a query",[73,174,175],{},"Multi-step ETL with per-batch transaction control",[44,177,179],{"id":178},"plpgsql-basics-the-block-structure","PL\u002FpgSQL Basics — the block structure",[181,182,184],"code-wrapper",{"language":183},"sql",[185,186,190],"pre",{"className":187,"code":188,"language":183,"meta":189,"style":189},"language-sql shiki shiki-themes github-light github-dark","CREATE OR REPLACE FUNCTION example(input TEXT) RETURNS TEXT AS $$\nDECLARE                          -- DECLARE section (optional): variables, types\n  counter   INTEGER := 0;        -- initialized; without :=, defaults to NULL\n  name      TEXT;                -- untyped-init → NULL\n  ids       BIGINT[];            -- array type\n  row       RECORD;              -- generic row — structure assigned at first use\n  user_row  users%ROWTYPE;       -- anchored to a table's row type (auto-tracks schema changes)\n  user_id   users.id%TYPE;       -- anchored to a column's type\n  found     BOOLEAN;             -- set by SELECT INTO \u002F RETURN\nBEGIN                           -- executable section\n  -- PL\u002FpgSQL is a block-structured language: DECLARE ... BEGIN ... EXCEPTION ... END\n  -- Variables live for the function call (not persistent across calls).\n  -- %ROWTYPE and %TYPE anchor to the schema — if users.id changes from int to bigint,\n  -- the function's user_id variable type updates automatically on next function call.\n  RETURN input;\nEND;\n$$ LANGUAGE plpgsql;\n","",[27,191,192,230,240,265,280,295,307,316,343,358,367,373,379,385,391,400,409],{"__ignoreMap":189},[193,194,197,201,204,208,212,215,218,221,224,227],"span",{"class":195,"line":196},"line",1,[193,198,200],{"class":199},"svdQ7","CREATE OR REPLACE",[193,202,203],{"class":199}," FUNCTION",[193,205,207],{"class":206},"sIsaT"," example",[193,209,211],{"class":210},"ssxIu","(input ",[193,213,214],{"class":199},"TEXT",[193,216,217],{"class":210},") ",[193,219,220],{"class":199},"RETURNS",[193,222,223],{"class":199}," TEXT",[193,225,226],{"class":199}," AS",[193,228,229],{"class":210}," $$\n",[193,231,233,236],{"class":195,"line":232},2,[193,234,235],{"class":199},"DECLARE",[193,237,239],{"class":238},"sdCPZ","                          -- DECLARE section (optional): variables, types\n",[193,241,243,246,249,252,255,259,262],{"class":195,"line":242},3,[193,244,245],{"class":199},"  counter",[193,247,248],{"class":199},"   INTEGER",[193,250,251],{"class":210}," :",[193,253,254],{"class":199},"=",[193,256,258],{"class":257},"snvgF"," 0",[193,260,261],{"class":210},";        ",[193,263,264],{"class":238},"-- initialized; without :=, defaults to NULL\n",[193,266,268,271,274,277],{"class":195,"line":267},4,[193,269,270],{"class":199},"  name",[193,272,273],{"class":199},"      TEXT",[193,275,276],{"class":210},";                ",[193,278,279],{"class":238},"-- untyped-init → NULL\n",[193,281,283,286,289,292],{"class":195,"line":282},5,[193,284,285],{"class":210},"  ids       ",[193,287,288],{"class":199},"BIGINT",[193,290,291],{"class":210},"[];            ",[193,293,294],{"class":238},"-- array type\n",[193,296,298,301,304],{"class":195,"line":297},6,[193,299,300],{"class":199},"  row",[193,302,303],{"class":210},"       RECORD;              ",[193,305,306],{"class":238},"-- generic row — structure assigned at first use\n",[193,308,310,313],{"class":195,"line":309},7,[193,311,312],{"class":210},"  user_row  users%ROWTYPE;       ",[193,314,315],{"class":238},"-- anchored to a table's row type (auto-tracks schema changes)\n",[193,317,319,322,325,328,331,334,337,340],{"class":195,"line":318},8,[193,320,321],{"class":210},"  user_id   ",[193,323,324],{"class":257},"users",[193,326,327],{"class":210},".",[193,329,330],{"class":257},"id",[193,332,333],{"class":210},"%",[193,335,336],{"class":199},"TYPE",[193,338,339],{"class":210},";       ",[193,341,342],{"class":238},"-- anchored to a column's type\n",[193,344,346,349,352,355],{"class":195,"line":345},9,[193,347,348],{"class":210},"  found     ",[193,350,351],{"class":199},"BOOLEAN",[193,353,354],{"class":210},";             ",[193,356,357],{"class":238},"-- set by SELECT INTO \u002F RETURN\n",[193,359,361,364],{"class":195,"line":360},10,[193,362,363],{"class":199},"BEGIN",[193,365,366],{"class":238},"                           -- executable section\n",[193,368,370],{"class":195,"line":369},11,[193,371,372],{"class":238},"  -- PL\u002FpgSQL is a block-structured language: DECLARE ... BEGIN ... EXCEPTION ... END\n",[193,374,376],{"class":195,"line":375},12,[193,377,378],{"class":238},"  -- Variables live for the function call (not persistent across calls).\n",[193,380,382],{"class":195,"line":381},13,[193,383,384],{"class":238},"  -- %ROWTYPE and %TYPE anchor to the schema — if users.id changes from int to bigint,\n",[193,386,388],{"class":195,"line":387},14,[193,389,390],{"class":238},"  -- the function's user_id variable type updates automatically on next function call.\n",[193,392,394,397],{"class":195,"line":393},15,[193,395,396],{"class":199},"  RETURN",[193,398,399],{"class":210}," input;\n",[193,401,403,406],{"class":195,"line":402},16,[193,404,405],{"class":199},"END",[193,407,408],{"class":210},";\n",[193,410,412,415,418],{"class":195,"line":411},17,[193,413,414],{"class":210},"$$ ",[193,416,417],{"class":199},"LANGUAGE",[193,419,420],{"class":210}," plpgsql;\n",[44,422,424],{"id":423},"control-flow-if-case-loop-for-while","Control Flow — IF, CASE, LOOP, FOR, WHILE",[181,426,427],{"language":183},[185,428,430],{"className":187,"code":429,"language":183,"meta":189,"style":189},"CREATE OR REPLACE FUNCTION control_flow_demo(n INTEGER) RETURNS TEXT AS $$\nDECLARE\n  i INTEGER := 0;\n  result TEXT;\nBEGIN\n  -- IF \u002F ELSIF \u002F ELSE\n  IF n > 0 THEN\n    result := 'positive';\n  ELSIF n \u003C 0 THEN\n    result := 'negative';\n  ELSE\n    result := 'zero';\n  END IF;\n\n  -- CASE (expression form, like SQL CASE)\n  result := CASE\n    WHEN n > 100 THEN 'big'\n    WHEN n > 10  THEN 'medium'\n    ELSE 'small'\n  END;\n\n  -- Simple LOOP with EXIT\u002FCONTINUE\n  i := 0;\n  LOOP\n    i := i + 1;\n    EXIT WHEN i >= n;             -- breaks the loop\n    CONTINUE WHEN i % 2 = 0;      -- skips even iterations (goes to next i)\n    -- odd-iteration logic here\n  END LOOP;\n\n  -- WHILE (condition checked at loop top)\n  WHILE i \u003C n * 2 LOOP\n    i := i + 1;\n  END LOOP;\n\n  -- FOR (integer range — inclusive bounds)\n  FOR i IN 1..10 LOOP             -- 1 to 10 inclusive (REVERSE 10..1 for descending)\n    -- i is loop-local; cannot be assigned inside the loop\n    NULL;\n  END LOOP;\n\n  -- FOR (query result — cursor-like iteration)\n  FOR result IN\n    SELECT name FROM users WHERE active ORDER BY name\n  LOOP\n    -- `result` is a RECORD; result.name available\n    RAISE NOTICE 'User: %', result;\n  END LOOP;\n\n  RETURN result;\nEND;\n$$ LANGUAGE plpgsql;\n",[27,431,432,457,462,477,486,491,496,512,525,537,548,553,564,574,580,585,595,613,631,640,647,652,658,670,676,695,715,741,747,757,762,768,789,804,813,818,824,848,854,862,871,876,882,893,919,924,930,942,951,956,964,971],{"__ignoreMap":189},[193,433,434,436,438,441,444,447,449,451,453,455],{"class":195,"line":196},[193,435,200],{"class":199},[193,437,203],{"class":199},[193,439,440],{"class":206}," control_flow_demo",[193,442,443],{"class":210},"(n ",[193,445,446],{"class":199},"INTEGER",[193,448,217],{"class":210},[193,450,220],{"class":199},[193,452,223],{"class":199},[193,454,226],{"class":199},[193,456,229],{"class":210},[193,458,459],{"class":195,"line":232},[193,460,461],{"class":199},"DECLARE\n",[193,463,464,467,469,471,473,475],{"class":195,"line":242},[193,465,466],{"class":210},"  i ",[193,468,446],{"class":199},[193,470,251],{"class":210},[193,472,254],{"class":199},[193,474,258],{"class":257},[193,476,408],{"class":210},[193,478,479,482,484],{"class":195,"line":267},[193,480,481],{"class":210},"  result ",[193,483,214],{"class":199},[193,485,408],{"class":210},[193,487,488],{"class":195,"line":282},[193,489,490],{"class":199},"BEGIN\n",[193,492,493],{"class":195,"line":297},[193,494,495],{"class":238},"  -- IF \u002F ELSIF \u002F ELSE\n",[193,497,498,501,504,507,509],{"class":195,"line":309},[193,499,500],{"class":199},"  IF",[193,502,503],{"class":210}," n ",[193,505,506],{"class":199},">",[193,508,258],{"class":257},[193,510,511],{"class":199}," THEN\n",[193,513,514,517,519,523],{"class":195,"line":318},[193,515,516],{"class":210},"    result :",[193,518,254],{"class":199},[193,520,522],{"class":521},"sJ6F3"," 'positive'",[193,524,408],{"class":210},[193,526,527,530,533,535],{"class":195,"line":345},[193,528,529],{"class":210},"  ELSIF n ",[193,531,532],{"class":199},"\u003C",[193,534,258],{"class":257},[193,536,511],{"class":199},[193,538,539,541,543,546],{"class":195,"line":360},[193,540,516],{"class":210},[193,542,254],{"class":199},[193,544,545],{"class":521}," 'negative'",[193,547,408],{"class":210},[193,549,550],{"class":195,"line":369},[193,551,552],{"class":199},"  ELSE\n",[193,554,555,557,559,562],{"class":195,"line":375},[193,556,516],{"class":210},[193,558,254],{"class":199},[193,560,561],{"class":521}," 'zero'",[193,563,408],{"class":210},[193,565,566,569,572],{"class":195,"line":381},[193,567,568],{"class":199},"  END",[193,570,571],{"class":199}," IF",[193,573,408],{"class":210},[193,575,576],{"class":195,"line":387},[193,577,579],{"emptyLinePlaceholder":578},true,"\n",[193,581,582],{"class":195,"line":393},[193,583,584],{"class":238},"  -- CASE (expression form, like SQL CASE)\n",[193,586,587,590,592],{"class":195,"line":402},[193,588,589],{"class":210},"  result :",[193,591,254],{"class":199},[193,593,594],{"class":199}," CASE\n",[193,596,597,600,602,604,607,610],{"class":195,"line":411},[193,598,599],{"class":199},"    WHEN",[193,601,503],{"class":210},[193,603,506],{"class":199},[193,605,606],{"class":257}," 100",[193,608,609],{"class":199}," THEN",[193,611,612],{"class":521}," 'big'\n",[193,614,616,618,620,622,625,628],{"class":195,"line":615},18,[193,617,599],{"class":199},[193,619,503],{"class":210},[193,621,506],{"class":199},[193,623,624],{"class":257}," 10",[193,626,627],{"class":199},"  THEN",[193,629,630],{"class":521}," 'medium'\n",[193,632,634,637],{"class":195,"line":633},19,[193,635,636],{"class":199},"    ELSE",[193,638,639],{"class":521}," 'small'\n",[193,641,643,645],{"class":195,"line":642},20,[193,644,568],{"class":199},[193,646,408],{"class":210},[193,648,650],{"class":195,"line":649},21,[193,651,579],{"emptyLinePlaceholder":578},[193,653,655],{"class":195,"line":654},22,[193,656,657],{"class":238},"  -- Simple LOOP with EXIT\u002FCONTINUE\n",[193,659,661,664,666,668],{"class":195,"line":660},23,[193,662,663],{"class":210},"  i :",[193,665,254],{"class":199},[193,667,258],{"class":257},[193,669,408],{"class":210},[193,671,673],{"class":195,"line":672},24,[193,674,675],{"class":199},"  LOOP\n",[193,677,679,682,684,687,690,693],{"class":195,"line":678},25,[193,680,681],{"class":210},"    i :",[193,683,254],{"class":199},[193,685,686],{"class":210}," i ",[193,688,689],{"class":199},"+",[193,691,692],{"class":257}," 1",[193,694,408],{"class":210},[193,696,698,701,704,706,709,712],{"class":195,"line":697},26,[193,699,700],{"class":210},"    EXIT ",[193,702,703],{"class":199},"WHEN",[193,705,686],{"class":210},[193,707,708],{"class":199},">=",[193,710,711],{"class":210}," n;             ",[193,713,714],{"class":238},"-- breaks the loop\n",[193,716,718,721,724,727,730,733,735,738],{"class":195,"line":717},27,[193,719,720],{"class":199},"    CONTINUE",[193,722,723],{"class":199}," WHEN",[193,725,726],{"class":210}," i % ",[193,728,729],{"class":257},"2",[193,731,732],{"class":199}," =",[193,734,258],{"class":257},[193,736,737],{"class":210},";      ",[193,739,740],{"class":238},"-- skips even iterations (goes to next i)\n",[193,742,744],{"class":195,"line":743},28,[193,745,746],{"class":238},"    -- odd-iteration logic here\n",[193,748,750,752,755],{"class":195,"line":749},29,[193,751,568],{"class":199},[193,753,754],{"class":199}," LOOP",[193,756,408],{"class":210},[193,758,760],{"class":195,"line":759},30,[193,761,579],{"emptyLinePlaceholder":578},[193,763,765],{"class":195,"line":764},31,[193,766,767],{"class":238},"  -- WHILE (condition checked at loop top)\n",[193,769,771,774,776,778,780,783,786],{"class":195,"line":770},32,[193,772,773],{"class":199},"  WHILE",[193,775,686],{"class":210},[193,777,532],{"class":199},[193,779,503],{"class":210},[193,781,782],{"class":199},"*",[193,784,785],{"class":257}," 2",[193,787,788],{"class":199}," LOOP\n",[193,790,792,794,796,798,800,802],{"class":195,"line":791},33,[193,793,681],{"class":210},[193,795,254],{"class":199},[193,797,686],{"class":210},[193,799,689],{"class":199},[193,801,692],{"class":257},[193,803,408],{"class":210},[193,805,807,809,811],{"class":195,"line":806},34,[193,808,568],{"class":199},[193,810,754],{"class":199},[193,812,408],{"class":210},[193,814,816],{"class":195,"line":815},35,[193,817,579],{"emptyLinePlaceholder":578},[193,819,821],{"class":195,"line":820},36,[193,822,823],{"class":238},"  -- FOR (integer range — inclusive bounds)\n",[193,825,827,830,832,835,837,840,843,845],{"class":195,"line":826},37,[193,828,829],{"class":199},"  FOR",[193,831,686],{"class":210},[193,833,834],{"class":199},"IN",[193,836,692],{"class":257},[193,838,839],{"class":210},"..",[193,841,842],{"class":257},"10",[193,844,754],{"class":199},[193,846,847],{"class":238},"             -- 1 to 10 inclusive (REVERSE 10..1 for descending)\n",[193,849,851],{"class":195,"line":850},38,[193,852,853],{"class":238},"    -- i is loop-local; cannot be assigned inside the loop\n",[193,855,857,860],{"class":195,"line":856},39,[193,858,859],{"class":199},"    NULL",[193,861,408],{"class":210},[193,863,865,867,869],{"class":195,"line":864},40,[193,866,568],{"class":199},[193,868,754],{"class":199},[193,870,408],{"class":210},[193,872,874],{"class":195,"line":873},41,[193,875,579],{"emptyLinePlaceholder":578},[193,877,879],{"class":195,"line":878},42,[193,880,881],{"class":238},"  -- FOR (query result — cursor-like iteration)\n",[193,883,885,887,890],{"class":195,"line":884},43,[193,886,829],{"class":199},[193,888,889],{"class":210}," result ",[193,891,892],{"class":199},"IN\n",[193,894,896,899,902,905,908,910,913,916],{"class":195,"line":895},44,[193,897,898],{"class":199},"    SELECT",[193,900,901],{"class":199}," name",[193,903,904],{"class":199}," FROM",[193,906,907],{"class":210}," users ",[193,909,85],{"class":199},[193,911,912],{"class":210}," active ",[193,914,915],{"class":199},"ORDER BY",[193,917,918],{"class":199}," name\n",[193,920,922],{"class":195,"line":921},45,[193,923,675],{"class":199},[193,925,927],{"class":195,"line":926},46,[193,928,929],{"class":238},"    -- `result` is a RECORD; result.name available\n",[193,931,933,936,939],{"class":195,"line":932},47,[193,934,935],{"class":210},"    RAISE NOTICE ",[193,937,938],{"class":521},"'User: %'",[193,940,941],{"class":210},", result;\n",[193,943,945,947,949],{"class":195,"line":944},48,[193,946,568],{"class":199},[193,948,754],{"class":199},[193,950,408],{"class":210},[193,952,954],{"class":195,"line":953},49,[193,955,579],{"emptyLinePlaceholder":578},[193,957,959,961],{"class":195,"line":958},50,[193,960,396],{"class":199},[193,962,963],{"class":210}," result;\n",[193,965,967,969],{"class":195,"line":966},51,[193,968,405],{"class":199},[193,970,408],{"class":210},[193,972,974,976,978],{"class":195,"line":973},52,[193,975,414],{"class":210},[193,977,417],{"class":199},[193,979,420],{"class":210},[44,981,983],{"id":982},"function-parameters-in-out-inout-default","Function Parameters — IN, OUT, INOUT, DEFAULT",[181,985,986],{"language":183},[185,987,989],{"className":187,"code":988,"language":183,"meta":189,"style":189},"-- IN: input only (default). OUT: output only (returned to caller).\n-- INOUT: both input and output. DEFAULT: optional parameter.\nCREATE OR REPLACE FUNCTION split_name(\n  full_name TEXT,                 -- IN (default direction)\n  OUT first TEXT,                 -- OUT: returned, not passed by caller\n  OUT last TEXT,\n  separator TEXT DEFAULT ' '      -- DEFAULT: caller can omit\n) AS $$\nBEGIN\n  -- split full_name on first occurrence of separator\n  first := split_part(full_name, separator, 1);\n  last  := split_part(full_name, separator, 2);\n  -- No RETURN needed — OUT params are the return value (composite type)\nEND;\n$$ LANGUAGE plpgsql IMMUTABLE;\n\n-- Call: returns a composite (first, last)\nSELECT * FROM split_name('Ada Lovelace');\n--  first |   last\n-- -------+----------\n--  Ada   | Lovelace\n\nSELECT * FROM split_name('Ada-Lovelace', '-');  -- uses the DEFAULT param\n",[27,990,991,996,1001,1013,1026,1041,1053,1069,1078,1082,1087,1105,1121,1126,1132,1141,1145,1150,1167,1172,1177,1182,1186],{"__ignoreMap":189},[193,992,993],{"class":195,"line":196},[193,994,995],{"class":238},"-- IN: input only (default). OUT: output only (returned to caller).\n",[193,997,998],{"class":195,"line":232},[193,999,1000],{"class":238},"-- INOUT: both input and output. DEFAULT: optional parameter.\n",[193,1002,1003,1005,1007,1010],{"class":195,"line":242},[193,1004,200],{"class":199},[193,1006,203],{"class":199},[193,1008,1009],{"class":206}," split_name",[193,1011,1012],{"class":210},"(\n",[193,1014,1015,1018,1020,1023],{"class":195,"line":267},[193,1016,1017],{"class":210},"  full_name ",[193,1019,214],{"class":199},[193,1021,1022],{"class":210},",                 ",[193,1024,1025],{"class":238},"-- IN (default direction)\n",[193,1027,1028,1031,1034,1036,1038],{"class":195,"line":282},[193,1029,1030],{"class":199},"  OUT",[193,1032,1033],{"class":199}," first",[193,1035,223],{"class":199},[193,1037,1022],{"class":210},[193,1039,1040],{"class":238},"-- OUT: returned, not passed by caller\n",[193,1042,1043,1045,1048,1050],{"class":195,"line":297},[193,1044,1030],{"class":199},[193,1046,1047],{"class":199}," last",[193,1049,223],{"class":199},[193,1051,1052],{"class":210},",\n",[193,1054,1055,1058,1060,1063,1066],{"class":195,"line":309},[193,1056,1057],{"class":210},"  separator ",[193,1059,214],{"class":199},[193,1061,1062],{"class":199}," DEFAULT",[193,1064,1065],{"class":521}," ' '",[193,1067,1068],{"class":238},"      -- DEFAULT: caller can omit\n",[193,1070,1071,1073,1076],{"class":195,"line":318},[193,1072,217],{"class":210},[193,1074,1075],{"class":199},"AS",[193,1077,229],{"class":210},[193,1079,1080],{"class":195,"line":345},[193,1081,490],{"class":199},[193,1083,1084],{"class":195,"line":360},[193,1085,1086],{"class":238},"  -- split full_name on first occurrence of separator\n",[193,1088,1089,1092,1094,1096,1099,1102],{"class":195,"line":369},[193,1090,1091],{"class":199},"  first",[193,1093,251],{"class":210},[193,1095,254],{"class":199},[193,1097,1098],{"class":210}," split_part(full_name, separator, ",[193,1100,1101],{"class":257},"1",[193,1103,1104],{"class":210},");\n",[193,1106,1107,1110,1113,1115,1117,1119],{"class":195,"line":375},[193,1108,1109],{"class":199},"  last",[193,1111,1112],{"class":210},"  :",[193,1114,254],{"class":199},[193,1116,1098],{"class":210},[193,1118,729],{"class":257},[193,1120,1104],{"class":210},[193,1122,1123],{"class":195,"line":381},[193,1124,1125],{"class":238},"  -- No RETURN needed — OUT params are the return value (composite type)\n",[193,1127,1128,1130],{"class":195,"line":387},[193,1129,405],{"class":199},[193,1131,408],{"class":210},[193,1133,1134,1136,1138],{"class":195,"line":393},[193,1135,414],{"class":210},[193,1137,417],{"class":199},[193,1139,1140],{"class":210}," plpgsql IMMUTABLE;\n",[193,1142,1143],{"class":195,"line":402},[193,1144,579],{"emptyLinePlaceholder":578},[193,1146,1147],{"class":195,"line":411},[193,1148,1149],{"class":238},"-- Call: returns a composite (first, last)\n",[193,1151,1152,1154,1157,1159,1162,1165],{"class":195,"line":615},[193,1153,159],{"class":199},[193,1155,1156],{"class":199}," *",[193,1158,904],{"class":199},[193,1160,1161],{"class":210}," split_name(",[193,1163,1164],{"class":521},"'Ada Lovelace'",[193,1166,1104],{"class":210},[193,1168,1169],{"class":195,"line":633},[193,1170,1171],{"class":238},"--  first |   last\n",[193,1173,1174],{"class":195,"line":642},[193,1175,1176],{"class":238},"-- -------+----------\n",[193,1178,1179],{"class":195,"line":649},[193,1180,1181],{"class":238},"--  Ada   | Lovelace\n",[193,1183,1184],{"class":195,"line":654},[193,1185,579],{"emptyLinePlaceholder":578},[193,1187,1188,1190,1192,1194,1196,1199,1202,1205,1208],{"class":195,"line":660},[193,1189,159],{"class":199},[193,1191,1156],{"class":199},[193,1193,904],{"class":199},[193,1195,1161],{"class":210},[193,1197,1198],{"class":521},"'Ada-Lovelace'",[193,1200,1201],{"class":210},", ",[193,1203,1204],{"class":521},"'-'",[193,1206,1207],{"class":210},");  ",[193,1209,1210],{"class":238},"-- uses the DEFAULT param\n",[44,1212,1214],{"id":1213},"return-types-scalar-set-table","Return Types — scalar, set, TABLE",[181,1216,1217],{"language":183},[185,1218,1220],{"className":187,"code":1219,"language":183,"meta":189,"style":189},"-- Scalar: returns a single value\nCREATE OR REPLACE FUNCTION tax(amount NUMERIC, rate NUMERIC DEFAULT 0.08)\nRETURNS NUMERIC AS $$\nBEGIN\n  RETURN amount * rate;\nEND;\n$$ LANGUAGE plpgsql IMMUTABLE;\n\n-- Set-returning (SETOF): returns 0+ rows. Use RETURN NEXT (one row at a time)\n-- or RETURN QUERY (append a query's results).\nCREATE OR REPLACE FUNCTION generate_series_custom(lo INTEGER, hi INTEGER)\nRETURNS SETOF INTEGER AS $$\nBEGIN\n  FOR i IN lo..hi LOOP\n    RETURN NEXT i;               -- emits one row per call\n  END LOOP;\n  RETURN;                        -- end of function (no more rows)\nEND;\n$$ LANGUAGE plpgsql IMMUTABLE;\n\n-- RETURNS TABLE: typed set-returning (named columns + types)\nCREATE OR REPLACE FUNCTION active_orders_for(cust_id BIGINT)\nRETURNS TABLE (id BIGINT, amount NUMERIC, ordered_on TIMESTAMPTZ) AS $$\nBEGIN\n  -- RETURN QUERY appends a query's result set to the function's output\n  RETURN QUERY\n    SELECT o.id, o.amount, o.ordered_on\n    FROM orders o\n    WHERE o.customer_id = active_orders_for.cust_id   -- qualify to avoid shadowing\n      AND o.status = 'active'\n    ORDER BY o.ordered_on DESC;\n  -- Can have multiple RETURN QUERY statements (results concatenated)\nEND;\n$$ LANGUAGE plpgsql STABLE;\n\n-- Usage: treat it like a table in FROM\nSELECT * FROM active_orders_for(42);\n-- Can be joined, filtered, aggregated — it's a relation\nSELECT sum(amount) FROM active_orders_for(42);\n",[27,1221,1222,1227,1259,1270,1274,1286,1292,1300,1304,1309,1314,1335,1348,1352,1366,1380,1388,1398,1404,1412,1416,1421,1437,1466,1470,1475,1482,1512,1520,1544,1561,1578,1583,1589,1598,1602,1607,1623,1628],{"__ignoreMap":189},[193,1223,1224],{"class":195,"line":196},[193,1225,1226],{"class":238},"-- Scalar: returns a single value\n",[193,1228,1229,1231,1233,1236,1239,1242,1245,1247,1249,1251,1253,1256],{"class":195,"line":232},[193,1230,200],{"class":199},[193,1232,203],{"class":199},[193,1234,1235],{"class":206}," tax",[193,1237,1238],{"class":210},"(amount ",[193,1240,1241],{"class":199},"NUMERIC",[193,1243,1244],{"class":210},", rate ",[193,1246,1241],{"class":199},[193,1248,1062],{"class":199},[193,1250,258],{"class":257},[193,1252,327],{"class":210},[193,1254,1255],{"class":257},"08",[193,1257,1258],{"class":210},")\n",[193,1260,1261,1263,1266,1268],{"class":195,"line":242},[193,1262,220],{"class":199},[193,1264,1265],{"class":199}," NUMERIC",[193,1267,226],{"class":199},[193,1269,229],{"class":210},[193,1271,1272],{"class":195,"line":267},[193,1273,490],{"class":199},[193,1275,1276,1278,1281,1283],{"class":195,"line":282},[193,1277,396],{"class":199},[193,1279,1280],{"class":210}," amount ",[193,1282,782],{"class":199},[193,1284,1285],{"class":210}," rate;\n",[193,1287,1288,1290],{"class":195,"line":297},[193,1289,405],{"class":199},[193,1291,408],{"class":210},[193,1293,1294,1296,1298],{"class":195,"line":309},[193,1295,414],{"class":210},[193,1297,417],{"class":199},[193,1299,1140],{"class":210},[193,1301,1302],{"class":195,"line":318},[193,1303,579],{"emptyLinePlaceholder":578},[193,1305,1306],{"class":195,"line":345},[193,1307,1308],{"class":238},"-- Set-returning (SETOF): returns 0+ rows. Use RETURN NEXT (one row at a time)\n",[193,1310,1311],{"class":195,"line":360},[193,1312,1313],{"class":238},"-- or RETURN QUERY (append a query's results).\n",[193,1315,1316,1318,1320,1323,1326,1328,1331,1333],{"class":195,"line":369},[193,1317,200],{"class":199},[193,1319,203],{"class":199},[193,1321,1322],{"class":206}," generate_series_custom",[193,1324,1325],{"class":210},"(lo ",[193,1327,446],{"class":199},[193,1329,1330],{"class":210},", hi ",[193,1332,446],{"class":199},[193,1334,1258],{"class":210},[193,1336,1337,1339,1342,1344,1346],{"class":195,"line":375},[193,1338,220],{"class":199},[193,1340,1341],{"class":210}," SETOF ",[193,1343,446],{"class":199},[193,1345,226],{"class":199},[193,1347,229],{"class":210},[193,1349,1350],{"class":195,"line":381},[193,1351,490],{"class":199},[193,1353,1354,1356,1358,1360,1363],{"class":195,"line":387},[193,1355,829],{"class":199},[193,1357,686],{"class":210},[193,1359,834],{"class":199},[193,1361,1362],{"class":210}," lo..hi ",[193,1364,1365],{"class":199},"LOOP\n",[193,1367,1368,1371,1374,1377],{"class":195,"line":393},[193,1369,1370],{"class":199},"    RETURN",[193,1372,1373],{"class":199}," NEXT",[193,1375,1376],{"class":210}," i;               ",[193,1378,1379],{"class":238},"-- emits one row per call\n",[193,1381,1382,1384,1386],{"class":195,"line":402},[193,1383,568],{"class":199},[193,1385,754],{"class":199},[193,1387,408],{"class":210},[193,1389,1390,1392,1395],{"class":195,"line":411},[193,1391,396],{"class":199},[193,1393,1394],{"class":210},";                        ",[193,1396,1397],{"class":238},"-- end of function (no more rows)\n",[193,1399,1400,1402],{"class":195,"line":615},[193,1401,405],{"class":199},[193,1403,408],{"class":210},[193,1405,1406,1408,1410],{"class":195,"line":633},[193,1407,414],{"class":210},[193,1409,417],{"class":199},[193,1411,1140],{"class":210},[193,1413,1414],{"class":195,"line":642},[193,1415,579],{"emptyLinePlaceholder":578},[193,1417,1418],{"class":195,"line":649},[193,1419,1420],{"class":238},"-- RETURNS TABLE: typed set-returning (named columns + types)\n",[193,1422,1423,1425,1427,1430,1433,1435],{"class":195,"line":654},[193,1424,200],{"class":199},[193,1426,203],{"class":199},[193,1428,1429],{"class":206}," active_orders_for",[193,1431,1432],{"class":210},"(cust_id ",[193,1434,288],{"class":199},[193,1436,1258],{"class":210},[193,1438,1439,1441,1444,1447,1449,1452,1454,1457,1460,1462,1464],{"class":195,"line":660},[193,1440,220],{"class":199},[193,1442,1443],{"class":199}," TABLE",[193,1445,1446],{"class":210}," (id ",[193,1448,288],{"class":199},[193,1450,1451],{"class":210},", amount ",[193,1453,1241],{"class":199},[193,1455,1456],{"class":210},", ordered_on ",[193,1458,1459],{"class":199},"TIMESTAMPTZ",[193,1461,217],{"class":210},[193,1463,1075],{"class":199},[193,1465,229],{"class":210},[193,1467,1468],{"class":195,"line":672},[193,1469,490],{"class":199},[193,1471,1472],{"class":195,"line":678},[193,1473,1474],{"class":238},"  -- RETURN QUERY appends a query's result set to the function's output\n",[193,1476,1477,1479],{"class":195,"line":697},[193,1478,396],{"class":199},[193,1480,1481],{"class":210}," QUERY\n",[193,1483,1484,1486,1489,1491,1493,1495,1498,1500,1503,1505,1507,1509],{"class":195,"line":717},[193,1485,898],{"class":199},[193,1487,1488],{"class":257}," o",[193,1490,327],{"class":210},[193,1492,330],{"class":257},[193,1494,1201],{"class":210},[193,1496,1497],{"class":257},"o",[193,1499,327],{"class":210},[193,1501,1502],{"class":257},"amount",[193,1504,1201],{"class":210},[193,1506,1497],{"class":257},[193,1508,327],{"class":210},[193,1510,1511],{"class":257},"ordered_on\n",[193,1513,1514,1517],{"class":195,"line":743},[193,1515,1516],{"class":199},"    FROM",[193,1518,1519],{"class":210}," orders o\n",[193,1521,1522,1525,1527,1529,1532,1534,1536,1538,1541],{"class":195,"line":749},[193,1523,1524],{"class":199},"    WHERE",[193,1526,1488],{"class":257},[193,1528,327],{"class":210},[193,1530,1531],{"class":257},"customer_id",[193,1533,732],{"class":199},[193,1535,1429],{"class":257},[193,1537,327],{"class":210},[193,1539,1540],{"class":257},"cust_id",[193,1542,1543],{"class":238},"   -- qualify to avoid shadowing\n",[193,1545,1546,1549,1551,1553,1556,1558],{"class":195,"line":759},[193,1547,1548],{"class":199},"      AND",[193,1550,1488],{"class":257},[193,1552,327],{"class":210},[193,1554,1555],{"class":257},"status",[193,1557,732],{"class":199},[193,1559,1560],{"class":521}," 'active'\n",[193,1562,1563,1566,1568,1570,1573,1576],{"class":195,"line":764},[193,1564,1565],{"class":199},"    ORDER BY",[193,1567,1488],{"class":257},[193,1569,327],{"class":210},[193,1571,1572],{"class":257},"ordered_on",[193,1574,1575],{"class":199}," DESC",[193,1577,408],{"class":210},[193,1579,1580],{"class":195,"line":770},[193,1581,1582],{"class":238},"  -- Can have multiple RETURN QUERY statements (results concatenated)\n",[193,1584,1585,1587],{"class":195,"line":791},[193,1586,405],{"class":199},[193,1588,408],{"class":210},[193,1590,1591,1593,1595],{"class":195,"line":806},[193,1592,414],{"class":210},[193,1594,417],{"class":199},[193,1596,1597],{"class":210}," plpgsql STABLE;\n",[193,1599,1600],{"class":195,"line":815},[193,1601,579],{"emptyLinePlaceholder":578},[193,1603,1604],{"class":195,"line":820},[193,1605,1606],{"class":238},"-- Usage: treat it like a table in FROM\n",[193,1608,1609,1611,1613,1615,1618,1621],{"class":195,"line":826},[193,1610,159],{"class":199},[193,1612,1156],{"class":199},[193,1614,904],{"class":199},[193,1616,1617],{"class":210}," active_orders_for(",[193,1619,1620],{"class":257},"42",[193,1622,1104],{"class":210},[193,1624,1625],{"class":195,"line":850},[193,1626,1627],{"class":238},"-- Can be joined, filtered, aggregated — it's a relation\n",[193,1629,1630,1632,1635,1638,1641,1643,1645],{"class":195,"line":856},[193,1631,159],{"class":199},[193,1633,1634],{"class":257}," sum",[193,1636,1637],{"class":210},"(amount) ",[193,1639,1640],{"class":199},"FROM",[193,1642,1617],{"class":210},[193,1644,1620],{"class":257},[193,1646,1104],{"class":210},[44,1648,1650],{"id":1649},"volatility-immutable-stable-volatile","Volatility — IMMUTABLE \u002F STABLE \u002F VOLATILE",[14,1652,1653,1654],{},"The volatility label tells the planner how it can optimize calls. ",[18,1655,1656],{},"Mislabeling produces wrong results.",[49,1658,1659,1675],{},[52,1660,1661],{},[55,1662,1663,1666,1669,1672],{},[58,1664,1665],{},"Category",[58,1667,1668],{},"Same inputs → same output?",[58,1670,1671],{},"Reads tables?",[58,1673,1674],{},"Planner optimizations",[68,1676,1677,1691,1707],{},[55,1678,1679,1683,1686,1688],{},[73,1680,1681],{},[27,1682,146],{},[73,1684,1685],{},"Always (for all time)",[73,1687,149],{},[73,1689,1690],{},"Constant folding, index eligibility, cached per query",[55,1692,1693,1698,1701,1704],{},[73,1694,1695],{},[27,1696,1697],{},"STABLE",[73,1699,1700],{},"Within one statement\u002Ftransaction",[73,1702,1703],{},"Yes (read-only)",[73,1705,1706],{},"Cached within one statement",[55,1708,1709,1715,1718,1721],{},[73,1710,1711,1714],{},[27,1712,1713],{},"VOLATILE"," (default)",[73,1716,1717],{},"Can vary per call",[73,1719,1720],{},"Yes (writes allowed)",[73,1722,1723],{},"Re-evaluated every call, no caching",[181,1725,1726],{"language":183},[185,1727,1729],{"className":187,"code":1728,"language":183,"meta":189,"style":189},"-- IMMUTABLE: pure function. No table reads, no time\u002Frandom\u002Fside effects.\n-- The planner can precompute the result at plan time and reuse it.\n-- REQUIRED for use in expression indexes (the index stores the computed\n-- value at index time — if the function could change, the index is stale).\nCREATE OR REPLACE FUNCTION normalized_email(raw TEXT) RETURNS TEXT AS $$\n  SELECT lower(trim(raw));         -- pure string transform, no tables\n$$ LANGUAGE sql IMMUTABLE;\n\nCREATE INDEX users_norm_email_idx ON users(normalized_email(email));\n-- The index stores normalized_email(email) for each row. If the function\n-- were STABLE (could read tables), the index couldn't guarantee correctness.\n\n-- STABLE: reads tables but result is consistent within one statement.\n-- The planner can cache the result for the duration of one query.\nCREATE OR REPLACE FUNCTION user_order_count(uid BIGINT) RETURNS BIGINT AS $$\n  SELECT count(*) FROM orders WHERE customer_id = uid;\n$$ LANGUAGE sql STABLE;\n-- STABLE because: the count could change between transactions, but within\n-- one statement (one snapshot), it's consistent.\n\n-- VOLATILE (default): can return different results on each call.\n-- now(), random(), nextval() are VOLATILE. Functions that write are VOLATILE.\nCREATE OR REPLACE FUNCTION add_audit_entry(msg TEXT) RETURNS VOID AS $$\nBEGIN\n  INSERT INTO audit_log (message) VALUES (msg);  -- side effect = VOLATILE\nEND;\n$$ LANGUAGE plpgsql;  -- VOLATILE is the default, explicitly omitted here\n",[27,1730,1731,1736,1741,1746,1751,1778,1801,1813,1817,1834,1839,1844,1848,1853,1858,1885,1915,1926,1931,1936,1940,1945,1950,1975,1979,2001,2007],{"__ignoreMap":189},[193,1732,1733],{"class":195,"line":196},[193,1734,1735],{"class":238},"-- IMMUTABLE: pure function. No table reads, no time\u002Frandom\u002Fside effects.\n",[193,1737,1738],{"class":195,"line":232},[193,1739,1740],{"class":238},"-- The planner can precompute the result at plan time and reuse it.\n",[193,1742,1743],{"class":195,"line":242},[193,1744,1745],{"class":238},"-- REQUIRED for use in expression indexes (the index stores the computed\n",[193,1747,1748],{"class":195,"line":267},[193,1749,1750],{"class":238},"-- value at index time — if the function could change, the index is stale).\n",[193,1752,1753,1755,1757,1760,1763,1766,1768,1770,1772,1774,1776],{"class":195,"line":282},[193,1754,200],{"class":199},[193,1756,203],{"class":199},[193,1758,1759],{"class":206}," normalized_email",[193,1761,1762],{"class":210},"(",[193,1764,1765],{"class":199},"raw",[193,1767,223],{"class":199},[193,1769,217],{"class":210},[193,1771,220],{"class":199},[193,1773,223],{"class":199},[193,1775,226],{"class":199},[193,1777,229],{"class":210},[193,1779,1780,1783,1786,1788,1791,1793,1795,1798],{"class":195,"line":297},[193,1781,1782],{"class":199},"  SELECT",[193,1784,1785],{"class":257}," lower",[193,1787,1762],{"class":210},[193,1789,1790],{"class":257},"trim",[193,1792,1762],{"class":210},[193,1794,1765],{"class":199},[193,1796,1797],{"class":210},"));         ",[193,1799,1800],{"class":238},"-- pure string transform, no tables\n",[193,1802,1803,1805,1807,1810],{"class":195,"line":309},[193,1804,414],{"class":210},[193,1806,417],{"class":199},[193,1808,1809],{"class":199}," sql",[193,1811,1812],{"class":210}," IMMUTABLE;\n",[193,1814,1815],{"class":195,"line":318},[193,1816,579],{"emptyLinePlaceholder":578},[193,1818,1819,1822,1825,1828,1831],{"class":195,"line":345},[193,1820,1821],{"class":199},"CREATE",[193,1823,1824],{"class":199}," INDEX",[193,1826,1827],{"class":206}," users_norm_email_idx",[193,1829,1830],{"class":199}," ON",[193,1832,1833],{"class":210}," users(normalized_email(email));\n",[193,1835,1836],{"class":195,"line":360},[193,1837,1838],{"class":238},"-- The index stores normalized_email(email) for each row. If the function\n",[193,1840,1841],{"class":195,"line":369},[193,1842,1843],{"class":238},"-- were STABLE (could read tables), the index couldn't guarantee correctness.\n",[193,1845,1846],{"class":195,"line":375},[193,1847,579],{"emptyLinePlaceholder":578},[193,1849,1850],{"class":195,"line":381},[193,1851,1852],{"class":238},"-- STABLE: reads tables but result is consistent within one statement.\n",[193,1854,1855],{"class":195,"line":387},[193,1856,1857],{"class":238},"-- The planner can cache the result for the duration of one query.\n",[193,1859,1860,1862,1864,1867,1869,1872,1875,1877,1879,1881,1883],{"class":195,"line":393},[193,1861,200],{"class":199},[193,1863,203],{"class":199},[193,1865,1866],{"class":206}," user_order_count",[193,1868,1762],{"class":210},[193,1870,1871],{"class":199},"uid",[193,1873,1874],{"class":199}," BIGINT",[193,1876,217],{"class":210},[193,1878,220],{"class":199},[193,1880,1874],{"class":199},[193,1882,226],{"class":199},[193,1884,229],{"class":210},[193,1886,1887,1889,1892,1894,1896,1898,1900,1903,1905,1908,1910,1913],{"class":195,"line":402},[193,1888,1782],{"class":199},[193,1890,1891],{"class":257}," count",[193,1893,1762],{"class":210},[193,1895,782],{"class":199},[193,1897,217],{"class":210},[193,1899,1640],{"class":199},[193,1901,1902],{"class":210}," orders ",[193,1904,85],{"class":199},[193,1906,1907],{"class":210}," customer_id ",[193,1909,254],{"class":199},[193,1911,1912],{"class":199}," uid",[193,1914,408],{"class":210},[193,1916,1917,1919,1921,1923],{"class":195,"line":411},[193,1918,414],{"class":210},[193,1920,417],{"class":199},[193,1922,1809],{"class":199},[193,1924,1925],{"class":210}," STABLE;\n",[193,1927,1928],{"class":195,"line":615},[193,1929,1930],{"class":238},"-- STABLE because: the count could change between transactions, but within\n",[193,1932,1933],{"class":195,"line":633},[193,1934,1935],{"class":238},"-- one statement (one snapshot), it's consistent.\n",[193,1937,1938],{"class":195,"line":642},[193,1939,579],{"emptyLinePlaceholder":578},[193,1941,1942],{"class":195,"line":649},[193,1943,1944],{"class":238},"-- VOLATILE (default): can return different results on each call.\n",[193,1946,1947],{"class":195,"line":654},[193,1948,1949],{"class":238},"-- now(), random(), nextval() are VOLATILE. Functions that write are VOLATILE.\n",[193,1951,1952,1954,1956,1959,1962,1964,1966,1968,1971,1973],{"class":195,"line":660},[193,1953,200],{"class":199},[193,1955,203],{"class":199},[193,1957,1958],{"class":206}," add_audit_entry",[193,1960,1961],{"class":210},"(msg ",[193,1963,214],{"class":199},[193,1965,217],{"class":210},[193,1967,220],{"class":199},[193,1969,1970],{"class":210}," VOID ",[193,1972,1075],{"class":199},[193,1974,229],{"class":210},[193,1976,1977],{"class":195,"line":672},[193,1978,490],{"class":199},[193,1980,1981,1984,1987,1990,1992,1995,1998],{"class":195,"line":678},[193,1982,1983],{"class":199},"  INSERT INTO",[193,1985,1986],{"class":210}," audit_log (",[193,1988,1989],{"class":199},"message",[193,1991,217],{"class":210},[193,1993,1994],{"class":199},"VALUES",[193,1996,1997],{"class":210}," (msg);  ",[193,1999,2000],{"class":238},"-- side effect = VOLATILE\n",[193,2002,2003,2005],{"class":195,"line":697},[193,2004,405],{"class":199},[193,2006,408],{"class":210},[193,2008,2009,2011,2013,2016],{"class":195,"line":717},[193,2010,414],{"class":210},[193,2012,417],{"class":199},[193,2014,2015],{"class":210}," plpgsql;  ",[193,2017,2018],{"class":238},"-- VOLATILE is the default, explicitly omitted here\n",[2020,2021,2023],"h3",{"id":2022},"volatility-and-index-eligibility-the-critical-link","Volatility and Index Eligibility — the critical link",[181,2025,2026],{"language":183},[185,2027,2029],{"className":187,"code":2028,"language":183,"meta":189,"style":189},"-- Expression indexes REQUIRE IMMUTABLE functions. The index stores the\n-- function's result at insert\u002Fupdate time. If the function isn't immutable\n-- (reads a table, depends on time), the stored value can go stale.\nCREATE INDEX users_email_lower_idx ON users(lower(email));  -- lower() is IMMUTABLE ✓\n\n-- ❌ This will FAIL at CREATE INDEX time:\nCREATE OR REPLACE FUNCTION current_tax_rate(state TEXT) RETURNS NUMERIC AS $$\n  SELECT rate FROM tax_rates WHERE state_code = state;  -- reads a table!\n$$ LANGUAGE sql IMMUTABLE;\nCREATE INDEX orders_tax_idx ON orders(current_tax_rate(state));\n-- ERROR: functions in index expression must be marked IMMUTABLE\n-- (PostgreSQL allows the function to be CREATED with IMMUTABLE even if it\n--  reads a table — the mistake surfaces as WRONG RESULTS, not an error,\n--  if you trick it. See the Spot the Bug section.)\n",[27,2030,2031,2036,2041,2046,2069,2073,2078,2104,2132,2142,2161,2166,2171,2176],{"__ignoreMap":189},[193,2032,2033],{"class":195,"line":196},[193,2034,2035],{"class":238},"-- Expression indexes REQUIRE IMMUTABLE functions. The index stores the\n",[193,2037,2038],{"class":195,"line":232},[193,2039,2040],{"class":238},"-- function's result at insert\u002Fupdate time. If the function isn't immutable\n",[193,2042,2043],{"class":195,"line":242},[193,2044,2045],{"class":238},"-- (reads a table, depends on time), the stored value can go stale.\n",[193,2047,2048,2050,2052,2055,2057,2060,2063,2066],{"class":195,"line":267},[193,2049,1821],{"class":199},[193,2051,1824],{"class":199},[193,2053,2054],{"class":206}," users_email_lower_idx",[193,2056,1830],{"class":199},[193,2058,2059],{"class":210}," users(",[193,2061,2062],{"class":257},"lower",[193,2064,2065],{"class":210},"(email));  ",[193,2067,2068],{"class":238},"-- lower() is IMMUTABLE ✓\n",[193,2070,2071],{"class":195,"line":282},[193,2072,579],{"emptyLinePlaceholder":578},[193,2074,2075],{"class":195,"line":297},[193,2076,2077],{"class":238},"-- ❌ This will FAIL at CREATE INDEX time:\n",[193,2079,2080,2082,2084,2087,2089,2092,2094,2096,2098,2100,2102],{"class":195,"line":309},[193,2081,200],{"class":199},[193,2083,203],{"class":199},[193,2085,2086],{"class":206}," current_tax_rate",[193,2088,1762],{"class":210},[193,2090,2091],{"class":199},"state",[193,2093,223],{"class":199},[193,2095,217],{"class":210},[193,2097,220],{"class":199},[193,2099,1265],{"class":199},[193,2101,226],{"class":199},[193,2103,229],{"class":210},[193,2105,2106,2108,2111,2113,2116,2118,2121,2123,2126,2129],{"class":195,"line":318},[193,2107,1782],{"class":199},[193,2109,2110],{"class":210}," rate ",[193,2112,1640],{"class":199},[193,2114,2115],{"class":210}," tax_rates ",[193,2117,85],{"class":199},[193,2119,2120],{"class":210}," state_code ",[193,2122,254],{"class":199},[193,2124,2125],{"class":199}," state",[193,2127,2128],{"class":210},";  ",[193,2130,2131],{"class":238},"-- reads a table!\n",[193,2133,2134,2136,2138,2140],{"class":195,"line":345},[193,2135,414],{"class":210},[193,2137,417],{"class":199},[193,2139,1809],{"class":199},[193,2141,1812],{"class":210},[193,2143,2144,2146,2148,2151,2153,2156,2158],{"class":195,"line":360},[193,2145,1821],{"class":199},[193,2147,1824],{"class":199},[193,2149,2150],{"class":206}," orders_tax_idx",[193,2152,1830],{"class":199},[193,2154,2155],{"class":210}," orders(current_tax_rate(",[193,2157,2091],{"class":199},[193,2159,2160],{"class":210},"));\n",[193,2162,2163],{"class":195,"line":369},[193,2164,2165],{"class":238},"-- ERROR: functions in index expression must be marked IMMUTABLE\n",[193,2167,2168],{"class":195,"line":375},[193,2169,2170],{"class":238},"-- (PostgreSQL allows the function to be CREATED with IMMUTABLE even if it\n",[193,2172,2173],{"class":195,"line":381},[193,2174,2175],{"class":238},"--  reads a table — the mistake surfaces as WRONG RESULTS, not an error,\n",[193,2177,2178],{"class":195,"line":387},[193,2179,2180],{"class":238},"--  if you trick it. See the Spot the Bug section.)\n",[44,2182,2184],{"id":2183},"strict-and-leakproof","STRICT and LEAKPROOF",[181,2186,2187],{"language":183},[185,2188,2190],{"className":187,"code":2189,"language":183,"meta":189,"style":189},"-- STRICT (aka CALLED ON NULL INPUT vs RETURNS NULL ON NULL INPUT):\n-- if any argument is NULL, the function is not called and NULL is returned\n-- immediately. Saves a function call and simplifies NULL handling.\nCREATE OR REPLACE FUNCTION safe_divide(a NUMERIC, b NUMERIC)\nRETURNS NUMERIC AS $$\nBEGIN\n  RETURN a \u002F b;   -- no need to check for NULL a or b — STRICT handles it\nEND;\n$$ LANGUAGE plpgsql IMMUTABLE STRICT;\n-- safe_divide(NULL, 2) → NULL (function body never executes)\n\n-- LEAKPROOF: guarantees the function throws no error for any input and\n-- reveals nothing about arguments via side effects. Used by the planner\n-- for security-barrier views and RLS predicate pushdown. Only superusers\n-- can mark functions LEAKPROOF. Rarely needed in application code.\n",[27,2191,2192,2197,2202,2207,2228,2238,2242,2257,2263,2272,2277,2281,2286,2291,2296],{"__ignoreMap":189},[193,2193,2194],{"class":195,"line":196},[193,2195,2196],{"class":238},"-- STRICT (aka CALLED ON NULL INPUT vs RETURNS NULL ON NULL INPUT):\n",[193,2198,2199],{"class":195,"line":232},[193,2200,2201],{"class":238},"-- if any argument is NULL, the function is not called and NULL is returned\n",[193,2203,2204],{"class":195,"line":242},[193,2205,2206],{"class":238},"-- immediately. Saves a function call and simplifies NULL handling.\n",[193,2208,2209,2211,2213,2216,2219,2221,2224,2226],{"class":195,"line":267},[193,2210,200],{"class":199},[193,2212,203],{"class":199},[193,2214,2215],{"class":206}," safe_divide",[193,2217,2218],{"class":210},"(a ",[193,2220,1241],{"class":199},[193,2222,2223],{"class":210},", b ",[193,2225,1241],{"class":199},[193,2227,1258],{"class":210},[193,2229,2230,2232,2234,2236],{"class":195,"line":282},[193,2231,220],{"class":199},[193,2233,1265],{"class":199},[193,2235,226],{"class":199},[193,2237,229],{"class":210},[193,2239,2240],{"class":195,"line":297},[193,2241,490],{"class":199},[193,2243,2244,2246,2249,2251,2254],{"class":195,"line":309},[193,2245,396],{"class":199},[193,2247,2248],{"class":210}," a ",[193,2250,34],{"class":199},[193,2252,2253],{"class":210}," b;   ",[193,2255,2256],{"class":238},"-- no need to check for NULL a or b — STRICT handles it\n",[193,2258,2259,2261],{"class":195,"line":318},[193,2260,405],{"class":199},[193,2262,408],{"class":210},[193,2264,2265,2267,2269],{"class":195,"line":345},[193,2266,414],{"class":210},[193,2268,417],{"class":199},[193,2270,2271],{"class":210}," plpgsql IMMUTABLE STRICT;\n",[193,2273,2274],{"class":195,"line":360},[193,2275,2276],{"class":238},"-- safe_divide(NULL, 2) → NULL (function body never executes)\n",[193,2278,2279],{"class":195,"line":369},[193,2280,579],{"emptyLinePlaceholder":578},[193,2282,2283],{"class":195,"line":375},[193,2284,2285],{"class":238},"-- LEAKPROOF: guarantees the function throws no error for any input and\n",[193,2287,2288],{"class":195,"line":381},[193,2289,2290],{"class":238},"-- reveals nothing about arguments via side effects. Used by the planner\n",[193,2292,2293],{"class":195,"line":387},[193,2294,2295],{"class":238},"-- for security-barrier views and RLS predicate pushdown. Only superusers\n",[193,2297,2298],{"class":195,"line":393},[193,2299,2300],{"class":238},"-- can mark functions LEAKPROOF. Rarely needed in application code.\n",[44,2302,2304],{"id":2303},"sql-functions-vs-plpgsql-inlining-matters","SQL Functions vs PL\u002FpgSQL — inlining matters",[181,2306,2307],{"language":183},[185,2308,2310],{"className":187,"code":2309,"language":183,"meta":189,"style":189},"-- SQL function: no BEGIN\u002FEND. The planner can INLINE it (substitute the\n-- function body into the calling query), enabling predicate pushdown,\n-- better cost estimates, and join optimization across the function boundary.\nCREATE OR REPLACE FUNCTION active_customer(cid BIGINT) RETURNS BOOLEAN AS $$\n  SELECT EXISTS (SELECT 1 FROM orders WHERE customer_id = cid AND status = 'active');\n$$ LANGUAGE sql STABLE;\n-- The planner can inline this into: WHERE EXISTS (SELECT 1 FROM orders ...)\n\n-- PL\u002FpgSQL function: opaque to the planner. The planner treats it as a\n-- black box — it can't see inside, can't push predicates, can't estimate\n-- row counts. For hot-path functions, prefer SQL functions (inlinable).\nCREATE OR REPLACE FUNCTION active_customer_plpgsql(cid BIGINT) RETURNS BOOLEAN AS $$\nDECLARE\n  found BOOLEAN;\nBEGIN\n  SELECT EXISTS (SELECT 1 FROM orders WHERE customer_id = cid AND status = 'active')\n  INTO found;\n  RETURN found;\nEND;\n$$ LANGUAGE plpgsql STABLE;\n-- The planner calls this as a black box. No inlining, no pushdown.\n",[27,2311,2312,2317,2322,2327,2352,2392,2402,2407,2411,2416,2421,2426,2449,2453,2462,2466,2500,2508,2514,2520,2528],{"__ignoreMap":189},[193,2313,2314],{"class":195,"line":196},[193,2315,2316],{"class":238},"-- SQL function: no BEGIN\u002FEND. The planner can INLINE it (substitute the\n",[193,2318,2319],{"class":195,"line":232},[193,2320,2321],{"class":238},"-- function body into the calling query), enabling predicate pushdown,\n",[193,2323,2324],{"class":195,"line":242},[193,2325,2326],{"class":238},"-- better cost estimates, and join optimization across the function boundary.\n",[193,2328,2329,2331,2333,2336,2339,2341,2343,2345,2348,2350],{"class":195,"line":267},[193,2330,200],{"class":199},[193,2332,203],{"class":199},[193,2334,2335],{"class":206}," active_customer",[193,2337,2338],{"class":210},"(cid ",[193,2340,288],{"class":199},[193,2342,217],{"class":210},[193,2344,220],{"class":199},[193,2346,2347],{"class":199}," BOOLEAN",[193,2349,226],{"class":199},[193,2351,229],{"class":210},[193,2353,2354,2356,2359,2362,2364,2366,2368,2370,2372,2374,2376,2379,2382,2385,2387,2390],{"class":195,"line":282},[193,2355,1782],{"class":199},[193,2357,2358],{"class":199}," EXISTS",[193,2360,2361],{"class":210}," (",[193,2363,159],{"class":199},[193,2365,692],{"class":257},[193,2367,904],{"class":199},[193,2369,1902],{"class":210},[193,2371,85],{"class":199},[193,2373,1907],{"class":210},[193,2375,254],{"class":199},[193,2377,2378],{"class":210}," cid ",[193,2380,2381],{"class":199},"AND",[193,2383,2384],{"class":199}," status",[193,2386,732],{"class":199},[193,2388,2389],{"class":521}," 'active'",[193,2391,1104],{"class":210},[193,2393,2394,2396,2398,2400],{"class":195,"line":297},[193,2395,414],{"class":210},[193,2397,417],{"class":199},[193,2399,1809],{"class":199},[193,2401,1925],{"class":210},[193,2403,2404],{"class":195,"line":309},[193,2405,2406],{"class":238},"-- The planner can inline this into: WHERE EXISTS (SELECT 1 FROM orders ...)\n",[193,2408,2409],{"class":195,"line":318},[193,2410,579],{"emptyLinePlaceholder":578},[193,2412,2413],{"class":195,"line":345},[193,2414,2415],{"class":238},"-- PL\u002FpgSQL function: opaque to the planner. The planner treats it as a\n",[193,2417,2418],{"class":195,"line":360},[193,2419,2420],{"class":238},"-- black box — it can't see inside, can't push predicates, can't estimate\n",[193,2422,2423],{"class":195,"line":369},[193,2424,2425],{"class":238},"-- row counts. For hot-path functions, prefer SQL functions (inlinable).\n",[193,2427,2428,2430,2432,2435,2437,2439,2441,2443,2445,2447],{"class":195,"line":375},[193,2429,200],{"class":199},[193,2431,203],{"class":199},[193,2433,2434],{"class":206}," active_customer_plpgsql",[193,2436,2338],{"class":210},[193,2438,288],{"class":199},[193,2440,217],{"class":210},[193,2442,220],{"class":199},[193,2444,2347],{"class":199},[193,2446,226],{"class":199},[193,2448,229],{"class":210},[193,2450,2451],{"class":195,"line":381},[193,2452,461],{"class":199},[193,2454,2455,2458,2460],{"class":195,"line":387},[193,2456,2457],{"class":210},"  found ",[193,2459,351],{"class":199},[193,2461,408],{"class":210},[193,2463,2464],{"class":195,"line":393},[193,2465,490],{"class":199},[193,2467,2468,2470,2472,2474,2476,2478,2480,2482,2484,2486,2488,2490,2492,2494,2496,2498],{"class":195,"line":402},[193,2469,1782],{"class":199},[193,2471,2358],{"class":199},[193,2473,2361],{"class":210},[193,2475,159],{"class":199},[193,2477,692],{"class":257},[193,2479,904],{"class":199},[193,2481,1902],{"class":210},[193,2483,85],{"class":199},[193,2485,1907],{"class":210},[193,2487,254],{"class":199},[193,2489,2378],{"class":210},[193,2491,2381],{"class":199},[193,2493,2384],{"class":199},[193,2495,732],{"class":199},[193,2497,2389],{"class":521},[193,2499,1258],{"class":210},[193,2501,2502,2505],{"class":195,"line":411},[193,2503,2504],{"class":199},"  INTO",[193,2506,2507],{"class":210}," found;\n",[193,2509,2510,2512],{"class":195,"line":615},[193,2511,396],{"class":199},[193,2513,2507],{"class":210},[193,2515,2516,2518],{"class":195,"line":633},[193,2517,405],{"class":199},[193,2519,408],{"class":210},[193,2521,2522,2524,2526],{"class":195,"line":642},[193,2523,414],{"class":210},[193,2525,417],{"class":199},[193,2527,1597],{"class":210},[193,2529,2530],{"class":195,"line":649},[193,2531,2532],{"class":238},"-- The planner calls this as a black box. No inlining, no pushdown.\n",[14,2534,2535,2538,2539,2542,2543,2546,2547,2550,2551,2554,2555,2558,2559,2562],{},[18,2536,2537],{},"When SQL functions don't inline",": if they have multiple statements, use ",[27,2540,2541],{},"SET"," clauses, reference ",[27,2544,2545],{},"tg_*"," variables, or return ",[27,2548,2549],{},"SETOF"," with ",[27,2552,2553],{},"RETURNS TABLE"," in some cases. Check with ",[27,2556,2557],{},"EXPLAIN"," — if you see a ",[27,2560,2561],{},"Function Scan"," instead of the underlying table access, it wasn't inlined.",[44,2564,2566],{"id":2565},"procedures-call-and-transaction-control","Procedures — CALL and Transaction Control",[181,2568,2569],{"language":183},[185,2570,2572],{"className":187,"code":2571,"language":183,"meta":189,"style":189},"-- Procedures are called with CALL (not SELECT). They can COMMIT\u002FROLLBACK\n-- mid-execution — the key difference from functions. Use for multi-step\n-- ETL that needs per-batch transactions (short transactions = less\n-- bloat, resumable on restart).\nCREATE OR REPLACE PROCEDURE archive_old_orders(days_old INTEGER)\nLANGUAGE plpgsql AS $$\nDECLARE\n  batch_count INTEGER;\n  total       INTEGER := 0;\nBEGIN\n  LOOP\n    -- Move one batch (10k rows) in a single statement (atomic)\n    WITH to_move AS (\n      SELECT id FROM orders\n      WHERE ordered_on \u003C now() - (days_old || ' days')::interval\n      ORDER BY id\n      LIMIT 10000                       -- small batches = short transactions\n      FOR UPDATE SKIP LOCKED            -- skip rows locked by other workers\n    ),\n    moved AS (\n      DELETE FROM orders WHERE id IN (SELECT id FROM to_move)\n      RETURNING *                        -- capture deleted rows for insert\n    )\n    INSERT INTO orders_archive SELECT * FROM moved;\n\n    GET DIAGNOSTICS batch_count = ROW_COUNT;   -- rows affected by last statement\n    total := total + batch_count;\n\n    COMMIT;                               -- ← PROCEDURES CAN COMMIT. Functions CANNOT.\n    -- Each batch is its own transaction. If the procedure crashes after\n    -- batch 5, batches 1-5 are committed, 6+ are not — resumable.\n\n    EXIT WHEN batch_count = 0;            -- no more rows to archive\n  END LOOP;\n\n  RAISE NOTICE 'Archived % orders', total;\nEND;\n$$;\n\nCALL archive_old_orders(365);\n",[27,2573,2574,2579,2584,2589,2594,2614,2625,2629,2638,2653,2657,2661,2666,2679,2692,2723,2731,2742,2759,2764,2773,2799,2809,2814,2831,2835,2851,2866,2870,2881,2886,2891,2895,2914,2922,2926,2937,2943,2948,2952],{"__ignoreMap":189},[193,2575,2576],{"class":195,"line":196},[193,2577,2578],{"class":238},"-- Procedures are called with CALL (not SELECT). They can COMMIT\u002FROLLBACK\n",[193,2580,2581],{"class":195,"line":232},[193,2582,2583],{"class":238},"-- mid-execution — the key difference from functions. Use for multi-step\n",[193,2585,2586],{"class":195,"line":242},[193,2587,2588],{"class":238},"-- ETL that needs per-batch transactions (short transactions = less\n",[193,2590,2591],{"class":195,"line":267},[193,2592,2593],{"class":238},"-- bloat, resumable on restart).\n",[193,2595,2596,2598,2601,2604,2607,2610,2612],{"class":195,"line":282},[193,2597,1821],{"class":199},[193,2599,2600],{"class":199}," OR",[193,2602,2603],{"class":199}," REPLACE",[193,2605,2606],{"class":199}," PROCEDURE",[193,2608,2609],{"class":210}," archive_old_orders(days_old ",[193,2611,446],{"class":199},[193,2613,1258],{"class":210},[193,2615,2616,2618,2621,2623],{"class":195,"line":297},[193,2617,417],{"class":199},[193,2619,2620],{"class":210}," plpgsql ",[193,2622,1075],{"class":199},[193,2624,229],{"class":210},[193,2626,2627],{"class":195,"line":309},[193,2628,461],{"class":199},[193,2630,2631,2634,2636],{"class":195,"line":318},[193,2632,2633],{"class":210},"  batch_count ",[193,2635,446],{"class":199},[193,2637,408],{"class":210},[193,2639,2640,2643,2645,2647,2649,2651],{"class":195,"line":345},[193,2641,2642],{"class":210},"  total       ",[193,2644,446],{"class":199},[193,2646,251],{"class":210},[193,2648,254],{"class":199},[193,2650,258],{"class":257},[193,2652,408],{"class":210},[193,2654,2655],{"class":195,"line":360},[193,2656,490],{"class":199},[193,2658,2659],{"class":195,"line":369},[193,2660,675],{"class":199},[193,2662,2663],{"class":195,"line":375},[193,2664,2665],{"class":238},"    -- Move one batch (10k rows) in a single statement (atomic)\n",[193,2667,2668,2671,2674,2676],{"class":195,"line":381},[193,2669,2670],{"class":199},"    WITH",[193,2672,2673],{"class":210}," to_move ",[193,2675,1075],{"class":199},[193,2677,2678],{"class":210}," (\n",[193,2680,2681,2684,2687,2689],{"class":195,"line":387},[193,2682,2683],{"class":199},"      SELECT",[193,2685,2686],{"class":210}," id ",[193,2688,1640],{"class":199},[193,2690,2691],{"class":210}," orders\n",[193,2693,2694,2697,2700,2702,2705,2708,2711,2714,2717,2720],{"class":195,"line":393},[193,2695,2696],{"class":199},"      WHERE",[193,2698,2699],{"class":210}," ordered_on ",[193,2701,532],{"class":199},[193,2703,2704],{"class":199}," now",[193,2706,2707],{"class":210},"() ",[193,2709,2710],{"class":199},"-",[193,2712,2713],{"class":210}," (days_old ",[193,2715,2716],{"class":199},"||",[193,2718,2719],{"class":521}," ' days'",[193,2721,2722],{"class":210},")::interval\n",[193,2724,2725,2728],{"class":195,"line":402},[193,2726,2727],{"class":199},"      ORDER BY",[193,2729,2730],{"class":210}," id\n",[193,2732,2733,2736,2739],{"class":195,"line":411},[193,2734,2735],{"class":199},"      LIMIT",[193,2737,2738],{"class":257}," 10000",[193,2740,2741],{"class":238},"                       -- small batches = short transactions\n",[193,2743,2744,2747,2750,2753,2756],{"class":195,"line":615},[193,2745,2746],{"class":199},"      FOR",[193,2748,2749],{"class":199}," UPDATE",[193,2751,2752],{"class":199}," SKIP",[193,2754,2755],{"class":210}," LOCKED            ",[193,2757,2758],{"class":238},"-- skip rows locked by other workers\n",[193,2760,2761],{"class":195,"line":633},[193,2762,2763],{"class":210},"    ),\n",[193,2765,2766,2769,2771],{"class":195,"line":642},[193,2767,2768],{"class":210},"    moved ",[193,2770,1075],{"class":199},[193,2772,2678],{"class":210},[193,2774,2775,2778,2780,2782,2784,2786,2788,2790,2792,2794,2796],{"class":195,"line":649},[193,2776,2777],{"class":199},"      DELETE",[193,2779,904],{"class":199},[193,2781,1902],{"class":210},[193,2783,85],{"class":199},[193,2785,2686],{"class":210},[193,2787,834],{"class":199},[193,2789,2361],{"class":210},[193,2791,159],{"class":199},[193,2793,2686],{"class":210},[193,2795,1640],{"class":199},[193,2797,2798],{"class":210}," to_move)\n",[193,2800,2801,2804,2806],{"class":195,"line":654},[193,2802,2803],{"class":210},"      RETURNING ",[193,2805,782],{"class":199},[193,2807,2808],{"class":238},"                        -- capture deleted rows for insert\n",[193,2810,2811],{"class":195,"line":660},[193,2812,2813],{"class":210},"    )\n",[193,2815,2816,2819,2822,2824,2826,2828],{"class":195,"line":672},[193,2817,2818],{"class":199},"    INSERT INTO",[193,2820,2821],{"class":210}," orders_archive ",[193,2823,159],{"class":199},[193,2825,1156],{"class":199},[193,2827,904],{"class":199},[193,2829,2830],{"class":210}," moved;\n",[193,2832,2833],{"class":195,"line":678},[193,2834,579],{"emptyLinePlaceholder":578},[193,2836,2837,2840,2843,2845,2848],{"class":195,"line":697},[193,2838,2839],{"class":199},"    GET",[193,2841,2842],{"class":210}," DIAGNOSTICS batch_count ",[193,2844,254],{"class":199},[193,2846,2847],{"class":210}," ROW_COUNT;   ",[193,2849,2850],{"class":238},"-- rows affected by last statement\n",[193,2852,2853,2856,2858,2861,2863],{"class":195,"line":717},[193,2854,2855],{"class":210},"    total :",[193,2857,254],{"class":199},[193,2859,2860],{"class":210}," total ",[193,2862,689],{"class":199},[193,2864,2865],{"class":210}," batch_count;\n",[193,2867,2868],{"class":195,"line":743},[193,2869,579],{"emptyLinePlaceholder":578},[193,2871,2872,2875,2878],{"class":195,"line":749},[193,2873,2874],{"class":199},"    COMMIT",[193,2876,2877],{"class":210},";                               ",[193,2879,2880],{"class":238},"-- ← PROCEDURES CAN COMMIT. Functions CANNOT.\n",[193,2882,2883],{"class":195,"line":759},[193,2884,2885],{"class":238},"    -- Each batch is its own transaction. If the procedure crashes after\n",[193,2887,2888],{"class":195,"line":764},[193,2889,2890],{"class":238},"    -- batch 5, batches 1-5 are committed, 6+ are not — resumable.\n",[193,2892,2893],{"class":195,"line":770},[193,2894,579],{"emptyLinePlaceholder":578},[193,2896,2897,2899,2901,2904,2906,2908,2911],{"class":195,"line":791},[193,2898,700],{"class":210},[193,2900,703],{"class":199},[193,2902,2903],{"class":210}," batch_count ",[193,2905,254],{"class":199},[193,2907,258],{"class":257},[193,2909,2910],{"class":210},";            ",[193,2912,2913],{"class":238},"-- no more rows to archive\n",[193,2915,2916,2918,2920],{"class":195,"line":806},[193,2917,568],{"class":199},[193,2919,754],{"class":199},[193,2921,408],{"class":210},[193,2923,2924],{"class":195,"line":815},[193,2925,579],{"emptyLinePlaceholder":578},[193,2927,2928,2931,2934],{"class":195,"line":820},[193,2929,2930],{"class":210},"  RAISE NOTICE ",[193,2932,2933],{"class":521},"'Archived % orders'",[193,2935,2936],{"class":210},", total;\n",[193,2938,2939,2941],{"class":195,"line":826},[193,2940,405],{"class":199},[193,2942,408],{"class":210},[193,2944,2945],{"class":195,"line":850},[193,2946,2947],{"class":210},"$$;\n",[193,2949,2950],{"class":195,"line":856},[193,2951,579],{"emptyLinePlaceholder":578},[193,2953,2954,2956,2959,2962],{"class":195,"line":864},[193,2955,29],{"class":199},[193,2957,2958],{"class":210}," archive_old_orders(",[193,2960,2961],{"class":257},"365",[193,2963,1104],{"class":210},[44,2965,2967],{"id":2966},"exception-handling-the-subtransaction-cost","Exception Handling — the subtransaction cost",[181,2969,2970],{"language":183},[185,2971,2973],{"className":187,"code":2972,"language":183,"meta":189,"style":189},"-- An EXCEPTION block in PL\u002FpgSQL creates a SUBTRANSACTION (savepoint).\n-- On error, the block's changes are rolled back to the savepoint — but\n-- the surrounding transaction continues. This is EXPENSIVE: each entry\n-- into an EXCEPTION block sets up a savepoint, even if no exception\n-- occurs. Don't wrap hot-path functions in exception handlers \"just in case.\"\nCREATE OR REPLACE FUNCTION safe_divide(a NUMERIC, b NUMERIC) RETURNS NUMERIC AS $$\nBEGIN\n  RETURN a \u002F b;\nEXCEPTION\n  WHEN division_by_zero THEN\n    RETURN NULL;                       -- specific error: handle gracefully\n  WHEN OTHERS THEN                     -- catch-all: log and return NULL\n    RAISE NOTICE 'Unexpected: %', SQLERRM;\n    RETURN NULL;\nEND;\n$$ LANGUAGE plpgsql IMMUTABLE STRICT;\n-- The savepoint is set up on EVERY call, even when b is non-zero.\n-- For a function called 1M times in a query, that's 1M savepoints.\n",[27,2974,2975,2980,2985,2990,2995,3000,3026,3030,3041,3046,3057,3070,3083,3093,3101,3107,3115,3120],{"__ignoreMap":189},[193,2976,2977],{"class":195,"line":196},[193,2978,2979],{"class":238},"-- An EXCEPTION block in PL\u002FpgSQL creates a SUBTRANSACTION (savepoint).\n",[193,2981,2982],{"class":195,"line":232},[193,2983,2984],{"class":238},"-- On error, the block's changes are rolled back to the savepoint — but\n",[193,2986,2987],{"class":195,"line":242},[193,2988,2989],{"class":238},"-- the surrounding transaction continues. This is EXPENSIVE: each entry\n",[193,2991,2992],{"class":195,"line":267},[193,2993,2994],{"class":238},"-- into an EXCEPTION block sets up a savepoint, even if no exception\n",[193,2996,2997],{"class":195,"line":282},[193,2998,2999],{"class":238},"-- occurs. Don't wrap hot-path functions in exception handlers \"just in case.\"\n",[193,3001,3002,3004,3006,3008,3010,3012,3014,3016,3018,3020,3022,3024],{"class":195,"line":297},[193,3003,200],{"class":199},[193,3005,203],{"class":199},[193,3007,2215],{"class":206},[193,3009,2218],{"class":210},[193,3011,1241],{"class":199},[193,3013,2223],{"class":210},[193,3015,1241],{"class":199},[193,3017,217],{"class":210},[193,3019,220],{"class":199},[193,3021,1265],{"class":199},[193,3023,226],{"class":199},[193,3025,229],{"class":210},[193,3027,3028],{"class":195,"line":309},[193,3029,490],{"class":199},[193,3031,3032,3034,3036,3038],{"class":195,"line":318},[193,3033,396],{"class":199},[193,3035,2248],{"class":210},[193,3037,34],{"class":199},[193,3039,3040],{"class":210}," b;\n",[193,3042,3043],{"class":195,"line":345},[193,3044,3045],{"class":210},"EXCEPTION\n",[193,3047,3048,3051,3054],{"class":195,"line":360},[193,3049,3050],{"class":199},"  WHEN",[193,3052,3053],{"class":210}," division_by_zero ",[193,3055,3056],{"class":199},"THEN\n",[193,3058,3059,3061,3064,3067],{"class":195,"line":369},[193,3060,1370],{"class":199},[193,3062,3063],{"class":199}," NULL",[193,3065,3066],{"class":210},";                       ",[193,3068,3069],{"class":238},"-- specific error: handle gracefully\n",[193,3071,3072,3074,3077,3080],{"class":195,"line":375},[193,3073,3050],{"class":199},[193,3075,3076],{"class":210}," OTHERS ",[193,3078,3079],{"class":199},"THEN",[193,3081,3082],{"class":238},"                     -- catch-all: log and return NULL\n",[193,3084,3085,3087,3090],{"class":195,"line":381},[193,3086,935],{"class":210},[193,3088,3089],{"class":521},"'Unexpected: %'",[193,3091,3092],{"class":210},", SQLERRM;\n",[193,3094,3095,3097,3099],{"class":195,"line":387},[193,3096,1370],{"class":199},[193,3098,3063],{"class":199},[193,3100,408],{"class":210},[193,3102,3103,3105],{"class":195,"line":393},[193,3104,405],{"class":199},[193,3106,408],{"class":210},[193,3108,3109,3111,3113],{"class":195,"line":402},[193,3110,414],{"class":210},[193,3112,417],{"class":199},[193,3114,2271],{"class":210},[193,3116,3117],{"class":195,"line":411},[193,3118,3119],{"class":238},"-- The savepoint is set up on EVERY call, even when b is non-zero.\n",[193,3121,3122],{"class":195,"line":615},[193,3123,3124],{"class":238},"-- For a function called 1M times in a query, that's 1M savepoints.\n",[2020,3126,3128],{"id":3127},"raise-notice-warning-exception","RAISE — NOTICE, WARNING, EXCEPTION",[181,3130,3131],{"language":183},[185,3132,3134],{"className":187,"code":3133,"language":183,"meta":189,"style":189},"RAISE NOTICE  'Processed % rows in % ms', row_count, elapsed;  -- client-visible log\nRAISE WARNING 'Deprecated function called by role %', current_user;\nRAISE EXCEPTION 'Invalid input: %', input;                     -- aborts the transaction\nRAISE EXCEPTION 'Division by zero' USING ERRCODE = '22012';   -- custom SQLSTATE\n-- After an uncaught EXCEPTION, the transaction is ABORTED — all subsequent\n-- statements fail until ROLLBACK. Use EXCEPTION blocks to handle locally.\n",[27,3135,3136,3150,3161,3175,3199,3204],{"__ignoreMap":189},[193,3137,3138,3141,3144,3147],{"class":195,"line":196},[193,3139,3140],{"class":210},"RAISE NOTICE  ",[193,3142,3143],{"class":521},"'Processed % rows in % ms'",[193,3145,3146],{"class":210},", row_count, elapsed;  ",[193,3148,3149],{"class":238},"-- client-visible log\n",[193,3151,3152,3155,3158],{"class":195,"line":232},[193,3153,3154],{"class":210},"RAISE WARNING ",[193,3156,3157],{"class":521},"'Deprecated function called by role %'",[193,3159,3160],{"class":210},", current_user;\n",[193,3162,3163,3166,3169,3172],{"class":195,"line":242},[193,3164,3165],{"class":210},"RAISE EXCEPTION ",[193,3167,3168],{"class":521},"'Invalid input: %'",[193,3170,3171],{"class":210},", input;                     ",[193,3173,3174],{"class":238},"-- aborts the transaction\n",[193,3176,3177,3179,3182,3185,3188,3190,3193,3196],{"class":195,"line":267},[193,3178,3165],{"class":210},[193,3180,3181],{"class":521},"'Division by zero'",[193,3183,3184],{"class":199}," USING",[193,3186,3187],{"class":210}," ERRCODE ",[193,3189,254],{"class":199},[193,3191,3192],{"class":521}," '22012'",[193,3194,3195],{"class":210},";   ",[193,3197,3198],{"class":238},"-- custom SQLSTATE\n",[193,3200,3201],{"class":195,"line":282},[193,3202,3203],{"class":238},"-- After an uncaught EXCEPTION, the transaction is ABORTED — all subsequent\n",[193,3205,3206],{"class":195,"line":297},[193,3207,3208],{"class":238},"-- statements fail until ROLLBACK. Use EXCEPTION blocks to handle locally.\n",[44,3210,3212],{"id":3211},"complex-implementation-batch-processing-with-error-logging","Complex Implementation — Batch Processing with Error Logging",[14,3214,3215],{},"A PL\u002FpgSQL function that processes a batch of records, catches per-row errors, logs failures to an error table, and returns a summary. Uses an exception block per row (subtransaction) to isolate failures.",[181,3217,3218],{"language":183},[185,3219,3221],{"className":187,"code":3220,"language":183,"meta":189,"style":189},"-- Error log table for failed processing\nCREATE TABLE processing_errors (\n  id          BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n  batch_id    UUID NOT NULL DEFAULT gen_random_uuid(),\n  source_id   BIGINT NOT NULL,           -- ID of the row that failed\n  error_code  TEXT,                      -- SQLSTATE\n  error_msg   TEXT,                      -- SQLERRM\n  failed_at   TIMESTAMPTZ NOT NULL DEFAULT now()\n);\nCREATE INDEX processing_errors_batch_idx ON processing_errors(batch_id);\n\n-- The batch processor: processes rows from a staging table, catches\n-- per-row errors (subtransaction per row), logs failures, returns summary.\nCREATE OR REPLACE FUNCTION process_batch(batch_limit INTEGER DEFAULT 1000)\nRETURNS TABLE (processed INTEGER, failed INTEGER, batch_id UUID) AS $$\nDECLARE\n  rec RECORD;\n  ok_count  INTEGER := 0;\n  fail_count INTEGER := 0;\n  bid       UUID := gen_random_uuid();\nBEGIN\n  -- Iterate over unprocessed rows in staging. FOR ... IN SELECT is cursor-like.\n  FOR rec IN\n    SELECT id, payload FROM staging_table\n    WHERE status = 'pending'\n    ORDER BY id\n    LIMIT batch_limit\n    FOR UPDATE SKIP LOCKED          -- skip rows other workers are processing\n  LOOP\n    BEGIN                           -- ← subtransaction (savepoint) per row\n      -- Business logic: validate and insert into the target table\n      INSERT INTO target_table (data, source_id)\n      VALUES (rec.payload, rec.id);\n\n      UPDATE staging_table SET status = 'done' WHERE id = rec.id;\n      ok_count := ok_count + 1;\n\n    EXCEPTION\n      WHEN OTHERS THEN              -- catch per-row errors, log, continue\n        INSERT INTO processing_errors (batch_id, source_id, error_code, error_msg)\n        VALUES (bid, rec.id, SQLSTATE, SQLERRM);\n        -- SQLSTATE: 5-char SQLSTATE code. SQLERRM: error message text.\n        UPDATE staging_table SET status = 'error' WHERE id = rec.id;\n        fail_count := fail_count + 1;\n        -- The subtransaction rolls back the INSERT into target_table but\n        -- the INSERT into processing_errors (after EXCEPTION) succeeds.\n        -- Without the subtransaction, one bad row would abort the whole batch.\n    END;\n  END LOOP;\n\n  RETURN QUERY SELECT ok_count, fail_count, bid;\nEND;\n$$ LANGUAGE plpgsql;\n\n-- Usage\nSELECT * FROM process_batch(5000);\n--  processed | failed |              batch_id\n-- -----------+--------+--------------------------------------\n--       4998 |      2 | a1b2c3d4-...\n-- 4998 rows committed, 2 logged to processing_errors with their SQLSTATE.\n",[27,3222,3223,3228,3239,3262,3275,3291,3304,3316,3332,3336,3350,3354,3359,3364,3385,3408,3412,3417,3432,3447,3457,3461,3466,3475,3487,3498,3504,3512,3527,3531,3539,3544,3558,3583,3587,3620,3636,3640,3645,3657,3665,3682,3687,3717,3733,3738,3743,3748,3755,3763,3767,3779,3785,3794,3799,3805,3822,3828,3834,3840],{"__ignoreMap":189},[193,3224,3225],{"class":195,"line":196},[193,3226,3227],{"class":238},"-- Error log table for failed processing\n",[193,3229,3230,3232,3234,3237],{"class":195,"line":232},[193,3231,1821],{"class":199},[193,3233,1443],{"class":199},[193,3235,3236],{"class":206}," processing_errors",[193,3238,2678],{"class":210},[193,3240,3241,3244,3246,3249,3252,3254,3257,3260],{"class":195,"line":242},[193,3242,3243],{"class":210},"  id          ",[193,3245,288],{"class":199},[193,3247,3248],{"class":199}," GENERATED",[193,3250,3251],{"class":199}," ALWAYS",[193,3253,226],{"class":199},[193,3255,3256],{"class":199}," IDENTITY",[193,3258,3259],{"class":199}," PRIMARY KEY",[193,3261,1052],{"class":210},[193,3263,3264,3267,3270,3272],{"class":195,"line":267},[193,3265,3266],{"class":210},"  batch_id    UUID ",[193,3268,3269],{"class":199},"NOT NULL",[193,3271,1062],{"class":199},[193,3273,3274],{"class":210}," gen_random_uuid(),\n",[193,3276,3277,3280,3282,3285,3288],{"class":195,"line":282},[193,3278,3279],{"class":210},"  source_id   ",[193,3281,288],{"class":199},[193,3283,3284],{"class":199}," NOT NULL",[193,3286,3287],{"class":210},",           ",[193,3289,3290],{"class":238},"-- ID of the row that failed\n",[193,3292,3293,3296,3298,3301],{"class":195,"line":297},[193,3294,3295],{"class":210},"  error_code  ",[193,3297,214],{"class":199},[193,3299,3300],{"class":210},",                      ",[193,3302,3303],{"class":238},"-- SQLSTATE\n",[193,3305,3306,3309,3311,3313],{"class":195,"line":309},[193,3307,3308],{"class":210},"  error_msg   ",[193,3310,214],{"class":199},[193,3312,3300],{"class":210},[193,3314,3315],{"class":238},"-- SQLERRM\n",[193,3317,3318,3321,3323,3325,3327,3329],{"class":195,"line":318},[193,3319,3320],{"class":210},"  failed_at   ",[193,3322,1459],{"class":199},[193,3324,3284],{"class":199},[193,3326,1062],{"class":199},[193,3328,2704],{"class":199},[193,3330,3331],{"class":210},"()\n",[193,3333,3334],{"class":195,"line":345},[193,3335,1104],{"class":210},[193,3337,3338,3340,3342,3345,3347],{"class":195,"line":360},[193,3339,1821],{"class":199},[193,3341,1824],{"class":199},[193,3343,3344],{"class":206}," processing_errors_batch_idx",[193,3346,1830],{"class":199},[193,3348,3349],{"class":210}," processing_errors(batch_id);\n",[193,3351,3352],{"class":195,"line":369},[193,3353,579],{"emptyLinePlaceholder":578},[193,3355,3356],{"class":195,"line":375},[193,3357,3358],{"class":238},"-- The batch processor: processes rows from a staging table, catches\n",[193,3360,3361],{"class":195,"line":381},[193,3362,3363],{"class":238},"-- per-row errors (subtransaction per row), logs failures, returns summary.\n",[193,3365,3366,3368,3370,3373,3376,3378,3380,3383],{"class":195,"line":387},[193,3367,200],{"class":199},[193,3369,203],{"class":199},[193,3371,3372],{"class":206}," process_batch",[193,3374,3375],{"class":210},"(batch_limit ",[193,3377,446],{"class":199},[193,3379,1062],{"class":199},[193,3381,3382],{"class":257}," 1000",[193,3384,1258],{"class":210},[193,3386,3387,3389,3391,3394,3396,3399,3401,3404,3406],{"class":195,"line":393},[193,3388,220],{"class":199},[193,3390,1443],{"class":199},[193,3392,3393],{"class":210}," (processed ",[193,3395,446],{"class":199},[193,3397,3398],{"class":210},", failed ",[193,3400,446],{"class":199},[193,3402,3403],{"class":210},", batch_id UUID) ",[193,3405,1075],{"class":199},[193,3407,229],{"class":210},[193,3409,3410],{"class":195,"line":402},[193,3411,461],{"class":199},[193,3413,3414],{"class":195,"line":411},[193,3415,3416],{"class":210},"  rec RECORD;\n",[193,3418,3419,3422,3424,3426,3428,3430],{"class":195,"line":615},[193,3420,3421],{"class":210},"  ok_count  ",[193,3423,446],{"class":199},[193,3425,251],{"class":210},[193,3427,254],{"class":199},[193,3429,258],{"class":257},[193,3431,408],{"class":210},[193,3433,3434,3437,3439,3441,3443,3445],{"class":195,"line":633},[193,3435,3436],{"class":210},"  fail_count ",[193,3438,446],{"class":199},[193,3440,251],{"class":210},[193,3442,254],{"class":199},[193,3444,258],{"class":257},[193,3446,408],{"class":210},[193,3448,3449,3452,3454],{"class":195,"line":642},[193,3450,3451],{"class":210},"  bid       UUID :",[193,3453,254],{"class":199},[193,3455,3456],{"class":210}," gen_random_uuid();\n",[193,3458,3459],{"class":195,"line":649},[193,3460,490],{"class":199},[193,3462,3463],{"class":195,"line":654},[193,3464,3465],{"class":238},"  -- Iterate over unprocessed rows in staging. FOR ... IN SELECT is cursor-like.\n",[193,3467,3468,3470,3473],{"class":195,"line":660},[193,3469,829],{"class":199},[193,3471,3472],{"class":210}," rec ",[193,3474,892],{"class":199},[193,3476,3477,3479,3482,3484],{"class":195,"line":672},[193,3478,898],{"class":199},[193,3480,3481],{"class":210}," id, payload ",[193,3483,1640],{"class":199},[193,3485,3486],{"class":210}," staging_table\n",[193,3488,3489,3491,3493,3495],{"class":195,"line":678},[193,3490,1524],{"class":199},[193,3492,2384],{"class":199},[193,3494,732],{"class":199},[193,3496,3497],{"class":521}," 'pending'\n",[193,3499,3500,3502],{"class":195,"line":697},[193,3501,1565],{"class":199},[193,3503,2730],{"class":210},[193,3505,3506,3509],{"class":195,"line":717},[193,3507,3508],{"class":199},"    LIMIT",[193,3510,3511],{"class":210}," batch_limit\n",[193,3513,3514,3517,3519,3521,3524],{"class":195,"line":743},[193,3515,3516],{"class":199},"    FOR",[193,3518,2749],{"class":199},[193,3520,2752],{"class":199},[193,3522,3523],{"class":210}," LOCKED          ",[193,3525,3526],{"class":238},"-- skip rows other workers are processing\n",[193,3528,3529],{"class":195,"line":749},[193,3530,675],{"class":199},[193,3532,3533,3536],{"class":195,"line":759},[193,3534,3535],{"class":199},"    BEGIN",[193,3537,3538],{"class":238},"                           -- ← subtransaction (savepoint) per row\n",[193,3540,3541],{"class":195,"line":764},[193,3542,3543],{"class":238},"      -- Business logic: validate and insert into the target table\n",[193,3545,3546,3549,3552,3555],{"class":195,"line":770},[193,3547,3548],{"class":199},"      INSERT INTO",[193,3550,3551],{"class":210}," target_table (",[193,3553,3554],{"class":199},"data",[193,3556,3557],{"class":210},", source_id)\n",[193,3559,3560,3563,3565,3568,3570,3573,3575,3577,3579,3581],{"class":195,"line":791},[193,3561,3562],{"class":199},"      VALUES",[193,3564,2361],{"class":210},[193,3566,3567],{"class":257},"rec",[193,3569,327],{"class":210},[193,3571,3572],{"class":257},"payload",[193,3574,1201],{"class":210},[193,3576,3567],{"class":257},[193,3578,327],{"class":210},[193,3580,330],{"class":257},[193,3582,1104],{"class":210},[193,3584,3585],{"class":195,"line":806},[193,3586,579],{"emptyLinePlaceholder":578},[193,3588,3589,3592,3595,3597,3599,3601,3604,3607,3609,3611,3614,3616,3618],{"class":195,"line":815},[193,3590,3591],{"class":199},"      UPDATE",[193,3593,3594],{"class":210}," staging_table ",[193,3596,2541],{"class":199},[193,3598,2384],{"class":199},[193,3600,732],{"class":199},[193,3602,3603],{"class":521}," 'done'",[193,3605,3606],{"class":199}," WHERE",[193,3608,2686],{"class":210},[193,3610,254],{"class":199},[193,3612,3613],{"class":257}," rec",[193,3615,327],{"class":210},[193,3617,330],{"class":257},[193,3619,408],{"class":210},[193,3621,3622,3625,3627,3630,3632,3634],{"class":195,"line":820},[193,3623,3624],{"class":210},"      ok_count :",[193,3626,254],{"class":199},[193,3628,3629],{"class":210}," ok_count ",[193,3631,689],{"class":199},[193,3633,692],{"class":257},[193,3635,408],{"class":210},[193,3637,3638],{"class":195,"line":826},[193,3639,579],{"emptyLinePlaceholder":578},[193,3641,3642],{"class":195,"line":850},[193,3643,3644],{"class":210},"    EXCEPTION\n",[193,3646,3647,3650,3652,3654],{"class":195,"line":856},[193,3648,3649],{"class":199},"      WHEN",[193,3651,3076],{"class":210},[193,3653,3079],{"class":199},[193,3655,3656],{"class":238},"              -- catch per-row errors, log, continue\n",[193,3658,3659,3662],{"class":195,"line":864},[193,3660,3661],{"class":199},"        INSERT INTO",[193,3663,3664],{"class":210}," processing_errors (batch_id, source_id, error_code, error_msg)\n",[193,3666,3667,3670,3673,3675,3677,3679],{"class":195,"line":873},[193,3668,3669],{"class":199},"        VALUES",[193,3671,3672],{"class":210}," (bid, ",[193,3674,3567],{"class":257},[193,3676,327],{"class":210},[193,3678,330],{"class":257},[193,3680,3681],{"class":210},", SQLSTATE, SQLERRM);\n",[193,3683,3684],{"class":195,"line":878},[193,3685,3686],{"class":238},"        -- SQLSTATE: 5-char SQLSTATE code. SQLERRM: error message text.\n",[193,3688,3689,3692,3694,3696,3698,3700,3703,3705,3707,3709,3711,3713,3715],{"class":195,"line":884},[193,3690,3691],{"class":199},"        UPDATE",[193,3693,3594],{"class":210},[193,3695,2541],{"class":199},[193,3697,2384],{"class":199},[193,3699,732],{"class":199},[193,3701,3702],{"class":521}," 'error'",[193,3704,3606],{"class":199},[193,3706,2686],{"class":210},[193,3708,254],{"class":199},[193,3710,3613],{"class":257},[193,3712,327],{"class":210},[193,3714,330],{"class":257},[193,3716,408],{"class":210},[193,3718,3719,3722,3724,3727,3729,3731],{"class":195,"line":895},[193,3720,3721],{"class":210},"        fail_count :",[193,3723,254],{"class":199},[193,3725,3726],{"class":210}," fail_count ",[193,3728,689],{"class":199},[193,3730,692],{"class":257},[193,3732,408],{"class":210},[193,3734,3735],{"class":195,"line":921},[193,3736,3737],{"class":238},"        -- The subtransaction rolls back the INSERT into target_table but\n",[193,3739,3740],{"class":195,"line":926},[193,3741,3742],{"class":238},"        -- the INSERT into processing_errors (after EXCEPTION) succeeds.\n",[193,3744,3745],{"class":195,"line":932},[193,3746,3747],{"class":238},"        -- Without the subtransaction, one bad row would abort the whole batch.\n",[193,3749,3750,3753],{"class":195,"line":944},[193,3751,3752],{"class":199},"    END",[193,3754,408],{"class":210},[193,3756,3757,3759,3761],{"class":195,"line":953},[193,3758,568],{"class":199},[193,3760,754],{"class":199},[193,3762,408],{"class":210},[193,3764,3765],{"class":195,"line":958},[193,3766,579],{"emptyLinePlaceholder":578},[193,3768,3769,3771,3774,3776],{"class":195,"line":966},[193,3770,396],{"class":199},[193,3772,3773],{"class":210}," QUERY ",[193,3775,159],{"class":199},[193,3777,3778],{"class":210}," ok_count, fail_count, bid;\n",[193,3780,3781,3783],{"class":195,"line":973},[193,3782,405],{"class":199},[193,3784,408],{"class":210},[193,3786,3788,3790,3792],{"class":195,"line":3787},53,[193,3789,414],{"class":210},[193,3791,417],{"class":199},[193,3793,420],{"class":210},[193,3795,3797],{"class":195,"line":3796},54,[193,3798,579],{"emptyLinePlaceholder":578},[193,3800,3802],{"class":195,"line":3801},55,[193,3803,3804],{"class":238},"-- Usage\n",[193,3806,3808,3810,3812,3814,3817,3820],{"class":195,"line":3807},56,[193,3809,159],{"class":199},[193,3811,1156],{"class":199},[193,3813,904],{"class":199},[193,3815,3816],{"class":210}," process_batch(",[193,3818,3819],{"class":257},"5000",[193,3821,1104],{"class":210},[193,3823,3825],{"class":195,"line":3824},57,[193,3826,3827],{"class":238},"--  processed | failed |              batch_id\n",[193,3829,3831],{"class":195,"line":3830},58,[193,3832,3833],{"class":238},"-- -----------+--------+--------------------------------------\n",[193,3835,3837],{"class":195,"line":3836},59,[193,3838,3839],{"class":238},"--       4998 |      2 | a1b2c3d4-...\n",[193,3841,3843],{"class":195,"line":3842},60,[193,3844,3845],{"class":238},"-- 4998 rows committed, 2 logged to processing_errors with their SQLSTATE.\n",[44,3847,3849],{"id":3848},"anti-pattern-immutable-on-a-table-reading-function","Anti-Pattern: IMMUTABLE on a Table-Reading Function",[181,3851,3852],{"language":183},[185,3853,3855],{"className":187,"code":3854,"language":183,"meta":189,"style":189},"-- ❌ WRONG: this function reads from the users table, but is labeled\n-- IMMUTABLE. The planner trusts the label and caches the result.\n-- If used in an index, the index stores a value that goes STALE when\n-- the table changes. Silent data corruption.\nCREATE OR REPLACE FUNCTION full_name(uid BIGINT) RETURNS TEXT AS $$\nBEGIN\n  RETURN (SELECT first_name || ' ' || last_name FROM users WHERE id = uid);\n  --        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  --        This reads a TABLE. The result changes when the table changes.\n  --        This function is STABLE at best, NOT IMMUTABLE.\nEND;\n$$ LANGUAGE plpgsql IMMUTABLE;       -- ← LIE. The planner trusts this.\n\nCREATE INDEX users_full_name_idx ON users(full_name(id));\n-- The index stores full_name(id) at index time. When a user updates\n-- first_name, the index is NOT updated (the planner thinks IMMUTABLE =\n-- never changes). Queries via the index return STALE names.\n\n-- ✅ RIGHT: label it STABLE (reads a table, consistent within a statement).\n-- Do NOT use it in an index. For indexed computed values, use a generated\n-- column (the database maintains it on updates).\nCREATE OR REPLACE FUNCTION full_name(uid BIGINT) RETURNS TEXT AS $$\nBEGIN\n  RETURN (SELECT first_name || ' ' || last_name FROM users WHERE id = uid);\nEND;\n$$ LANGUAGE plpgsql STABLE;          -- ← honest. Can't be indexed, but correct.\n\n-- ✅ BEST: use a generated column for indexed computed values\nALTER TABLE users ADD COLUMN full_name TEXT\n  GENERATED ALWAYS AS (first_name || ' ' || last_name) STORED;\nCREATE INDEX users_full_name_idx ON users(full_name);\n-- The generated column is auto-updated on any change to first_name\u002Flast_name.\n-- The index stays in sync. No function, no volatility question.\n",[27,3856,3857,3862,3867,3872,3877,3902,3906,3941,3946,3951,3956,3962,3974,3978,3992,3997,4002,4007,4011,4016,4021,4026,4050,4054,4086,4092,4104,4108,4113,4131,4152,4165,4170],{"__ignoreMap":189},[193,3858,3859],{"class":195,"line":196},[193,3860,3861],{"class":238},"-- ❌ WRONG: this function reads from the users table, but is labeled\n",[193,3863,3864],{"class":195,"line":232},[193,3865,3866],{"class":238},"-- IMMUTABLE. The planner trusts the label and caches the result.\n",[193,3868,3869],{"class":195,"line":242},[193,3870,3871],{"class":238},"-- If used in an index, the index stores a value that goes STALE when\n",[193,3873,3874],{"class":195,"line":267},[193,3875,3876],{"class":238},"-- the table changes. Silent data corruption.\n",[193,3878,3879,3881,3883,3886,3888,3890,3892,3894,3896,3898,3900],{"class":195,"line":282},[193,3880,200],{"class":199},[193,3882,203],{"class":199},[193,3884,3885],{"class":206}," full_name",[193,3887,1762],{"class":210},[193,3889,1871],{"class":199},[193,3891,1874],{"class":199},[193,3893,217],{"class":210},[193,3895,220],{"class":199},[193,3897,223],{"class":199},[193,3899,226],{"class":199},[193,3901,229],{"class":210},[193,3903,3904],{"class":195,"line":297},[193,3905,490],{"class":199},[193,3907,3908,3910,3912,3914,3917,3919,3921,3924,3927,3929,3931,3933,3935,3937,3939],{"class":195,"line":309},[193,3909,396],{"class":199},[193,3911,2361],{"class":210},[193,3913,159],{"class":199},[193,3915,3916],{"class":210}," first_name ",[193,3918,2716],{"class":199},[193,3920,1065],{"class":521},[193,3922,3923],{"class":199}," ||",[193,3925,3926],{"class":210}," last_name ",[193,3928,1640],{"class":199},[193,3930,907],{"class":210},[193,3932,85],{"class":199},[193,3934,2686],{"class":210},[193,3936,254],{"class":199},[193,3938,1912],{"class":199},[193,3940,1104],{"class":210},[193,3942,3943],{"class":195,"line":318},[193,3944,3945],{"class":238},"  --        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",[193,3947,3948],{"class":195,"line":345},[193,3949,3950],{"class":238},"  --        This reads a TABLE. The result changes when the table changes.\n",[193,3952,3953],{"class":195,"line":360},[193,3954,3955],{"class":238},"  --        This function is STABLE at best, NOT IMMUTABLE.\n",[193,3957,3958,3960],{"class":195,"line":369},[193,3959,405],{"class":199},[193,3961,408],{"class":210},[193,3963,3964,3966,3968,3971],{"class":195,"line":375},[193,3965,414],{"class":210},[193,3967,417],{"class":199},[193,3969,3970],{"class":210}," plpgsql IMMUTABLE;       ",[193,3972,3973],{"class":238},"-- ← LIE. The planner trusts this.\n",[193,3975,3976],{"class":195,"line":381},[193,3977,579],{"emptyLinePlaceholder":578},[193,3979,3980,3982,3984,3987,3989],{"class":195,"line":387},[193,3981,1821],{"class":199},[193,3983,1824],{"class":199},[193,3985,3986],{"class":206}," users_full_name_idx",[193,3988,1830],{"class":199},[193,3990,3991],{"class":210}," users(full_name(id));\n",[193,3993,3994],{"class":195,"line":393},[193,3995,3996],{"class":238},"-- The index stores full_name(id) at index time. When a user updates\n",[193,3998,3999],{"class":195,"line":402},[193,4000,4001],{"class":238},"-- first_name, the index is NOT updated (the planner thinks IMMUTABLE =\n",[193,4003,4004],{"class":195,"line":411},[193,4005,4006],{"class":238},"-- never changes). Queries via the index return STALE names.\n",[193,4008,4009],{"class":195,"line":615},[193,4010,579],{"emptyLinePlaceholder":578},[193,4012,4013],{"class":195,"line":633},[193,4014,4015],{"class":238},"-- ✅ RIGHT: label it STABLE (reads a table, consistent within a statement).\n",[193,4017,4018],{"class":195,"line":642},[193,4019,4020],{"class":238},"-- Do NOT use it in an index. For indexed computed values, use a generated\n",[193,4022,4023],{"class":195,"line":649},[193,4024,4025],{"class":238},"-- column (the database maintains it on updates).\n",[193,4027,4028,4030,4032,4034,4036,4038,4040,4042,4044,4046,4048],{"class":195,"line":654},[193,4029,200],{"class":199},[193,4031,203],{"class":199},[193,4033,3885],{"class":206},[193,4035,1762],{"class":210},[193,4037,1871],{"class":199},[193,4039,1874],{"class":199},[193,4041,217],{"class":210},[193,4043,220],{"class":199},[193,4045,223],{"class":199},[193,4047,226],{"class":199},[193,4049,229],{"class":210},[193,4051,4052],{"class":195,"line":660},[193,4053,490],{"class":199},[193,4055,4056,4058,4060,4062,4064,4066,4068,4070,4072,4074,4076,4078,4080,4082,4084],{"class":195,"line":672},[193,4057,396],{"class":199},[193,4059,2361],{"class":210},[193,4061,159],{"class":199},[193,4063,3916],{"class":210},[193,4065,2716],{"class":199},[193,4067,1065],{"class":521},[193,4069,3923],{"class":199},[193,4071,3926],{"class":210},[193,4073,1640],{"class":199},[193,4075,907],{"class":210},[193,4077,85],{"class":199},[193,4079,2686],{"class":210},[193,4081,254],{"class":199},[193,4083,1912],{"class":199},[193,4085,1104],{"class":210},[193,4087,4088,4090],{"class":195,"line":678},[193,4089,405],{"class":199},[193,4091,408],{"class":210},[193,4093,4094,4096,4098,4101],{"class":195,"line":697},[193,4095,414],{"class":210},[193,4097,417],{"class":199},[193,4099,4100],{"class":210}," plpgsql STABLE;          ",[193,4102,4103],{"class":238},"-- ← honest. Can't be indexed, but correct.\n",[193,4105,4106],{"class":195,"line":717},[193,4107,579],{"emptyLinePlaceholder":578},[193,4109,4110],{"class":195,"line":743},[193,4111,4112],{"class":238},"-- ✅ BEST: use a generated column for indexed computed values\n",[193,4114,4115,4118,4120,4122,4125,4128],{"class":195,"line":749},[193,4116,4117],{"class":199},"ALTER",[193,4119,1443],{"class":199},[193,4121,907],{"class":210},[193,4123,4124],{"class":199},"ADD",[193,4126,4127],{"class":210}," COLUMN full_name ",[193,4129,4130],{"class":199},"TEXT\n",[193,4132,4133,4136,4138,4140,4143,4145,4147,4149],{"class":195,"line":759},[193,4134,4135],{"class":199},"  GENERATED",[193,4137,3251],{"class":199},[193,4139,226],{"class":199},[193,4141,4142],{"class":210}," (first_name ",[193,4144,2716],{"class":199},[193,4146,1065],{"class":521},[193,4148,3923],{"class":199},[193,4150,4151],{"class":210}," last_name) STORED;\n",[193,4153,4154,4156,4158,4160,4162],{"class":195,"line":764},[193,4155,1821],{"class":199},[193,4157,1824],{"class":199},[193,4159,3986],{"class":206},[193,4161,1830],{"class":199},[193,4163,4164],{"class":210}," users(full_name);\n",[193,4166,4167],{"class":195,"line":770},[193,4168,4169],{"class":238},"-- The generated column is auto-updated on any change to first_name\u002Flast_name.\n",[193,4171,4172],{"class":195,"line":791},[193,4173,4174],{"class":238},"-- The index stays in sync. No function, no volatility question.\n",[44,4176,4178],{"id":4177},"anti-pattern-complex-business-logic-in-functions","Anti-Pattern: Complex Business Logic in Functions",[181,4180,4181],{"language":183},[185,4182,4184],{"className":187,"code":4183,"language":183,"meta":189,"style":189},"-- ❌ WRONG: burying business logic (order pricing, discounts, tax rules)\n-- in a PL\u002FpgSQL function. It's invisible to the application, can't be\n-- unit-tested without a database, can't be debugged with a debugger,\n-- and the version control is in migration files, not the app repo.\nCREATE OR REPLACE FUNCTION calculate_order_total(order_id BIGINT) RETURNS NUMERIC AS $$\nDECLARE\n  total NUMERIC;\nBEGIN\n  -- 100 lines of pricing rules, discount logic, tax calculation...\n  -- This is business logic. It belongs in the application, where it's\n  -- testable, reviewable, and version-controlled alongside the app.\n  SELECT sum(quantity * unit_price) INTO total FROM order_items WHERE order_id = calculate_order_total.order_id;\n  -- apply discounts, tax, etc. (omitted for brevity)\n  RETURN total;\nEND;\n$$ LANGUAGE plpgsql STABLE;\n\n-- ✅ RIGHT: put business logic in the application. Use functions for:\n--   - Pure data transforms (normalized_email, full_name)\n--   - Encapsulating a complex query (active_orders_for)\n--   - Computation that must be in the DB (trigger logic, audit)\n-- The app calls simple queries; the logic lives in the app's code.\n",[27,4185,4186,4191,4196,4201,4206,4230,4234,4243,4247,4252,4257,4262,4302,4307,4314,4320,4328,4332,4337,4342,4347,4352],{"__ignoreMap":189},[193,4187,4188],{"class":195,"line":196},[193,4189,4190],{"class":238},"-- ❌ WRONG: burying business logic (order pricing, discounts, tax rules)\n",[193,4192,4193],{"class":195,"line":232},[193,4194,4195],{"class":238},"-- in a PL\u002FpgSQL function. It's invisible to the application, can't be\n",[193,4197,4198],{"class":195,"line":242},[193,4199,4200],{"class":238},"-- unit-tested without a database, can't be debugged with a debugger,\n",[193,4202,4203],{"class":195,"line":267},[193,4204,4205],{"class":238},"-- and the version control is in migration files, not the app repo.\n",[193,4207,4208,4210,4212,4215,4218,4220,4222,4224,4226,4228],{"class":195,"line":282},[193,4209,200],{"class":199},[193,4211,203],{"class":199},[193,4213,4214],{"class":206}," calculate_order_total",[193,4216,4217],{"class":210},"(order_id ",[193,4219,288],{"class":199},[193,4221,217],{"class":210},[193,4223,220],{"class":199},[193,4225,1265],{"class":199},[193,4227,226],{"class":199},[193,4229,229],{"class":210},[193,4231,4232],{"class":195,"line":297},[193,4233,461],{"class":199},[193,4235,4236,4239,4241],{"class":195,"line":309},[193,4237,4238],{"class":210},"  total ",[193,4240,1241],{"class":199},[193,4242,408],{"class":210},[193,4244,4245],{"class":195,"line":318},[193,4246,490],{"class":199},[193,4248,4249],{"class":195,"line":345},[193,4250,4251],{"class":238},"  -- 100 lines of pricing rules, discount logic, tax calculation...\n",[193,4253,4254],{"class":195,"line":360},[193,4255,4256],{"class":238},"  -- This is business logic. It belongs in the application, where it's\n",[193,4258,4259],{"class":195,"line":369},[193,4260,4261],{"class":238},"  -- testable, reviewable, and version-controlled alongside the app.\n",[193,4263,4264,4266,4268,4271,4273,4276,4279,4281,4283,4286,4288,4291,4293,4295,4297,4300],{"class":195,"line":375},[193,4265,1782],{"class":199},[193,4267,1634],{"class":257},[193,4269,4270],{"class":210},"(quantity ",[193,4272,782],{"class":199},[193,4274,4275],{"class":210}," unit_price) ",[193,4277,4278],{"class":199},"INTO",[193,4280,2860],{"class":210},[193,4282,1640],{"class":199},[193,4284,4285],{"class":210}," order_items ",[193,4287,85],{"class":199},[193,4289,4290],{"class":210}," order_id ",[193,4292,254],{"class":199},[193,4294,4214],{"class":257},[193,4296,327],{"class":210},[193,4298,4299],{"class":257},"order_id",[193,4301,408],{"class":210},[193,4303,4304],{"class":195,"line":381},[193,4305,4306],{"class":238},"  -- apply discounts, tax, etc. (omitted for brevity)\n",[193,4308,4309,4311],{"class":195,"line":387},[193,4310,396],{"class":199},[193,4312,4313],{"class":210}," total;\n",[193,4315,4316,4318],{"class":195,"line":393},[193,4317,405],{"class":199},[193,4319,408],{"class":210},[193,4321,4322,4324,4326],{"class":195,"line":402},[193,4323,414],{"class":210},[193,4325,417],{"class":199},[193,4327,1597],{"class":210},[193,4329,4330],{"class":195,"line":411},[193,4331,579],{"emptyLinePlaceholder":578},[193,4333,4334],{"class":195,"line":615},[193,4335,4336],{"class":238},"-- ✅ RIGHT: put business logic in the application. Use functions for:\n",[193,4338,4339],{"class":195,"line":633},[193,4340,4341],{"class":238},"--   - Pure data transforms (normalized_email, full_name)\n",[193,4343,4344],{"class":195,"line":642},[193,4345,4346],{"class":238},"--   - Encapsulating a complex query (active_orders_for)\n",[193,4348,4349],{"class":195,"line":649},[193,4350,4351],{"class":238},"--   - Computation that must be in the DB (trigger logic, audit)\n",[193,4353,4354],{"class":195,"line":654},[193,4355,4356],{"class":238},"-- The app calls simple queries; the logic lives in the app's code.\n",[44,4358,4360],{"id":4359},"security-definer-and-search_path-hijacking","SECURITY DEFINER and search_path Hijacking",[181,4362,4363],{"language":183},[185,4364,4366],{"className":187,"code":4365,"language":183,"meta":189,"style":189},"-- SECURITY INVOKER (default): runs with the CALLER's privileges.\n-- SECURITY DEFINER: runs with the OWNER's privileges. Useful for\n-- allowing limited access to tables the caller can't see directly.\n\nCREATE OR REPLACE FUNCTION get_my_salary(emp_id BIGINT) RETURNS NUMERIC\nAS $$\nBEGIN\n  -- Authorization check: only the employee or HR can see a salary\n  IF emp_id != current_setting('app.user_id')::bigint\n     AND NOT has_role(current_user, 'hr', 'member') THEN\n    RAISE EXCEPTION 'Not authorized to view salary for employee %', emp_id;\n  END IF;\n  RETURN (SELECT salary FROM salaries WHERE employee_id = emp_id);\nEND;\n$$ LANGUAGE plpgsql\n   SECURITY DEFINER               -- runs as the function's OWNER (who CAN read salaries)\n   SET search_path = hr, pg_temp; -- CRITICAL: pin the search_path\n\n-- Revoke direct access; force access through the function\nREVOKE ALL ON salaries FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION get_my_salary(BIGINT) TO authenticated_users;\n-- Users can't SELECT from salaries, but can call get_my_salary (which checks auth).\n\n-- ── The search_path hijack attack ──────────────────────────────\n-- If a SECURITY DEFINER function doesn't set search_path, a malicious\n-- user can create a malicious function\u002Ftable in a schema earlier in\n-- the search_path, and the DEFINER function calls the MALICIOUS object\n-- instead of the intended one — running with the OWNER's privileges.\n-- Example: if the function calls lower() and the attacker creates a\n-- malicious lower() in pg_temp (which is early in the default\n-- search_path), the DEFINER function calls the attacker's lower().\n-- Fix: ALWAYS set search_path explicitly in SECURITY DEFINER functions.\n-- `SET search_path = my_schema, pg_temp` puts pg_temp LAST (safe).\n",[27,4367,4368,4373,4378,4383,4387,4408,4414,4418,4423,4445,4468,4479,4487,4513,4519,4528,4539,4555,4559,4564,4582,4607,4612,4616,4621,4626,4631,4636,4641,4646,4651,4656,4661],{"__ignoreMap":189},[193,4369,4370],{"class":195,"line":196},[193,4371,4372],{"class":238},"-- SECURITY INVOKER (default): runs with the CALLER's privileges.\n",[193,4374,4375],{"class":195,"line":232},[193,4376,4377],{"class":238},"-- SECURITY DEFINER: runs with the OWNER's privileges. Useful for\n",[193,4379,4380],{"class":195,"line":242},[193,4381,4382],{"class":238},"-- allowing limited access to tables the caller can't see directly.\n",[193,4384,4385],{"class":195,"line":267},[193,4386,579],{"emptyLinePlaceholder":578},[193,4388,4389,4391,4393,4396,4399,4401,4403,4405],{"class":195,"line":282},[193,4390,200],{"class":199},[193,4392,203],{"class":199},[193,4394,4395],{"class":206}," get_my_salary",[193,4397,4398],{"class":210},"(emp_id ",[193,4400,288],{"class":199},[193,4402,217],{"class":210},[193,4404,220],{"class":199},[193,4406,4407],{"class":199}," NUMERIC\n",[193,4409,4410,4412],{"class":195,"line":297},[193,4411,1075],{"class":199},[193,4413,229],{"class":210},[193,4415,4416],{"class":195,"line":309},[193,4417,490],{"class":199},[193,4419,4420],{"class":195,"line":318},[193,4421,4422],{"class":238},"  -- Authorization check: only the employee or HR can see a salary\n",[193,4424,4425,4427,4430,4433,4436,4439,4442],{"class":195,"line":345},[193,4426,500],{"class":199},[193,4428,4429],{"class":210}," emp_id ",[193,4431,4432],{"class":199},"!=",[193,4434,4435],{"class":210}," current_setting(",[193,4437,4438],{"class":521},"'app.user_id'",[193,4440,4441],{"class":210},")::",[193,4443,4444],{"class":199},"bigint\n",[193,4446,4447,4450,4453,4456,4459,4461,4464,4466],{"class":195,"line":360},[193,4448,4449],{"class":199},"     AND",[193,4451,4452],{"class":199}," NOT",[193,4454,4455],{"class":210}," has_role(current_user, ",[193,4457,4458],{"class":521},"'hr'",[193,4460,1201],{"class":210},[193,4462,4463],{"class":521},"'member'",[193,4465,217],{"class":210},[193,4467,3056],{"class":199},[193,4469,4470,4473,4476],{"class":195,"line":369},[193,4471,4472],{"class":210},"    RAISE EXCEPTION ",[193,4474,4475],{"class":521},"'Not authorized to view salary for employee %'",[193,4477,4478],{"class":210},", emp_id;\n",[193,4480,4481,4483,4485],{"class":195,"line":375},[193,4482,568],{"class":199},[193,4484,571],{"class":199},[193,4486,408],{"class":210},[193,4488,4489,4491,4493,4495,4498,4500,4503,4505,4508,4510],{"class":195,"line":381},[193,4490,396],{"class":199},[193,4492,2361],{"class":210},[193,4494,159],{"class":199},[193,4496,4497],{"class":210}," salary ",[193,4499,1640],{"class":199},[193,4501,4502],{"class":210}," salaries ",[193,4504,85],{"class":199},[193,4506,4507],{"class":210}," employee_id ",[193,4509,254],{"class":199},[193,4511,4512],{"class":210}," emp_id);\n",[193,4514,4515,4517],{"class":195,"line":387},[193,4516,405],{"class":199},[193,4518,408],{"class":210},[193,4520,4521,4523,4525],{"class":195,"line":393},[193,4522,414],{"class":210},[193,4524,417],{"class":199},[193,4526,4527],{"class":210}," plpgsql\n",[193,4529,4530,4533,4536],{"class":195,"line":402},[193,4531,4532],{"class":199},"   SECURITY",[193,4534,4535],{"class":210}," DEFINER               ",[193,4537,4538],{"class":238},"-- runs as the function's OWNER (who CAN read salaries)\n",[193,4540,4541,4544,4547,4549,4552],{"class":195,"line":411},[193,4542,4543],{"class":199},"   SET",[193,4545,4546],{"class":210}," search_path ",[193,4548,254],{"class":199},[193,4550,4551],{"class":210}," hr, pg_temp; ",[193,4553,4554],{"class":238},"-- CRITICAL: pin the search_path\n",[193,4556,4557],{"class":195,"line":615},[193,4558,579],{"emptyLinePlaceholder":578},[193,4560,4561],{"class":195,"line":633},[193,4562,4563],{"class":238},"-- Revoke direct access; force access through the function\n",[193,4565,4566,4569,4572,4575,4577,4579],{"class":195,"line":642},[193,4567,4568],{"class":199},"REVOKE",[193,4570,4571],{"class":210}," ALL ",[193,4573,4574],{"class":199},"ON",[193,4576,4502],{"class":210},[193,4578,1640],{"class":199},[193,4580,4581],{"class":210}," PUBLIC;\n",[193,4583,4584,4587,4590,4592,4594,4597,4599,4601,4604],{"class":195,"line":649},[193,4585,4586],{"class":199},"GRANT",[193,4588,4589],{"class":199}," EXECUTE",[193,4591,1830],{"class":199},[193,4593,203],{"class":199},[193,4595,4596],{"class":210}," get_my_salary(",[193,4598,288],{"class":199},[193,4600,217],{"class":210},[193,4602,4603],{"class":199},"TO",[193,4605,4606],{"class":210}," authenticated_users;\n",[193,4608,4609],{"class":195,"line":654},[193,4610,4611],{"class":238},"-- Users can't SELECT from salaries, but can call get_my_salary (which checks auth).\n",[193,4613,4614],{"class":195,"line":660},[193,4615,579],{"emptyLinePlaceholder":578},[193,4617,4618],{"class":195,"line":672},[193,4619,4620],{"class":238},"-- ── The search_path hijack attack ──────────────────────────────\n",[193,4622,4623],{"class":195,"line":678},[193,4624,4625],{"class":238},"-- If a SECURITY DEFINER function doesn't set search_path, a malicious\n",[193,4627,4628],{"class":195,"line":697},[193,4629,4630],{"class":238},"-- user can create a malicious function\u002Ftable in a schema earlier in\n",[193,4632,4633],{"class":195,"line":717},[193,4634,4635],{"class":238},"-- the search_path, and the DEFINER function calls the MALICIOUS object\n",[193,4637,4638],{"class":195,"line":743},[193,4639,4640],{"class":238},"-- instead of the intended one — running with the OWNER's privileges.\n",[193,4642,4643],{"class":195,"line":749},[193,4644,4645],{"class":238},"-- Example: if the function calls lower() and the attacker creates a\n",[193,4647,4648],{"class":195,"line":759},[193,4649,4650],{"class":238},"-- malicious lower() in pg_temp (which is early in the default\n",[193,4652,4653],{"class":195,"line":764},[193,4654,4655],{"class":238},"-- search_path), the DEFINER function calls the attacker's lower().\n",[193,4657,4658],{"class":195,"line":770},[193,4659,4660],{"class":238},"-- Fix: ALWAYS set search_path explicitly in SECURITY DEFINER functions.\n",[193,4662,4663],{"class":195,"line":791},[193,4664,4665],{"class":238},"-- `SET search_path = my_schema, pg_temp` puts pg_temp LAST (safe).\n",[44,4667,4669],{"id":4668},"dollar-quoting","Dollar-Quoting",[181,4671,4672],{"language":183},[185,4673,4675],{"className":187,"code":4674,"language":183,"meta":189,"style":189},"-- Function bodies are string literals. If the body contains single quotes\n-- (e.g., string literals in SQL), you'd need to escape them: ''.\n-- Dollar-quoting $$ ... $$ avoids this: no escaping needed inside $$.\n-- Use tags to nest: $outer$ ... $inner$ ... $inner$ ... $outer$.\nCREATE FUNCTION example() RETURNS void AS $$\nBEGIN\n  -- No need to escape the single quotes here:\n  EXECUTE 'INSERT INTO log (msg) VALUES (''hello'')';  -- escaped: ugly\n  -- With nested dollar-quoting:\n  EXECUTE $insert$INSERT INTO log (msg) VALUES ('hello')$insert$;  -- clean\nEND;\n$$ LANGUAGE plpgsql;\n",[27,4676,4677,4682,4687,4692,4697,4716,4720,4725,4738,4743,4783,4789],{"__ignoreMap":189},[193,4678,4679],{"class":195,"line":196},[193,4680,4681],{"class":238},"-- Function bodies are string literals. If the body contains single quotes\n",[193,4683,4684],{"class":195,"line":232},[193,4685,4686],{"class":238},"-- (e.g., string literals in SQL), you'd need to escape them: ''.\n",[193,4688,4689],{"class":195,"line":242},[193,4690,4691],{"class":238},"-- Dollar-quoting $$ ... $$ avoids this: no escaping needed inside $$.\n",[193,4693,4694],{"class":195,"line":267},[193,4695,4696],{"class":238},"-- Use tags to nest: $outer$ ... $inner$ ... $inner$ ... $outer$.\n",[193,4698,4699,4701,4703,4705,4707,4709,4712,4714],{"class":195,"line":282},[193,4700,1821],{"class":199},[193,4702,203],{"class":199},[193,4704,207],{"class":206},[193,4706,2707],{"class":210},[193,4708,220],{"class":199},[193,4710,4711],{"class":210}," void ",[193,4713,1075],{"class":199},[193,4715,229],{"class":210},[193,4717,4718],{"class":195,"line":297},[193,4719,490],{"class":199},[193,4721,4722],{"class":195,"line":309},[193,4723,4724],{"class":238},"  -- No need to escape the single quotes here:\n",[193,4726,4727,4730,4733,4735],{"class":195,"line":318},[193,4728,4729],{"class":199},"  EXECUTE",[193,4731,4732],{"class":521}," 'INSERT INTO log (msg) VALUES (''hello'')'",[193,4734,2128],{"class":210},[193,4736,4737],{"class":238},"-- escaped: ugly\n",[193,4739,4740],{"class":195,"line":345},[193,4741,4742],{"class":238},"  -- With nested dollar-quoting:\n",[193,4744,4745,4747,4750,4753,4756,4759,4762,4765,4767,4769,4772,4775,4777,4780],{"class":195,"line":360},[193,4746,4729],{"class":199},[193,4748,4749],{"class":210}," $",[193,4751,4752],{"class":199},"insert",[193,4754,4755],{"class":210},"$",[193,4757,4758],{"class":199},"INSERT INTO",[193,4760,4761],{"class":257}," log",[193,4763,4764],{"class":210}," (msg) ",[193,4766,1994],{"class":199},[193,4768,2361],{"class":210},[193,4770,4771],{"class":521},"'hello'",[193,4773,4774],{"class":210},")$",[193,4776,4752],{"class":199},[193,4778,4779],{"class":210},"$;  ",[193,4781,4782],{"class":238},"-- clean\n",[193,4784,4785,4787],{"class":195,"line":369},[193,4786,405],{"class":199},[193,4788,408],{"class":210},[193,4790,4791,4793,4795],{"class":195,"line":375},[193,4792,414],{"class":210},[193,4794,417],{"class":199},[193,4796,420],{"class":210},[44,4798,4800],{"id":4799},"tips-tricks","💡 Tips & Tricks",[4802,4803,4804,4827,4850,4865,4891,4907,4917,4932],"ul",{},[4805,4806,4807,4810,4811,4813,4814,4816,4817,4819,4820,4823,4824,4826],"li",{},[18,4808,4809],{},"Idiom",": use ",[27,4812,146],{}," for pure functions (no table reads, no time\u002Frandom dependence) — it unlocks expression indexes and constant folding. ",[27,4815,1697],{}," for read-only functions consistent within a statement. ",[27,4818,1713],{}," (default) for functions with side effects. ",[18,4821,4822],{},"Always specify volatility explicitly"," — the default is ",[27,4825,1713],{},", and forgetting the label silently disables all optimizations.",[4805,4828,4829,4831,4832,2361,4835,4838,4839,34,4841,4843,4844,4846,4847,4849],{},[18,4830,4809],{},": prefer ",[18,4833,4834],{},"SQL functions",[27,4836,4837],{},"LANGUAGE sql",", no ",[27,4840,363],{},[27,4842,405],{},") for simple one-query functions — the planner can inline them (substitute the body into the calling query), enabling predicate pushdown and accurate row estimates. PL\u002FpgSQL functions are opaque black boxes to the planner. Check inlining with ",[27,4845,2557],{}," (look for the underlying table access, not just ",[27,4848,2561],{},").",[4805,4851,4852,4810,4854,4857,4858,4860,4861,4864],{},[18,4853,4809],{},[27,4855,4856],{},"RETURN QUERY"," for set-returning functions — it appends a query's result to the function's output in one statement. Multiple ",[27,4859,4856],{}," calls concatenate results. For procedural row generation, use ",[27,4862,4863],{},"RETURN NEXT"," (one row per call).",[4805,4866,4867,4810,4869,4872,4873,4876,4877,4880,4881,4884,4885,34,4888,4890],{},[18,4868,4809],{},[27,4870,4871],{},"DEFAULT"," parameters for optional args — ",[27,4874,4875],{},"CREATE FUNCTION f(a INT, b INT DEFAULT 10)"," lets callers write ",[27,4878,4879],{},"f(5)"," or ",[27,4882,4883],{},"f(5, 20)",". Combined with ",[27,4886,4887],{},"OUT",[27,4889,110],{}," params, this reduces the number of function overloads needed.",[4805,4892,4893,4810,4895,4898,4899,4902,4903,4906],{},[18,4894,4809],{},[27,4896,4897],{},"STRICT"," (aka ",[27,4900,4901],{},"RETURNS NULL ON NULL INPUT",") for functions that should short-circuit on NULL — saves a function call and simplifies NULL handling (no ",[27,4904,4905],{},"IF a IS NULL"," boilerplate).",[4805,4908,4909,4912,4913,4916],{},[18,4910,4911],{},"Performance",": ",[27,4914,4915],{},"EXCEPTION"," blocks create a subtransaction (savepoint) per call — expensive even when no exception occurs. Don't wrap hot-path functions in exception handlers \"just in case.\" Handle specific expected errors only, and only in functions that genuinely need per-row error isolation (batch processing).",[4805,4918,4919,4921,4922,4925,4926,4928,4929,4931],{},[18,4920,4809],{},": use procedures (",[27,4923,4924],{},"CALL ...",") for multi-step batch jobs that need ",[27,4927,33],{}," per batch — functions can't commit, so a million-row archival in a function is one giant transaction (huge WAL, long lock holds, no resume). A procedure with a loop + ",[27,4930,33],{}," keeps transactions short and is resumable on restart.",[4805,4933,4934,4937,4938,4941,4942,4945,4946,4949,4950,4953],{},[18,4935,4936],{},"Security",": always set ",[27,4939,4940],{},"search_path"," explicitly in ",[27,4943,4944],{},"SECURITY DEFINER"," functions (",[27,4947,4948],{},"SET search_path = my_schema, pg_temp",") — prevents search-path hijacking where a malicious user creates a shadowing object in ",[27,4951,4952],{},"pg_temp"," that runs with the owner's privileges.",[44,4955,4957],{"id":4956},"️-edge-cases-gotchas","⚠️ Edge Cases & Gotchas",[4802,4959,4960,4974,4987,5001,5007,5020,5041,5060,5084,5101,5116,5138],{},[4805,4961,4962,4967,4968,4970,4971,4973],{},[18,4963,4964,4966],{},[27,4965,146],{}," mislabeling produces wrong cached results",": a function that reads a table is at least ",[27,4969,1697],{},". Marking it ",[27,4972,146],{}," and using it in an index stores a value that never updates — the index returns stale data. PostgreSQL doesn't verify the label; it trusts you. This is silent data corruption.",[4805,4975,4976,4983,4984,4986],{},[18,4977,4978,4979,34,4981],{},"Functions can't ",[27,4980,33],{},[27,4982,37],{},": they run in the caller's transaction. ",[27,4985,33],{}," inside a function raises an error. Use a procedure if you need transaction control.",[4805,4988,4989,4994,4995,4997,4998,5000],{},[18,4990,4991,4993],{},[27,4992,1713],{}," is the default",": forgetting to set volatility leaves the function ",[27,4996,1713],{},", disabling caching and index use. The planner re-evaluates a ",[27,4999,1713],{}," function on every row. Always specify the volatility explicitly.",[4805,5002,5003,5006],{},[18,5004,5005],{},"PL\u002FpgSQL functions are opaque to the planner",": the planner can't see inside the function body, so it can't optimize across the function boundary (push predicates, estimate row counts). SQL functions can be inlined. For hot paths, prefer SQL functions or inline the query directly.",[4805,5008,5009,5012,5013,5015,5016,5019],{},[18,5010,5011],{},"Exception handling cost",": each entry into an ",[27,5014,4915],{}," block sets up a savepoint (subtransaction), even if no exception occurs. In a function called 1M times, that's 1M savepoints — significant overhead. Handle specific errors only, not ",[27,5017,5018],{},"WHEN OTHERS THEN"," on every function.",[4805,5021,5022,4912,5030,5033,5034,5037,5038,5040],{},[18,5023,5024,5025,5027,5028],{},"Set-returning function in ",[27,5026,159],{}," vs ",[27,5029,1640],{},[27,5031,5032],{},"SELECT generate_series(1, 5)"," emits 5 rows (lateral). ",[27,5035,5036],{},"SELECT * FROM generate_series(1, 5)"," also emits 5 rows but is the preferred form (clearer, allows aliasing). Don't mix set-returning functions in the ",[27,5039,159],{}," list with normal columns — the row count is the product (cross join semantics).",[4805,5042,5043,4912,5046,5049,5050,5052,5053,5056,5057,4849],{},[18,5044,5045],{},"Variable shadowing column names",[27,5047,5048],{},"WHERE customer_id = customer_id"," in a function with a ",[27,5051,1531],{}," parameter — the parameter shadows the column, making the predicate always true (comparing the param to itself). Qualify column references: ",[27,5054,5055],{},"WHERE orders.customer_id = customer_id"," (or use a different parameter name like ",[27,5058,5059],{},"p_customer_id",[4805,5061,5062,4912,5068,5071,5072,5075,5076,5079,5080,5083],{},[18,5063,5064,5067],{},[27,5065,5066],{},"SELECT INTO"," semantics",[27,5069,5070],{},"SELECT col INTO var FROM ..."," assigns the first matching row; if no rows match, ",[27,5073,5074],{},"var"," is NULL and ",[27,5077,5078],{},"FOUND"," is false; if multiple rows match, only the first is used (NO error). Use ",[27,5081,5082],{},"SELECT ... INTO STRICT"," to raise an error on zero or multiple rows — catches bugs where you expected exactly one row.",[4805,5085,5086,4912,5089,5091,5092,5094,5095,5097,5098,5100],{},[18,5087,5088],{},"Function search_path security",[27,5090,4944],{}," functions run as the owner. If the search_path isn't pinned, a user can create a shadowing function in ",[27,5093,4952],{}," that the DEFINER function calls with the owner's privileges. Always ",[27,5096,4948],{}," in ",[27,5099,4944],{}," functions.",[4805,5102,5103,5108,5109,5112,5113,5115],{},[18,5104,5105,5107],{},[27,5106,4944],{}," runs as the owner",": a bug in the authorization check is a privilege escalation. Validate all inputs, check authorization explicitly, and grant ",[27,5110,5111],{},"EXECUTE"," only to roles that need it. Don't make functions ",[27,5114,4944],{}," unless you need privilege elevation.",[4805,5117,5118,4912,5121,5124,5125,5128,5129,5131,5132,5134,5135,5137],{},[18,5119,5120],{},"Procedures can't be called from expressions",[27,5122,5123],{},"SELECT my_proc()"," fails — use ",[27,5126,5127],{},"CALL my_proc()",". Procedures can't return a result set to a ",[27,5130,159],{}," (use a function with ",[27,5133,2553],{}," for that). ",[27,5136,110],{}," params are the only way to return values from a procedure.",[4805,5139,5140,5143],{},[18,5141,5142],{},"Recursive functions and stack depth",": PL\u002FpgSQL functions can call themselves, but PostgreSQL doesn't guarantee tail-call optimization. Deep recursion (thousands of levels) can overflow the stack. Prefer an iterative loop for unbounded input.",[44,5145,5147],{"id":5146},"spot-the-bug","🧠 Spot the Bug",[14,5149,5150,5151,5153],{},"A developer creates a function to compute a user's full name, marks it ",[27,5152,146],{},", and uses it in an index. Weeks later, the index returns stale results after users update their names. The function:",[181,5155,5156],{"language":183},[185,5157,5159],{"className":187,"code":5158,"language":183,"meta":189,"style":189},"CREATE OR REPLACE FUNCTION full_name(user_id BIGINT) RETURNS TEXT AS $$\nBEGIN\n  RETURN (SELECT first_name || ' ' || last_name FROM users WHERE id = user_id);\n  --        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ reads a TABLE\nEND;\n$$ LANGUAGE plpgsql IMMUTABLE;                    -- ← claims \"never changes\"\n\nCREATE INDEX users_full_name_idx ON users(full_name(id));\n",[27,5160,5161,5184,5188,5219,5224,5230,5242,5246],{"__ignoreMap":189},[193,5162,5163,5165,5167,5169,5172,5174,5176,5178,5180,5182],{"class":195,"line":196},[193,5164,200],{"class":199},[193,5166,203],{"class":199},[193,5168,3885],{"class":206},[193,5170,5171],{"class":210},"(user_id ",[193,5173,288],{"class":199},[193,5175,217],{"class":210},[193,5177,220],{"class":199},[193,5179,223],{"class":199},[193,5181,226],{"class":199},[193,5183,229],{"class":210},[193,5185,5186],{"class":195,"line":232},[193,5187,490],{"class":199},[193,5189,5190,5192,5194,5196,5198,5200,5202,5204,5206,5208,5210,5212,5214,5216],{"class":195,"line":242},[193,5191,396],{"class":199},[193,5193,2361],{"class":210},[193,5195,159],{"class":199},[193,5197,3916],{"class":210},[193,5199,2716],{"class":199},[193,5201,1065],{"class":521},[193,5203,3923],{"class":199},[193,5205,3926],{"class":210},[193,5207,1640],{"class":199},[193,5209,907],{"class":210},[193,5211,85],{"class":199},[193,5213,2686],{"class":210},[193,5215,254],{"class":199},[193,5217,5218],{"class":210}," user_id);\n",[193,5220,5221],{"class":195,"line":267},[193,5222,5223],{"class":238},"  --        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ reads a TABLE\n",[193,5225,5226,5228],{"class":195,"line":282},[193,5227,405],{"class":199},[193,5229,408],{"class":210},[193,5231,5232,5234,5236,5239],{"class":195,"line":297},[193,5233,414],{"class":210},[193,5235,417],{"class":199},[193,5237,5238],{"class":210}," plpgsql IMMUTABLE;                    ",[193,5240,5241],{"class":238},"-- ← claims \"never changes\"\n",[193,5243,5244],{"class":195,"line":309},[193,5245,579],{"emptyLinePlaceholder":578},[193,5247,5248,5250,5252,5254,5256],{"class":195,"line":318},[193,5249,1821],{"class":199},[193,5251,1824],{"class":199},[193,5253,3986],{"class":206},[193,5255,1830],{"class":199},[193,5257,3991],{"class":210},[5259,5260,5261,5265,5283,5301,5308,5461],"details",{},[5262,5263,5264],"summary",{},"Answer",[14,5266,5267,5268,5270,5271,5276,5277,5279,5280,5282],{},"The function reads from the ",[27,5269,324],{}," table, so it is ",[18,5272,5273,5274],{},"not ",[27,5275,146],{}," — it's ",[27,5278,1697],{}," at best (the result changes when the table changes). ",[27,5281,146],{}," means \"same inputs always give the same output, for all time\" — a table-reading function violates this.",[14,5284,5285,5286,5288,5289,5292,5293,5296,5297,5300],{},"By marking it ",[27,5287,146],{}," and using it in an index, the developer told the planner the result never changes. The index stores ",[27,5290,5291],{},"full_name(id)"," at index time, but when a user updates ",[27,5294,5295],{},"first_name",", the index ",[18,5298,5299],{},"is not updated"," (the planner thinks the value is immutable) — the index returns stale names. Silent data corruption.",[14,5302,5303,5304,5307],{},"The fix — don't index a table-reading function. Use a ",[18,5305,5306],{},"generated column"," for indexed computed values:",[181,5309,5310],{"language":183},[185,5311,5313],{"className":187,"code":5312,"language":183,"meta":189,"style":189},"-- Option 1: mark it STABLE (honest), don't index it\nCREATE OR REPLACE FUNCTION full_name(user_id BIGINT) RETURNS TEXT AS $$\nBEGIN\n  RETURN (SELECT first_name || ' ' || last_name FROM users WHERE id = user_id);\nEND;\n$$ LANGUAGE plpgsql STABLE;    -- STABLE: reads a table, consistent within a statement\n-- Can't be indexed. Correct, but no index acceleration.\n\n-- Option 2: generated column (the right way to index a computed value)\nALTER TABLE users ADD COLUMN full_name TEXT\n  GENERATED ALWAYS AS (first_name || ' ' || last_name) STORED;\nCREATE INDEX users_full_name_idx ON users(full_name);\n-- The generated column is auto-updated on any change to first_name\u002Flast_name.\n-- The index stays in sync. No function, no volatility question, no staleness.\n",[27,5314,5315,5320,5342,5346,5376,5382,5394,5399,5403,5408,5422,5440,5452,5456],{"__ignoreMap":189},[193,5316,5317],{"class":195,"line":196},[193,5318,5319],{"class":238},"-- Option 1: mark it STABLE (honest), don't index it\n",[193,5321,5322,5324,5326,5328,5330,5332,5334,5336,5338,5340],{"class":195,"line":232},[193,5323,200],{"class":199},[193,5325,203],{"class":199},[193,5327,3885],{"class":206},[193,5329,5171],{"class":210},[193,5331,288],{"class":199},[193,5333,217],{"class":210},[193,5335,220],{"class":199},[193,5337,223],{"class":199},[193,5339,226],{"class":199},[193,5341,229],{"class":210},[193,5343,5344],{"class":195,"line":242},[193,5345,490],{"class":199},[193,5347,5348,5350,5352,5354,5356,5358,5360,5362,5364,5366,5368,5370,5372,5374],{"class":195,"line":267},[193,5349,396],{"class":199},[193,5351,2361],{"class":210},[193,5353,159],{"class":199},[193,5355,3916],{"class":210},[193,5357,2716],{"class":199},[193,5359,1065],{"class":521},[193,5361,3923],{"class":199},[193,5363,3926],{"class":210},[193,5365,1640],{"class":199},[193,5367,907],{"class":210},[193,5369,85],{"class":199},[193,5371,2686],{"class":210},[193,5373,254],{"class":199},[193,5375,5218],{"class":210},[193,5377,5378,5380],{"class":195,"line":282},[193,5379,405],{"class":199},[193,5381,408],{"class":210},[193,5383,5384,5386,5388,5391],{"class":195,"line":297},[193,5385,414],{"class":210},[193,5387,417],{"class":199},[193,5389,5390],{"class":210}," plpgsql STABLE;    ",[193,5392,5393],{"class":238},"-- STABLE: reads a table, consistent within a statement\n",[193,5395,5396],{"class":195,"line":309},[193,5397,5398],{"class":238},"-- Can't be indexed. Correct, but no index acceleration.\n",[193,5400,5401],{"class":195,"line":318},[193,5402,579],{"emptyLinePlaceholder":578},[193,5404,5405],{"class":195,"line":345},[193,5406,5407],{"class":238},"-- Option 2: generated column (the right way to index a computed value)\n",[193,5409,5410,5412,5414,5416,5418,5420],{"class":195,"line":360},[193,5411,4117],{"class":199},[193,5413,1443],{"class":199},[193,5415,907],{"class":210},[193,5417,4124],{"class":199},[193,5419,4127],{"class":210},[193,5421,4130],{"class":199},[193,5423,5424,5426,5428,5430,5432,5434,5436,5438],{"class":195,"line":369},[193,5425,4135],{"class":199},[193,5427,3251],{"class":199},[193,5429,226],{"class":199},[193,5431,4142],{"class":210},[193,5433,2716],{"class":199},[193,5435,1065],{"class":521},[193,5437,3923],{"class":199},[193,5439,4151],{"class":210},[193,5441,5442,5444,5446,5448,5450],{"class":195,"line":375},[193,5443,1821],{"class":199},[193,5445,1824],{"class":199},[193,5447,3986],{"class":206},[193,5449,1830],{"class":199},[193,5451,4164],{"class":210},[193,5453,5454],{"class":195,"line":381},[193,5455,4169],{"class":238},[193,5457,5458],{"class":195,"line":387},[193,5459,5460],{"class":238},"-- The index stays in sync. No function, no volatility question, no staleness.\n",[14,5462,5463,4912,5466,5468,5469,5471,5472,5474],{},[18,5464,5465],{},"The lesson",[27,5467,146],{}," means \"no table reads, no time dependence, no randomness.\" A function that reads a table is ",[27,5470,1697],{}," (or ",[27,5473,1713],{},"), and using it in an index produces stale results. For indexed computed values, use a generated column — the database maintains it on updates.",[5476,5477,5478],"style",{},"html pre.shiki code .svdQ7, html code.shiki .svdQ7{--shiki-default:#D73A49;--shiki-github-dark:#F97583}html pre.shiki code .sIsaT, html code.shiki .sIsaT{--shiki-default:#6F42C1;--shiki-github-dark:#B392F0}html pre.shiki code .ssxIu, html code.shiki .ssxIu{--shiki-default:#24292E;--shiki-github-dark:#E1E4E8}html pre.shiki code .sdCPZ, html code.shiki .sdCPZ{--shiki-default:#6A737D;--shiki-github-dark:#6A737D}html pre.shiki code .snvgF, html code.shiki .snvgF{--shiki-default:#005CC5;--shiki-github-dark:#79B8FF}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 .github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html.github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html pre.shiki code .sJ6F3, html code.shiki .sJ6F3{--shiki-default:#032F62;--shiki-github-dark:#9ECBFF}",{"title":189,"searchDepth":232,"depth":232,"links":5480},[5481,5482,5483,5484,5485,5486,5489,5490,5491,5492,5495,5496,5497,5498,5499,5500,5501,5502],{"id":46,"depth":232,"text":47},{"id":178,"depth":232,"text":179},{"id":423,"depth":232,"text":424},{"id":982,"depth":232,"text":983},{"id":1213,"depth":232,"text":1214},{"id":1649,"depth":232,"text":1650,"children":5487},[5488],{"id":2022,"depth":242,"text":2023},{"id":2183,"depth":232,"text":2184},{"id":2303,"depth":232,"text":2304},{"id":2565,"depth":232,"text":2566},{"id":2966,"depth":232,"text":2967,"children":5493},[5494],{"id":3127,"depth":242,"text":3128},{"id":3211,"depth":232,"text":3212},{"id":3848,"depth":232,"text":3849},{"id":4177,"depth":232,"text":4178},{"id":4359,"depth":232,"text":4360},{"id":4668,"depth":232,"text":4669},{"id":4799,"depth":232,"text":4800},{"id":4956,"depth":232,"text":4957},{"id":5146,"depth":232,"text":5147},"FUNCTION vs PROCEDURE, PL\u002FpgSQL control flow, volatility (IMMUTABLE\u002FSTABLE\u002FVOLATILE) and its optimizer implications, set-returning functions, exception handling as subtransactions, SECURITY DEFINER and search_path hijacking — code-first reference with a batch-processing function and anti-patterns for volatility mislabeling.","md",{},"\u002Fsql\u002F22-stored-procedures-and-functions",{"title":5,"description":5503},"sql\u002F22-stored-procedures-and-functions","rwCXMh6LJAz6GrSI_4C33j09YOaXxsZC5Q6Y5yye3IU",1789924654891]