| 125 | }}} |
| 126 | |
| 127 | === p_event_create_bulk === |
| 128 | {{{ |
| 129 | CREATE OR REPLACE PROCEDURE p_event_bulk_create( |
| 130 | IN _items jsonb, |
| 131 | INOUT cur refcursor |
| 132 | ) |
| 133 | LANGUAGE plpgsql |
| 134 | AS $$ |
| 135 | BEGIN |
| 136 | -- Validate top-level shape |
| 137 | IF jsonb_typeof(_items) <> 'array' THEN |
| 138 | RAISE EXCEPTION 'payload must be a JSON array'; |
| 139 | END IF; |
| 140 | |
| 141 | OPEN cur FOR |
| 142 | WITH ins AS ( |
| 143 | INSERT INTO events ( |
| 144 | sql_date, is_root_event, event_code, goldstein_scale, |
| 145 | num_mentions, num_sources, num_articles, avg_tone, |
| 146 | month_year, year, fraction_date |
| 147 | ) |
| 148 | SELECT |
| 149 | (x->>'sql_date')::date, |
| 150 | COALESCE((x->>'is_root_event')::boolean, TRUE), |
| 151 | (x->>'event_code')::text, |
| 152 | NULLIF(x->>'goldstein_scale','')::numeric, |
| 153 | NULLIF(x->>'num_mentions','')::int, |
| 154 | NULLIF(x->>'num_sources','')::int, |
| 155 | NULLIF(x->>'num_articles','')::int, |
| 156 | NULLIF(x->>'avg_tone','')::numeric, |
| 157 | to_char((x->>'sql_date')::date, 'MMYYYY'), |
| 158 | EXTRACT(YEAR FROM (x->>'sql_date')::date)::int, |
| 159 | EXTRACT(DOY FROM (x->>'sql_date')::date) / 365.0 |
| 160 | FROM jsonb_array_elements(_items) AS x |
| 161 | RETURNING global_event_id, sql_date, event_code |
| 162 | ) |
| 163 | SELECT jsonb_build_object( |
| 164 | 'ok', true, |
| 165 | 'inserted_count', (SELECT count(*) FROM ins), |
| 166 | 'inserted_ids', (SELECT jsonb_agg(global_event_id) FROM ins) |
| 167 | ) AS j; |
| 168 | |
| 169 | EXCEPTION |
| 170 | WHEN OTHERS THEN |
| 171 | -- Any error will abort the outer transaction unless handled |
| 172 | RAISE; |
| 173 | END; |
| 174 | $$; |
| 175 | }}} |
| 176 | |
| 177 | === p_event_create_update_delete_bulk === |
| 178 | {{{ |
| 179 | |
| 180 | -- Multiple operations in one go: INSERT + UPDATE + DELETE |
| 181 | -- Payload shape: |
| 182 | -- { |
| 183 | -- "insert": [ |
| 184 | -- { |
| 185 | -- "sql_date": "2025-09-01", |
| 186 | -- "is_root_event": true, |
| 187 | -- "event_code": "E123", |
| 188 | -- "goldstein_scale": 1.5, |
| 189 | -- "num_mentions": 5, |
| 190 | -- "num_sources": 2, |
| 191 | -- "num_articles": 3, |
| 192 | -- "avg_tone": 0.75 |
| 193 | -- }, ... |
| 194 | -- ], |
| 195 | -- "update": [ |
| 196 | -- { |
| 197 | -- "global_event_id": 42, |
| 198 | -- "sql_date": "2025-09-02", |
| 199 | -- "num_mentions": 7, |
| 200 | -- "avg_tone": 0.1 |
| 201 | -- }, ... |
| 202 | -- ], |
| 203 | -- "delete": [ 10, 11, 12 ] |
| 204 | -- } |
| 205 | |
| 206 | CREATE OR REPLACE PROCEDURE p_event_bulk_apply( |
| 207 | IN _payload jsonb, |
| 208 | INOUT cur refcursor DEFAULT NULL |
| 209 | ) |
| 210 | LANGUAGE plpgsql |
| 211 | AS $$ |
| 212 | DECLARE |
| 213 | v_has_insert boolean := (_payload ? 'insert'); |
| 214 | v_has_update boolean := (_payload ? 'update'); |
| 215 | v_has_delete boolean := (_payload ? 'delete'); |
| 216 | BEGIN |
| 217 | -- Basic validation |
| 218 | IF _payload IS NULL OR jsonb_typeof(_payload) <> 'object' THEN |
| 219 | RAISE EXCEPTION 'payload must be a JSON object with optional keys: insert, update, delete'; |
| 220 | END IF; |
| 221 | IF v_has_insert AND jsonb_typeof(_payload->'insert') <> 'array' THEN |
| 222 | RAISE EXCEPTION '"insert" must be an array'; |
| 223 | END IF; |
| 224 | IF v_has_update AND jsonb_typeof(_payload->'update') <> 'array' THEN |
| 225 | RAISE EXCEPTION '"update" must be an array'; |
| 226 | END IF; |
| 227 | IF v_has_delete AND jsonb_typeof(_payload->'delete') <> 'array' THEN |
| 228 | RAISE EXCEPTION '"delete" must be an array of IDs'; |
| 229 | END IF; |
| 230 | |
| 231 | -- Perform all operations in a single statement block using CTEs |
| 232 | IF cur IS NULL THEN cur := 'cur_event_bulk_apply'; END IF; |
| 233 | OPEN cur FOR |
| 234 | WITH |
| 235 | -- 1) INSERT many (optional) |
| 236 | ins AS ( |
| 237 | SELECT * FROM ( |
| 238 | INSERT INTO events ( |
| 239 | sql_date, is_root_event, event_code, goldstein_scale, |
| 240 | num_mentions, num_sources, num_articles, avg_tone, |
| 241 | month_year, year, fraction_date |
| 242 | ) |
| 243 | SELECT |
| 244 | (x->>'sql_date')::date, |
| 245 | COALESCE((x->>'is_root_event')::boolean, TRUE), |
| 246 | UPPER((x->>'event_code')::text), |
| 247 | NULLIF(x->>'goldstein_scale','')::numeric, |
| 248 | NULLIF(x->>'num_mentions','')::int, |
| 249 | NULLIF(x->>'num_sources','')::int, |
| 250 | NULLIF(x->>'num_articles','')::int, |
| 251 | NULLIF(x->>'avg_tone','')::numeric, |
| 252 | TO_CHAR((x->>'sql_date')::date, 'MMYYYY'), |
| 253 | EXTRACT(YEAR FROM (x->>'sql_date')::date)::int, |
| 254 | EXTRACT(DOY FROM (x->>'sql_date')::date)::numeric / 365.0 |
| 255 | FROM jsonb_array_elements(COALESCE(_payload->'insert', '[]'::jsonb)) AS x |
| 256 | RETURNING global_event_id |
| 257 | ) t |
| 258 | ), |
| 259 | upd_src AS ( |
| 260 | SELECT * |
| 261 | FROM jsonb_to_recordset(COALESCE(_payload->'update','[]'::jsonb)) AS r( |
| 262 | global_event_id int, |
| 263 | sql_date date, |
| 264 | is_root_event boolean, |
| 265 | event_code text, |
| 266 | goldstein_scale numeric(5,2), |
| 267 | num_mentions int, |
| 268 | num_sources int, |
| 269 | num_articles int, |
| 270 | avg_tone numeric(5,2) |
| 271 | ) |
| 272 | ), |
| 273 | upd AS ( |
| 274 | UPDATE events e |
| 275 | SET |
| 276 | sql_date = COALESCE(u.sql_date, e.sql_date), |
| 277 | is_root_event = COALESCE(u.is_root_event, e.is_root_event), |
| 278 | event_code = COALESCE(UPPER(u.event_code), e.event_code), |
| 279 | goldstein_scale= COALESCE(u.goldstein_scale, e.goldstein_scale), |
| 280 | num_mentions = COALESCE(u.num_mentions, e.num_mentions), |
| 281 | num_sources = COALESCE(u.num_sources, e.num_sources), |
| 282 | num_articles = COALESCE(u.num_articles, e.num_articles), |
| 283 | avg_tone = COALESCE(u.avg_tone, e.avg_tone), |
| 284 | month_year = CASE WHEN u.sql_date IS NULL THEN e.month_year ELSE TO_CHAR(u.sql_date,'MMYYYY') END, |
| 285 | year = CASE WHEN u.sql_date IS NULL THEN e.year ELSE EXTRACT(YEAR FROM u.sql_date)::int END, |
| 286 | fraction_date = CASE WHEN u.sql_date IS NULL THEN e.fraction_date ELSE (EXTRACT(DOY FROM u.sql_date)::numeric / 365.0) END |
| 287 | FROM upd_src u |
| 288 | WHERE e.global_event_id = u.global_event_id |
| 289 | RETURNING e.global_event_id |
| 290 | ), |
| 291 | -- 3) DELETE many (optional) |
| 292 | del_ids AS ( |
| 293 | SELECT (x)::int AS id |
| 294 | FROM jsonb_array_elements_text(COALESCE(_payload->'delete','[]'::jsonb)) AS x |
| 295 | ), |
| 296 | del AS ( |
| 297 | DELETE FROM events e |
| 298 | USING del_ids d |
| 299 | WHERE e.global_event_id = d.id |
| 300 | RETURNING e.global_event_id |
| 301 | ) |
| 302 | SELECT jsonb_build_object( |
| 303 | 'ok', true, |
| 304 | 'inserted_count', (SELECT COUNT(*) FROM ins), |
| 305 | 'inserted_ids', COALESCE((SELECT jsonb_agg(global_event_id) FROM ins), '[]'::jsonb), |
| 306 | 'updated_count', (SELECT COUNT(*) FROM upd), |
| 307 | 'updated_ids', COALESCE((SELECT jsonb_agg(global_event_id) FROM upd), '[]'::jsonb), |
| 308 | 'deleted_count', (SELECT COUNT(*) FROM del), |
| 309 | 'deleted_ids', COALESCE((SELECT jsonb_agg(global_event_id) FROM del), '[]'::jsonb) |
| 310 | ) AS j; |
| 311 | |
| 312 | |
| 313 | EXCEPTION |
| 314 | WHEN OTHERS THEN |
| 315 | RAISE; |
| 316 | END; |
| 317 | $$; |