@cumulus/db 21.3.1-alpha.0 → 21.3.2-testlerna.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.js +3 -1
  3. package/dist/s3search/AsyncOperationS3Search.d.ts +20 -0
  4. package/dist/s3search/AsyncOperationS3Search.js +29 -0
  5. package/dist/s3search/CollectionS3Search.d.ts +39 -0
  6. package/dist/s3search/CollectionS3Search.js +113 -0
  7. package/dist/s3search/DuckDBSearchExecutor.d.ts +36 -0
  8. package/dist/s3search/DuckDBSearchExecutor.js +57 -0
  9. package/dist/s3search/ExecutionS3Search.d.ts +20 -0
  10. package/dist/s3search/ExecutionS3Search.js +29 -0
  11. package/dist/s3search/GranuleS3Search.d.ts +31 -0
  12. package/dist/s3search/GranuleS3Search.js +100 -0
  13. package/dist/s3search/PdrS3Search.d.ts +20 -0
  14. package/dist/s3search/PdrS3Search.js +29 -0
  15. package/dist/s3search/ProviderS3Search.d.ts +20 -0
  16. package/dist/s3search/ProviderS3Search.js +29 -0
  17. package/dist/s3search/ReconciliationReportS3Search.d.ts +20 -0
  18. package/dist/s3search/ReconciliationReportS3Search.js +29 -0
  19. package/dist/s3search/RuleS3Search.d.ts +20 -0
  20. package/dist/s3search/RuleS3Search.js +29 -0
  21. package/dist/s3search/StatsS3Search.d.ts +25 -0
  22. package/dist/s3search/StatsS3Search.js +51 -0
  23. package/dist/s3search/duckdbHelpers.d.ts +43 -0
  24. package/dist/s3search/duckdbHelpers.js +83 -0
  25. package/dist/s3search/s3TableSchemas.d.ts +11 -0
  26. package/dist/s3search/s3TableSchemas.js +272 -0
  27. package/dist/search/BaseSearch.d.ts +46 -2
  28. package/dist/search/BaseSearch.js +84 -22
  29. package/dist/search/CollectionSearch.d.ts +6 -4
  30. package/dist/search/CollectionSearch.js +2 -3
  31. package/dist/search/ExecutionSearch.d.ts +1 -1
  32. package/dist/search/ExecutionSearch.js +3 -3
  33. package/dist/search/GranuleSearch.d.ts +2 -3
  34. package/dist/search/GranuleSearch.js +3 -3
  35. package/dist/search/PdrSearch.js +1 -1
  36. package/dist/search/ReconciliationReportSearch.js +1 -1
  37. package/dist/search/RuleSearch.js +4 -4
  38. package/dist/search/StatsSearch.d.ts +15 -4
  39. package/dist/search/StatsSearch.js +12 -6
  40. package/dist/search/field-mapping.d.ts +1 -3
  41. package/dist/search/field-mapping.js +40 -19
  42. package/dist/test-duckdb-utils.d.ts +31 -0
  43. package/dist/test-duckdb-utils.js +125 -0
  44. package/dist/test-utils.js +6 -0
  45. package/dist/translate/async_operations.js +7 -3
  46. package/dist/translate/collections.js +6 -6
  47. package/dist/translate/executions.js +7 -7
  48. package/dist/translate/granules.js +16 -11
  49. package/dist/translate/pdr.js +4 -4
  50. package/dist/translate/providers.js +2 -2
  51. package/dist/translate/reconciliation_reports.js +5 -4
  52. package/dist/translate/rules.d.ts +1 -1
  53. package/dist/translate/rules.js +6 -6
  54. package/dist/types/file.d.ts +2 -0
  55. package/package.json +12 -11
@@ -22,12 +22,55 @@ exports.typeToTable = {
22
22
  reconciliationReport: tables_1.TableNames.reconciliationReports,
23
23
  };
24
24
  /**
25
- * Class to build and execute db search query
25
+ * BaseSearch
26
+ *
27
+ * Abstract base class for building and executing database search queries.
28
+ *
29
+ * Responsibilities:
30
+ * - Parse and normalize incoming query string parameters.
31
+ * - Build database queries using Knex.
32
+ * - Execute queries against PostgreSQL by default.
33
+ * - Return standardized search API response format including metadata.
34
+ *
35
+ * Default Behavior:
36
+ * - The `query()` method executes against PostgreSQL using a Knex client.
37
+ *
38
+ * DuckDB Support:
39
+ * - Subclasses that query DuckDB (e.g., *S3Search classes) must override
40
+ * the `query()` and related methods
41
+ * - DuckDB subclasses are responsible for:
42
+ * - Executing queries using a DuckDB connection.
43
+ * - Handling sequential execution (to avoid prepared statement conflicts).
44
+ * - Translating DuckDB result types (e.g., string dates/JSON) into proper API types.
45
+ *
46
+ * Design Notes:
47
+ * - Query construction logic (e.g., `buildSearch`) is shared across Postgres
48
+ * and DuckDB implementations.
49
+ * - Execution strategy is delegated to subclasses when a different database
50
+ * engine is required.
26
51
  */
