44Comext series IDs must be constructed exactly (pattern scans time out; string
55ranges return wrong rows — see references/data/sql-guide.md). This emits the
66correct exact-ID SQL mechanically: the cn6_total series for all-goods totals,
7- lookup-table joins for product breakdowns, and UNION ALL across any set of
8- the 27 reporter schemas. Run the output with the run_sql tool using the
7+ lookup-table joins for product breakdowns, and UNION ALL across reporter
8+ schemas. One statement may read at most 5 reporter schemas, so a request for
9+ more reporters (or 'all') prints several statements, each with the schema to
10+ run it with; run them one after another and combine the results as the
11+ header comment says. Otherwise run the output with the run_sql tool using the
912schema printed in the header comment.
1013
1114ID grammar it encodes:
3841import json
3942import re
4043import sys
44+ from collections .abc import Callable
4145
4246EU_MEMBERS = [
4347 "at" ,
9094GB_EXTRA_EU_MONTH = "2020-02-01"
9195NORTHERN_IRELAND_START = "2021-01-01"
9296
97+ # The API refuses a statement that reads data_points in more than 5 Comext
98+ # reporter schemas, so larger reporter lists are split into statements.
99+ MAX_REPORTERS_PER_STATEMENT = 5
100+ # run_sql returns at most 50 rows.
101+ MAX_ROWS = 50
102+
93103METRICS = {"value" : "eur" , "weight" : "kg" , "supplementary" : "su" }
94104METRIC_UNITS = {"value" : "EUR" , "weight" : "kg" , "supplementary" : "supplementary units" }
95105
@@ -277,17 +287,66 @@ def product_id_expr(
277287 return f"'{ prefix } ' || lower(p.product_code) || '_{ suffix } '"
278288
279289
290+ def generated_line (args : argparse .Namespace ) -> str :
291+ return (
292+ f"-- generated by comext_sql.py { args .command } : "
293+ f"{ args .flow } vs partner '{ args .partner } ', { args .start } ..{ args .end } , metric={ args .metric } "
294+ )
295+
296+
280297def header (reporters : list [str ], args : argparse .Namespace , extra : str = "" ) -> str :
281298 lines = [
282- f"-- generated by comext_sql.py { args .command } : "
283- f"{ args .flow } vs partner '{ args .partner } ', { args .start } ..{ args .end } , metric={ args .metric } " ,
299+ generated_line (args ),
284300 f'-- run with the run_sql tool, schema="eu_comext_{ reporters [0 ]} "' ,
285301 ]
286302 if extra :
287303 lines .append (f"-- { extra } " )
288304 return "\n " .join (lines )
289305
290306
307+ def reporter_groups (reporters : list [str ]) -> list [list [str ]]:
308+ """Split reporters into the fewest groups of at most 5, sizes within one."""
309+ count = - (- len (reporters ) // MAX_REPORTERS_PER_STATEMENT )
310+ size , extra = divmod (len (reporters ), count )
311+ groups , start = [], 0
312+ for index in range (count ):
313+ end = start + size + (1 if index < extra else 0 )
314+ groups .append (reporters [start :end ])
315+ start = end
316+ return groups
317+
318+
319+ def statements (
320+ reporters : list [str ],
321+ args : argparse .Namespace ,
322+ note : str ,
323+ build : Callable [[list [str ]], str ],
324+ combine : str ,
325+ ) -> str :
326+ """One statement for up to 5 reporters; otherwise one per group of at most
327+ 5, with `combine` saying how to put the results together."""
328+ if len (reporters ) <= MAX_REPORTERS_PER_STATEMENT :
329+ return header (reporters , args , note ) + "\n " + build (reporters )
330+ groups = reporter_groups (reporters )
331+ lines = [generated_line (args )]
332+ if note :
333+ lines .append (f"-- { note } " )
334+ lines .append (
335+ f"-- the API reads at most { MAX_REPORTERS_PER_STATEMENT } reporter schemas "
336+ f"in one statement, so this request is { len (groups )} statements: run them "
337+ f"one after another (not at the same time), each with the schema named "
338+ f"above it, then { combine } "
339+ )
340+ for number , group in enumerate (groups , 1 ):
341+ lines += [
342+ "" ,
343+ f"-- statement { number } of { len (groups )} : reporters { ', ' .join (group )} " ,
344+ f'-- run with the run_sql tool, schema="eu_comext_{ group [0 ]} "' ,
345+ build (group ),
346+ ]
347+ return "\n " .join (lines )
348+
349+
291350def sql_total (args : argparse .Namespace ) -> str :
292351 reporters = resolve_reporters (args .reporters )
293352 partner = resolve_partner (args .partner )
@@ -302,39 +361,46 @@ def sql_total(args: argparse.Namespace) -> str:
302361 lo , hi , coverage_note = comext_date_bounds (args .start , args .end )
303362 periods , period_note = trade_periods (partner , lo , hi )
304363
305- # Each reporter schema holds only its own series in data_points, so every
306- # reporter needs its own qualified branch — a flat IN list against one
307- # schema's table would silently drop the other 26.
308- branches = []
309- for r in reporters :
310- for period_partner , token , period_lo , period_hi in periods :
311- table = (
312- "data_points" if len (reporters ) == 1 else f"eu_comext_{ r } .data_points"
313- )
314- branches .append (
315- f" SELECT '{ r } ' AS reporter, time AS t, value AS v\n "
316- f" FROM { table } \n "
317- f" WHERE series_id = "
318- f"'{ total_series_id (flow , r , period_partner , token , suffix )} '\n "
319- f" AND time >= DATE '{ period_lo } ' AND time < DATE '{ period_hi } '"
320- )
321- union = "\n UNION ALL\n " .join (branches )
322364 if args .monthly :
323365 select , group = (
324366 f"t::date AS month, SUM(v) AS total_{ suffix } " ,
325367 "GROUP BY t\n ORDER BY t" ,
326368 )
369+ combine = f"add total_{ suffix } across the statements for each month"
327370 else :
328371 select , group = (
329372 f"reporter, SUM(v) AS total_{ suffix } " ,
330373 "GROUP BY reporter\n ORDER BY 2 DESC" ,
331374 )
332- body = f"SELECT { select } \n FROM (\n { union } \n ) u\n { group } ;"
375+ combine = "put the reporter rows of all statements together"
376+
377+ def build (group_reporters : list [str ]) -> str :
378+ # Each reporter schema holds only its own series in data_points, so
379+ # every reporter needs its own qualified branch — a flat IN list
380+ # against one schema's table would silently drop the others.
381+ branches = []
382+ for r in group_reporters :
383+ for period_partner , token , period_lo , period_hi in periods :
384+ table = (
385+ "data_points"
386+ if len (group_reporters ) == 1
387+ else f"eu_comext_{ r } .data_points"
388+ )
389+ branches .append (
390+ f" SELECT '{ r } ' AS reporter, time AS t, value AS v\n "
391+ f" FROM { table } \n "
392+ f" WHERE series_id = "
393+ f"'{ total_series_id (flow , r , period_partner , token , suffix )} '\n "
394+ f" AND time >= DATE '{ period_lo } ' AND time < DATE '{ period_hi } '"
395+ )
396+ union = "\n UNION ALL\n " .join (branches )
397+ return f"SELECT { select } \n FROM (\n { union } \n ) u\n { group } ;"
398+
333399 note = f"all-goods total via the exact cn6_total series; values in { METRIC_UNITS [args .metric ]} "
334400 note = add_note (note , domestic_note )
335401 note = add_note (note , coverage_note )
336402 note = add_note (note , period_note )
337- return header (reporters , args , note ) + " \n " + body
403+ return statements (reporters , args , note , build , combine )
338404
339405
340406def product_branches (
@@ -395,13 +461,24 @@ def sql_products(args: argparse.Namespace) -> str:
395461 "cn8" : "lower(p.product_code)" ,
396462 }[args .group_by ]
397463 inner = f"{ group_col } AS code, dp.value AS v"
398- union = product_branches (reporters , flow , suffix , periods , inner , "" )
399- body = (
400- f"SELECT code, SUM(v) AS total_{ suffix } \n "
401- f"FROM (\n { union } \n ) t\n "
402- f"GROUP BY code\n "
403- f"ORDER BY 2 DESC\n "
404- f"LIMIT { args .top } ;"
464+ # Split statements each return their top 50 so the combined ranking can
465+ # be built from them; a code missing from a statement's list is not zero.
466+ limit = args .top if len (reporters ) <= MAX_REPORTERS_PER_STATEMENT else MAX_ROWS
467+
468+ def build (group_reporters : list [str ]) -> str :
469+ union = product_branches (group_reporters , flow , suffix , periods , inner , "" )
470+ return (
471+ f"SELECT code, SUM(v) AS total_{ suffix } \n "
472+ f"FROM (\n { union } \n ) t\n "
473+ f"GROUP BY code\n "
474+ f"ORDER BY 2 DESC\n "
475+ f"LIMIT { limit } ;"
476+ )
477+
478+ combine = (
479+ f"add total_{ suffix } across the statements for each code and keep the "
480+ f"top { args .top } ; each statement lists only its top { MAX_ROWS } codes, so "
481+ f"a code missing from some statements has a total that is a lower bound"
405482 )
406483 label_hint = (
407484 "name the codes locally with the bundled hs_codes.py helper"
@@ -415,7 +492,7 @@ def sql_products(args: argparse.Namespace) -> str:
415492 note = add_note (note , domestic_note )
416493 note = add_note (note , coverage_note )
417494 note = add_note (note , period_note )
418- return header (reporters , args , note ) + " \n " + body
495+ return statements (reporters , args , note , build , combine )
419496
420497
421498def sql_trend (args : argparse .Namespace ) -> str :
@@ -452,31 +529,37 @@ def sql_trend(args: argparse.Namespace) -> str:
452529 if include_unit
453530 else "dp.time AS t, dp.value AS v"
454531 )
455- union = product_branches (
456- reporters ,
457- flow ,
458- suffix ,
459- periods ,
460- inner ,
461- where_extra ,
462- include_unit = include_unit ,
463- )
464- if include_unit :
465- body = (
466- f"SELECT unit AS measurement_units, t::date AS month, SUM(v) AS total_{ suffix } \n "
467- f"FROM (\n { union } \n ) u\n GROUP BY unit, t\n ORDER BY t, unit;"
532+
533+ def build (group_reporters : list [str ]) -> str :
534+ union = product_branches (
535+ group_reporters ,
536+ flow ,
537+ suffix ,
538+ periods ,
539+ inner ,
540+ where_extra ,
541+ include_unit = include_unit ,
468542 )
469- note = f"monthly trend for HS { code } ; units returned with each row"
470- else :
471- body = (
543+ if include_unit :
544+ return (
545+ f"SELECT unit AS measurement_units, t::date AS month, SUM(v) AS total_{ suffix } \n "
546+ f"FROM (\n { union } \n ) u\n GROUP BY unit, t\n ORDER BY t, unit;"
547+ )
548+ return (
472549 f"SELECT t::date AS month, SUM(v) AS total_{ suffix } \n "
473550 f"FROM (\n { union } \n ) u\n GROUP BY t\n ORDER BY t;"
474551 )
552+
553+ if include_unit :
554+ note = f"monthly trend for HS { code } ; units returned with each row"
555+ combine = f"add total_{ suffix } across the statements for each month and unit"
556+ else :
475557 note = f"monthly trend for HS { code } ; values in { METRIC_UNITS [args .metric ]} "
558+ combine = f"add total_{ suffix } across the statements for each month"
476559 note = add_note (note , domestic_note )
477560 note = add_note (note , coverage_note )
478561 note = add_note (note , period_note )
479- return header (reporters , args , note ) + " \n " + body
562+ return statements (reporters , args , note , build , combine )
480563
481564
482565def main () -> None :
0 commit comments