| | 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 | $$; |