27
52
  class BaseSearch {
28
53
  constructor(event, type) {
29
54
  // parsed from queryStringParameters for query build
30
55
  this.dbQueryParameters = {};
56
+ /**
57
+ * Build a JSON query expression string for nested fields.
58
+ *
59
+ *
60
+ * @param fullFieldName - Dot-separated JSON path, e.g., 'query_fields.cnm.receivedTime'
61
+ * @returns The JSON query path string
62
+ * @example
63
+ * buildJsonQueryExpression('query_fields.cnm.receivedTime')
64
+ * // returns: query_fields -> 'cnm' ->> 'receivedTime'
65
+ */
66
+ this.buildJsonQueryExpression = (fullFieldName) => {
67
+ const normalizedFieldName = fullFieldName === 'error.Error.keyword'
68
+ ? 'error.Error' : fullFieldName;
69
+ const [column, ...pathParts] = normalizedFieldName.split('.');
70
+ return `${column}${pathParts
71
+ .map((p, i) => (i === pathParts.length - 1 ? ` ->> '${p}'` : ` -> '${p}'`))
72
+ .join('')}`;
73
+ };
31
74
  this.type = type;
32
75
  this.tableName = exports.typeToTable[this.type];
33
76
  this.queryStringParameters = event?.queryStringParameters ?? {};
@@ -81,7 +124,7 @@ class BaseSearch {
81
124
  * @returns whether an estimated row count should be returned
82
125
  */
83
126
  shouldEstimateRowcount(countSql) {
84
- const isBasicQuery = (countSql === `select count(*) from "${this.tableName}"`);
127
+ const isBasicQuery = (countSql === `select count(* as count) from "${this.tableName}"`);
85
128
  return this.dbQueryParameters.estimateTableRowCount === true && isBasicQuery;
86
129
  }
87
130
  /**
@@ -127,7 +170,7 @@ class BaseSearch {
127
170
  */
128
171
  buildBasicQuery(knex) {
129
172
  const countQuery = knex(this.tableName)
130
- .count('*');
173
+ .count('* as count');
131
174
  const searchQuery = knex(this.tableName)
132
175
  .select(`${this.tableName}.*`);
133
176
  return { countQuery, searchQuery };
@@ -158,6 +201,10 @@ class BaseSearch {
158
201
  Object.entries(exists).forEach(([name, value]) => {
159
202
  const queryMethod = value ? 'whereNotNull' : 'whereNull';
160
203
  const checkNull = value ? 'not null' : 'null';
204
+ if (name.includes('.')) {
205
+ [countQuery, searchQuery].forEach((query) => query?.whereRaw(`(${this.tableName}.${this.buildJsonQueryExpression(name)}) is ${checkNull}`));
206
+ return;
207
+ }
161
208
  switch (name) {
162
209
  case 'collectionName':
163
210
  case 'collectionVersion':
@@ -175,10 +222,6 @@ class BaseSearch {
175
222
  case 'asyncOperationId':
176
223
  [countQuery, searchQuery].forEach((query) => query?.[queryMethod](`${this.tableName}.async_operation_cumulus_id`));
177
224
  break;
178
- case 'error':
179
- case 'error.Error':
180
- [countQuery, searchQuery].forEach((query) => query?.whereRaw(`${this.tableName}.error ->> 'Error' is ${checkNull}`));
181
- break;
182
225
  case 'parentArn':
183
226
  [countQuery, searchQuery].forEach((query) => query?.[queryMethod](`${this.tableName}.parent_cumulus_id`));
184
227
  break;
@@ -200,8 +243,21 @@ class BaseSearch {
200
243
  buildRangeQuery(params) {
201
244
  const { countQuery, searchQuery, dbQueryParameters } = params;
202
245
  const { range = {} } = dbQueryParameters ?? this.dbQueryParameters;
246
+ const queries = [countQuery, searchQuery];
203
247
  Object.entries(range).forEach(([name, rangeValues]) => {
204
- const { gte, lte } = rangeValues;
248
+ const { gte, lte } = rangeValues ?? {};
249
+ if (!gte && !lte)
250
+ return;
251
+ if (name.includes('.')) {
252
+ const jsonExpr = `(${this.tableName}.${this.buildJsonQueryExpression(name)})`;
253
+ if (gte) {
254
+ queries.forEach((query) => query?.whereRaw(`${jsonExpr} >= ?`, [gte]));
255
+ }
256
+ if (lte) {
257
+ queries.forEach((query) => query?.whereRaw(`${jsonExpr} <= ?`, [lte]));
258
+ }
259
+ return;
260
+ }
205
261
  if (gte) {
206
262
  [countQuery, searchQuery].forEach((query) => query?.where(`${this.tableName}.${name}`, '>=', gte));
207
263
  }
@@ -223,6 +279,10 @@ class BaseSearch {
223
279
  const { countQuery, searchQuery, dbQueryParameters } = params;
224
280
  const { term = {} } = dbQueryParameters ?? this.dbQueryParameters;
225
281
  Object.entries(term).forEach(([name, value]) => {
282
+ if (name.includes('.')) {
283
+ [countQuery, searchQuery].forEach((query) => query?.whereRaw(`(${this.tableName}.${this.buildJsonQueryExpression(name)}) = ?`, value));
284
+ return;
285
+ }
226
286
  switch (name) {
227
287
  case 'collectionName':
228
288
  [countQuery, searchQuery].forEach((query) => query?.where(`${collectionsTable}.name`, value));
@@ -239,10 +299,6 @@ class BaseSearch {
239
299
  case 'pdrName':
240
300
  [countQuery, searchQuery].forEach((query) => query?.where(`${pdrsTable}.name`, value));
241
301
  break;
242
- case 'error.Error':
243
- [countQuery, searchQuery]
244
- .forEach((query) => value && query?.whereRaw(`${this.tableName}.error->>'Error' = ?`, value));
245
- break;
246
302
  case 'asyncOperationId':
247
303
  [countQuery, searchQuery].forEach((query) => query?.where(`${asyncOperationsTable}.id`, value));
248
304
  break;
@@ -282,6 +338,10 @@ class BaseSearch {
282
338
  .forEach((query) => query?.whereIn([`${collectionsTable}.name`, `${collectionsTable}.version`], collectionPair));
283
339
  }
284
340
  Object.entries((0, omit_1.default)(terms, ['collectionName', 'collectionVersion'])).forEach(([name, value]) => {
341
+ if (name.includes('.')) {
342
+ [countQuery, searchQuery].forEach((query) => query?.whereRaw(`(${this.tableName}.${this.buildJsonQueryExpression(name)}) in (${value.map(() => '?').join(',')})`, [...value]));
343
+ return;
344
+ }
285
345
  switch (name) {
286
346
  case 'executionArn':
287
347
  [countQuery, searchQuery].forEach((query) => query?.whereIn(`${executionsTable}.arn`, value));
@@ -292,10 +352,6 @@ class BaseSearch {
292
352
  case 'pdrName':
293
353
  [countQuery, searchQuery].forEach((query) => query?.whereIn(`${pdrsTable}.name`, value));
294
354
  break;
295
- case 'error.Error':
296
- [countQuery, searchQuery]
297
- .forEach((query) => query?.whereRaw(`${this.tableName}.error->>'Error' in (${value.map(() => '?').join(',')})`, [...value]));
298
- break;
299
355
  case 'asyncOperationId':
300
356
  [countQuery, searchQuery].forEach((query) => query?.whereIn(`${asyncOperationsTable}.id`, value));
301
357
  break;
@@ -328,6 +384,10 @@ class BaseSearch {
328
384
  }));
329
385
  }
330
386
  Object.entries((0, omit_1.default)(term, ['collectionName', 'collectionVersion'])).forEach(([name, value]) => {
387
+ if (name.includes('.')) {
388
+ [countQuery, searchQuery].forEach((query) => query?.whereRaw(`(${this.tableName}.${this.buildJsonQueryExpression(name)}) != ?`, value));
389
+ return;
390
+ }
331
391
  switch (name) {
332
392
  case 'executionArn':
333
393
  [countQuery, searchQuery].forEach((query) => query?.whereNot(`${executionsTable}.arn`, value));
@@ -344,9 +404,6 @@ class BaseSearch {
344
404
  case 'parentArn':
345
405
  [countQuery, searchQuery].forEach((query) => query?.whereNot(`${executionsTable}_parent.arn`, value));
346
406
  break;
347
- case 'error.Error':
348
- [countQuery, searchQuery].forEach((query) => value && query?.whereRaw(`${this.tableName}.error->>'Error' != ?`, value));
349
- break;
350
407
  default:
351
408
  [countQuery, searchQuery].forEach((query) => query?.whereNot(`${this.tableName}.${name}`, value));
352
409
  break;
@@ -361,18 +418,23 @@ class BaseSearch {
361
418
  * @param [params.dbQueryParameters] - db query parameters
362
419
  */
363
420
  buildSortQuery(params) {
421
+ const customColumns = ['collectionName', 'collectionVersion', 'executionArn', 'providerName', 'pdrName', 'asyncOperationId', 'parentArn'];
364
422
  const { searchQuery, dbQueryParameters } = params;
365
423
  const { sort } = dbQueryParameters || this.dbQueryParameters;
366
424
  sort?.forEach((key) => {
367
- if (key.column.startsWith('error')) {
368
- searchQuery.orderByRaw(`${this.tableName}.error ->> 'Error' ${key.order}`);
425
+ const prefixedColumn = `${this.tableName}.${key.column}`;
426
+ if (key.column.includes('.')) {
427
+ searchQuery.orderByRaw(`(${this.tableName}.${this.buildJsonQueryExpression(key.column)}) ${key.order}`);
369
428
  }
370
429
  else if (dbQueryParameters?.collate) {
371
430
  searchQuery.orderByRaw(`${key} collate \"${dbQueryParameters.collate}\"`);
372
431
  }
373
- else {
432
+ else if (customColumns.includes(key.column)) {
374
433
  searchQuery.orderBy([key]);
375
434
  }
435
+ else {
436
+ searchQuery.orderBy(prefixedColumn, key.order);
437
+ }
376
438
  });
377
439
  }
378
440
  /**
@@ -3,14 +3,17 @@ import { CollectionRecord } from '@cumulus/types/api/collections';
3
3
  import { BaseSearch } from './BaseSearch';
4
4
  import { DbQueryParameters, QueryEvent } from '../types/search';
5
5
  import { PostgresCollectionRecord } from '../types/collection';
6
- declare type Statuses = {
6
+ export declare type Statuses = {
7
7
  queued: number;
8
8
  completed: number;
9
9
  failed: number;
10
10
  running: number;
11
11
  total: number;
12
12
  };
13
- interface CollectionRecordApi extends CollectionRecord {
13
+ export declare type StatsRecords = {
14
+ [key: number]: Statuses;
15
+ };
16
+ export interface CollectionRecordApi extends CollectionRecord {
14
17
  stats?: Statuses;
15
18
  }
16
19
  /**
@@ -58,7 +61,7 @@ export declare class CollectionSearch extends BaseSearch {
58
61
  * @param knex - knex for the stats query
59
62
  * @returns the collection's granules status' aggregation
60
63
  */
61
- private retrieveGranuleStats;
64
+ protected retrieveGranuleStats(collectionCumulusIds: number[], knex: Knex): Promise<StatsRecords>;
62
65
  /**
63
66
  * Translate postgres records to api records
64
67
  *
@@ -68,5 +71,4 @@ export declare class CollectionSearch extends BaseSearch {
68
71
  */
69
72
  protected translatePostgresRecordsToApiRecords(pgRecords: PostgresCollectionRecord[], knex: Knex): Promise<Partial<CollectionRecordApi>[]>;
70
73
  }
71
- export {};
72
74
  //# sourceMappingURL=CollectionSearch.d.ts.map
@@ -61,8 +61,7 @@ class CollectionSearch extends BaseSearch_1.BaseSearch {
61
61
  subQuery
62
62
  .clear('select')
63
63
  .select(1)
64
- .where(`${granulesTable}.collection_cumulus_id`, knex.raw(`${this.tableName}.cumulus_id`))
65
- .limit(1);
64
+ .where(`${granulesTable}.collection_cumulus_id`, knex.raw(`${this.tableName}.cumulus_id`));
66
65
  return subQuery;
67
66
  }
68
67
  /**
@@ -101,7 +100,7 @@ class CollectionSearch extends BaseSearch_1.BaseSearch {
101
100
  }
102
101
  statsQuery
103
102
  .select(`${granulesTable}.collection_cumulus_id`, `${granulesTable}.status`)
104
- .count('*')
103
+ .count('* as count')
105
104
  .groupBy(`${granulesTable}.collection_cumulus_id`, `${granulesTable}.status`)
106
105
  .whereIn(`${granulesTable}.collection_cumulus_id`, collectionCumulusIds);
107
106
  log.debug(`retrieveGranuleStats statsQuery: ${statsQuery?.toSQL().sql}`);
@@ -14,7 +14,7 @@ interface ExecutionRecord extends BaseRecord, PostgresExecutionRecord {
14
14
  * Class to build and execute db search query for executions
15
15
  */
16
16
  export declare class ExecutionSearch extends BaseSearch {
17
- constructor(event: QueryEvent);
17
+ constructor(event: QueryEvent, enableEstimate?: boolean);
18
18
  /**
19
19
  * check if joined async_operations table search is needed
20
20
  *
@@ -16,9 +16,9 @@ const log = new logger_1.default({ sender: '@cumulus/db/ExecutionSearch' });
16
16
  * Class to build and execute db search query for executions
17
17
  */
18
18
  class ExecutionSearch extends BaseSearch_1.BaseSearch {
19
- constructor(event) {
19
+ constructor(event, enableEstimate = true) {
20
20
  // estimate the table rowcount by default
21
- if (event?.queryStringParameters?.estimateTableRowCount !== 'false') {
21
+ if (enableEstimate && event?.queryStringParameters?.estimateTableRowCount !== 'false') {
22
22
  (0, set_1.default)(event, 'queryStringParameters.estimateTableRowCount', 'true');
23
23
  }
24
24
  super(event, 'execution');
@@ -62,7 +62,7 @@ class ExecutionSearch extends BaseSearch_1.BaseSearch {
62
62
  searchQuery.select({ parentArn: `${executionsTable}_parent.arn` });
63
63
  }
64
64
  const countQuery = knex(this.tableName)
65
- .count('*');
65
+ .count('* as count');
66
66
  if (this.searchCollection()) {
67
67
  countQuery.innerJoin(collectionsTable, `${this.tableName}.collection_cumulus_id`, `${collectionsTable}.cumulus_id`);
68
68
  searchQuery.innerJoin(collectionsTable, `${this.tableName}.collection_cumulus_id`, `${collectionsTable}.cumulus_id`);
@@ -4,7 +4,7 @@ import { BaseRecord } from '../types/base';
4
4
  import { BaseSearch } from './BaseSearch';
5
5
  import { DbQueryParameters, QueryEvent } from '../types/search';
6
6
  import { PostgresGranuleRecord } from '../types/granule';
7
- interface GranuleRecord extends BaseRecord, PostgresGranuleRecord {
7
+ export interface GranuleRecord extends BaseRecord, PostgresGranuleRecord {
8
8
  collectionName: string;
9
9
  collectionVersion: string;
10
10
  pdrName?: string;
@@ -14,7 +14,7 @@ interface GranuleRecord extends BaseRecord, PostgresGranuleRecord {
14
14
  * Class to build and execute db search query for granules
15
15
  */
16
16
  export declare class GranuleSearch extends BaseSearch {
17
- constructor(event: QueryEvent);
17
+ constructor(event: QueryEvent, enableEstimate?: boolean);
18
18
  /**
19
19
  * Build basic query
20
20
  *
@@ -59,5 +59,4 @@ export declare class GranuleSearch extends BaseSearch {
59
59
  */
60
60
  protected translatePostgresRecordsToApiRecords(pgRecords: GranuleRecord[], knex: Knex): Promise<Partial<ApiGranuleRecord>[]>;
61
61
  }
62
- export {};
63
62
  //# sourceMappingURL=GranuleSearch.d.ts.map
@@ -17,9 +17,9 @@ const log = new logger_1.default({ sender: '@cumulus/db/GranuleSearch' });
17
17
  * Class to build and execute db search query for granules
18
18
  */
19
19
  class GranuleSearch extends BaseSearch_1.BaseSearch {
20
- constructor(event) {
20
+ constructor(event, enableEstimate = true) {
21
21
  // estimate the table rowcount by default
22
- if (event?.queryStringParameters?.estimateTableRowCount !== 'false') {
22
+ if (enableEstimate && event?.queryStringParameters?.estimateTableRowCount !== 'false') {
23
23
  (0, set_1.default)(event, 'queryStringParameters.estimateTableRowCount', 'true');
24
24
  }
25
25
  super(event, 'granule');
@@ -33,7 +33,7 @@ class GranuleSearch extends BaseSearch_1.BaseSearch {
33
33
  buildBasicQuery(knex) {
34
34
  const { collections: collectionsTable, providers: providersTable, pdrs: pdrsTable, } = tables_1.TableNames;
35
35
  const countQuery = knex(this.tableName)
36
- .count('*');
36
+ .count('* as count');
37
37
  const searchQuery = knex(this.tableName)
38
38
  .select(`${this.tableName}.*`)
39
39
  .select({
@@ -26,7 +26,7 @@ class PdrSearch extends BaseSearch_1.BaseSearch {
26
26
  buildBasicQuery(knex) {
27
27
  const { collections: collectionsTable, providers: providersTable, executions: executionsTable, } = tables_1.TableNames;
28
28
  const countQuery = knex(this.tableName)
29
- .count('*');
29
+ .count('* as count');
30
30
  const searchQuery = knex(this.tableName)
31
31
  .select(`${this.tableName}.*`)
32
32
  .select({
@@ -26,7 +26,7 @@ class ReconciliationReportSearch extends BaseSearch_1.BaseSearch {
26
26
  buildBasicQuery(knex) {
27
27
  const { reconciliationReports: reconciliationReportsTable, } = tables_1.TableNames;
28
28
  const countQuery = knex(this.tableName)
29
- .count('*');
29
+ .count('* as count');
30
30
  const searchQuery = knex(this.tableName)
31
31
  .select(`${this.tableName}.*`)
32
32
  .select({
@@ -26,7 +26,7 @@ class RuleSearch extends BaseSearch_1.BaseSearch {
26
26
  buildBasicQuery(knex) {
27
27
  const { collections: collectionsTable, providers: providersTable, } = tables_1.TableNames;
28
28
  const countQuery = knex(this.tableName)
29
- .count(`${this.tableName}.cumulus_id`);
29
+ .count('* as count');
30
30
  const searchQuery = knex(this.tableName)
31
31
  .select(`${this.tableName}.*`)
32
32
  .select({
@@ -77,18 +77,18 @@ class RuleSearch extends BaseSearch_1.BaseSearch {
77
77
  */
78
78
  async translatePostgresRecordsToApiRecords(pgRecords) {
79
79
  log.debug(`translatePostgresRecordsToApiRecords number of records ${pgRecords.length} `);
80
- const apiRecords = pgRecords.map(async (record) => {
80
+ const apiRecords = pgRecords.map((record) => {
81
81
  const providerPgRecord = record.providerName ? { name: record.providerName } : undefined;
82
82
  const collectionPgRecord = record.collectionName ? {
83
83
  name: record.collectionName,
84
84
  version: record.collectionVersion,
85
85
  } : undefined;
86
- const apiRecord = await (0, rules_1.translatePostgresRuleToApiRuleWithoutDbQuery)(record, collectionPgRecord, providerPgRecord);
86
+ const apiRecord = (0, rules_1.translatePostgresRuleToApiRuleWithoutDbQuery)(record, collectionPgRecord, providerPgRecord);
87
87
  return this.dbQueryParameters.fields
88
88
  ? (0, pick_1.default)(apiRecord, this.dbQueryParameters.fields)
89
89
  : apiRecord;
90
90
  });
91
- return await Promise.all(apiRecords);
91
+ return apiRecords;
92
92
  }
93
93
  }
94
94
  exports.RuleSearch = RuleSearch;
@@ -1,6 +1,16 @@
1
1
  import { Knex } from 'knex';
2
2
  import { DbQueryParameters, QueryEvent } from '../types/search';
3
3
  import { BaseSearch } from './BaseSearch';
4
+ export declare type TotalSummary = {
5
+ count_errors: number;
6
+ count_collections: number;
7
+ count_granules: number;
8
+ avg_processing_time: number;
9
+ };
10
+ declare type Aggregate = {
11
+ count: string;
12
+ aggregatedfield: string;
13
+ };
4
14
  declare type Summary = {
5
15
  dateFrom: string;
6
16
  dateTo: string;
@@ -8,7 +18,7 @@ declare type Summary = {
8
18
  aggregation: string;
9
19
  unit: string;
10
20
  };
11
- declare type SummaryResult = {
21
+ export declare type SummaryResult = {
12
22
  errors: Summary;
13
23
  granules: Summary;
14
24
  collections: Summary;
@@ -23,7 +33,7 @@ declare type AggregateRes = {
23
33
  key: string;
24
34
  count: number;
25
35
  };
26
- declare type ApiAggregateResult = {
36
+ export declare type ApiAggregateResult = {
27
37
  meta: Meta;
28
38
  count: AggregateRes[];
29
39
  };
@@ -39,14 +49,15 @@ declare class StatsSearch extends BaseSearch {
39
49
  * @param result - the postgres query results
40
50
  * @returns the api object with the aggregate statistics
41
51
  */
42
- private formatAggregateResult;
52
+ protected formatAggregateResult(result: Record<string, Aggregate>): ApiAggregateResult;
43
53
  /**
44
54
  * Formats the postgres results into an API stats/summary response
45
55
  *
46
56
  * @param result - the knex summary query results
47
57
  * @returns the api object with the summary statistics
48
58
  */
49
- private formatSummaryResult;
59
+ protected formatSummaryResult(result: TotalSummary): SummaryResult;
60
+ protected buildSummaryQuery(knex: Knex): Knex.QueryBuilder;
50
61
  /**
51
62
  * Queries postgres for a summary of statistics around the granules in the system
52
63
  *
@@ -94,6 +94,12 @@ class StatsSearch extends BaseSearch_1.BaseSearch {
94
94
  },
95
95
  };
96
96
  }
97
+ buildSummaryQuery(knex) {
98
+ const aggregateQuery = knex(this.tableName);
99
+ this.buildRangeQuery({ searchQuery: aggregateQuery });
100
+ aggregateQuery.select(knex.raw(`COUNT(CASE WHEN ${this.tableName}.error ->> 'Error' is not null THEN 1 END) AS count_errors`), knex.raw('COUNT(*) AS count_granules'), knex.raw(`AVG(${this.tableName}.duration) AS avg_processing_time`), knex.raw(`COUNT(DISTINCT ${this.tableName}.collection_cumulus_id) AS count_collections`));
101
+ return aggregateQuery;
102
+ }
97
103
  /**
98
104
  * Queries postgres for a summary of statistics around the granules in the system
99
105
  *
@@ -102,9 +108,7 @@ class StatsSearch extends BaseSearch_1.BaseSearch {
102
108
  */
103
109
  async summary(testKnex) {
104
110
  const knex = testKnex ?? await (0, connection_1.getKnexClient)();
105
- const aggregateQuery = knex(this.tableName);
106
- this.buildRangeQuery({ searchQuery: aggregateQuery });
107
- aggregateQuery.select(knex.raw(`COUNT(CASE WHEN ${this.tableName}.error ->> 'Error' is not null THEN 1 END) AS count_errors`), knex.raw('COUNT(*) AS count_granules'), knex.raw(`AVG(${this.tableName}.duration) AS avg_processing_time`), knex.raw(`COUNT(DISTINCT ${this.tableName}.collection_cumulus_id) AS count_collections`));
111
+ const aggregateQuery = this.buildSummaryQuery(knex);
108
112
  log.debug(`summary about to execute query: ${aggregateQuery?.toSQL().sql}`);
109
113
  const aggregateQueryRes = await aggregateQuery;
110
114
  return this.formatSummaryResult(aggregateQueryRes[0]);
@@ -133,11 +137,13 @@ class StatsSearch extends BaseSearch_1.BaseSearch {
133
137
  * @param knex - the knex client to be used
134
138
  */
135
139
  aggregateQueryField(query, knex) {
136
- if (this.field?.includes('error.Error')) {
137
- query.select(knex.raw("error ->> 'Error' as aggregatedfield"));
140
+ const normalizedKey = this.field === 'error.Error.keyword' ? 'error.Error' : this.field;
141
+ if (normalizedKey?.includes('.')) {
142
+ const [root, ...nested] = normalizedKey.split('.');
143
+ query.select(knex.raw(`${root} ->> '${nested.join('.')}' as aggregatedfield`));
138
144
  }
139
145
  else {
140
- query.select(`${this.tableName}.${this.field} as aggregatedfield`);
146
+ query.select(`${this.tableName}.${normalizedKey} as aggregatedfield`);
141
147
  }
142
148
  query.modify((queryBuilder) => this.joinTables(queryBuilder))
143
149
  .count('* as count')
@@ -10,7 +10,5 @@
10
10
  export declare const mapQueryStringFieldToDbField: (type: string, queryField: {
11
11
  name: string;
12
12
  value?: string;
13
- }) => {
14
- [key: string]: any;
15
- } | undefined;
13
+ }) => Record<string, any> | undefined;
16
14
  //# sourceMappingURL=field-mapping.d.ts.map
@@ -10,7 +10,7 @@ const log = new logger_1.default({ sender: '@cumulus/db/field-mapping' });
10
10
  // functions to map the api search string field name and value to postgres db field
11
11
  const granuleMapping = {
12
12
  archived: (value) => ({
13
- archived: value,
13
+ archived: value === 'true',
14
14
  }),
15
15
  beginningDateTime: (value) => ({
16
16
  beginning_date_time: value,
@@ -72,13 +72,6 @@ const granuleMapping = {
72
72
  error: (value) => ({
73
73
  error: value,
74
74
  }),
75
- // nested error field
76
- 'error.Error': (value) => ({
77
- 'error.Error': value,
78
- }),
79
- 'error.Error.keyword': (value) => ({
80
- 'error.Error': value,
81
- }),
82
75
  // The following fields require querying other tables
83
76
  collectionId: (value) => {
84
77
  const { name, version } = (value && (0, Collections_1.deconstructCollectionId)(value)) || {};
@@ -175,13 +168,6 @@ const executionMapping = {
175
168
  duration: (value) => ({
176
169
  duration: value && Number(value),
177
170
  }),
178
- // nested error field
179
- 'error.Error': (value) => ({
180
- 'error.Error': value,
181
- }),
182
- 'error.Error.keyword': (value) => ({
183
- 'error.Error': value,
184
- }),
185
171
  execution: (value) => ({
186
172
  url: value,
187
173
  }),
@@ -212,7 +198,7 @@ const executionMapping = {
212
198
  };
213
199
  },
214
200
  archived: (value) => ({
215
- archived: value,
201
+ archived: value === 'true',
216
202
  }),
217
203
  };
218
204
  const pdrMapping = {
@@ -386,6 +372,12 @@ const reconciliationReportMapping = {
386
372
  updated_at: value && new Date(Number(value)),
387
373
  }),
388
374
  };
375
+ const nestedRootsByType = {
376
+ execution: new Set(['error']),
377
+ granule: new Set(['error', 'queryFields']),
378
+ pdr: new Set(['stats']),
379
+ reconciliationReport: new Set(['error']),
380
+ };
389
381
  // type and its mapping
390
382
  const supportedMappings = {
391
383
  granule: granuleMapping,
@@ -397,6 +389,19 @@ const supportedMappings = {
397
389
  rule: ruleMapping,
398
390
  reconciliationReport: reconciliationReportMapping,
399
391
  };
392
+ const toSnakeCase = (str) => str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
393
+ const mapNestedKey = (type, key) => {
394
+ const normalizedKey = key === 'error.Error.keyword' ? 'error.Error' : key;
395
+ const [root, ...nested] = normalizedKey.split('.');
396
+ const allowedRoots = nestedRootsByType[type];
397
+ if (!allowedRoots || !allowedRoots.has(root)) {
398
+ return undefined;
399
+ }
400
+ const mappedRoot = toSnakeCase(root);
401
+ if (nested.length === 0)
402
+ return mappedRoot;
403
+ return [mappedRoot, ...nested].join('.');
404
+ };
400
405
  /**
401
406
  * Map query string field to db field
402
407
  *
@@ -407,11 +412,27 @@ const supportedMappings = {
407
412
  * @returns db field
408
413
  */
409
414
  const mapQueryStringFieldToDbField = (type, queryField) => {
410
- if (!(supportedMappings[type] && supportedMappings[type][queryField.name])) {
411
- log.warn(`No db mapping field found for type: ${type}, field ${JSON.stringify(queryField)}`);
415
+ const typeMapping = supportedMappings[type];
416
+ if (!typeMapping) {
417
+ log.warn(`No mapping found for type: ${type}`);
412
418
  return undefined;
413
419
  }
414
- return supportedMappings[type] && supportedMappings[type][queryField.name](queryField.value);
420
+ // Exact match (typed + custom logic)
421
+ const exactMapper = typeMapping[queryField.name];
422
+ if (exactMapper) {
423
+ return exactMapper(queryField.value);
424
+ }
425
+ // Nested fallback with type inference
426
+ if (queryField.name.includes('.')) {
427
+ const mappedKey = mapNestedKey(type, queryField.name);
428
+ if (mappedKey) {
429
+ return {
430
+ [mappedKey]: queryField.value,
431
+ };
432
+ }
433
+ }
434
+ log.warn(`No db mapping field found for type: ${type}, field ${JSON.stringify(queryField)}`);
435
+ return undefined;
415
436
  };
416
437
  exports.mapQueryStringFieldToDbField = mapQueryStringFieldToDbField;
417
438
  //# sourceMappingURL=field-mapping.js.map
@@ -0,0 +1,31 @@
1
+ import type { Knex } from 'knex';
2
+ import { DuckDBInstance, DuckDBConnection } from '@duckdb/node-api';
3
+ /**
4
+ * Creates a DuckDB in-memory instance and sets up S3/httpfs for testing.
5
+ * Configures S3-related settings on the DuckDB instance.
6
+ *
7
+ * @param {string} dbFilePath - The path to the DuckDB database file. Defaults to in-memory
8
+ * @returns {Promise<{ instance: DuckDBInstance, connection: DuckDBConnection }>}
9
+ * - The created DuckDB instance and the connection object for interacting with the database.
10
+ * - The connection is configured with HTTPFS for S3.
11
+ */
12
+ export declare function setupDuckDBWithS3ForTesting(dbFilePath?: string): Promise<{
13
+ instance: DuckDBInstance;
14
+ connection: DuckDBConnection;
15
+ }>;
16
+ /**
17
+ * Stages data into a temporary DuckDB table, exports it to Parquet (S3),
18
+ * and then loads it into the target table.
19
+ *
20
+ * @template T - Shape of the row object being inserted.
21
+ * @param connection - Active DuckDB connection.
22
+ * @param knexBuilder - Knex instance used to generate SQL insert statements.
23
+ * @param tableName - Name of the destination table.
24
+ * @param tableSql - Function that returns the CREATE TABLE SQL for a given table name.
25
+ * @param data - A single row or array of rows to insert.
26
+ * @param s3Path - Destination S3 path where the staged data will be exported as Parquet.
27
+ * @returns Promise that resolves when the staging, export, and load process completes.
28
+ */
29
+ export declare function stageAndLoadDuckDBTableFromData<T extends Record<string, any>>(connection: DuckDBConnection, knexBuilder: Knex, tableName: string, tableSql: (tableName: string) => string, data: T | T[], s3Path: string): Promise<void>;
30
+ export declare function createDuckDBTables(connection: DuckDBConnection): Promise<void>;
31
+ //# sourceMappingURL=test-duckdb-utils.d.ts.map