bulltrackers-module 1.0.715 → 1.0.716

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.
@@ -555,15 +555,15 @@ async function writeToBigQuery(result, name, dateContext, category, logger, isAl
555
555
 
556
556
  const datasetId = process.env.BIGQUERY_DATASET_ID || 'bulltrackers_data';
557
557
 
558
- // Use streaming inserts for alert computations (immediate, time-sensitive)
559
- // Use load jobs for non-alert computations (batched, free)
560
- const { insertRows: insertRowsLoadJob, insertRowsStreaming } = require('../../core/utils/bigquery_utils');
558
+ // Use MERGE operation to overwrite existing results (by date + computation_name + category)
559
+ // This ensures re-running a computation overwrites the old result
560
+ // Key fields: date, computation_name, category (ignoring created_at)
561
+ const { insertRowsWithMerge } = require('../../core/utils/bigquery_utils');
562
+ const keyFields = ['date', 'computation_name', 'category'];
561
563
 
562
- if (isAlertComputation) {
563
- await insertRowsStreaming(datasetId, 'computation_results', [row], logger);
564
- } else {
565
- await insertRowsLoadJob(datasetId, 'computation_results', [row], logger);
566
- }
564
+ // For alert computations, we still want to use MERGE but it will use load jobs (free)
565
+ // This ensures overwrites work correctly for both alert and non-alert computations
566
+ await insertRowsWithMerge(datasetId, 'computation_results', [row], keyFields, logger);
567
567
 
568
568
  } catch (error) {
569
569
  // Log but don't throw - BigQuery write failure shouldn't break Firestore writes
@@ -663,8 +663,29 @@ function calculateFirestoreBytes(value) {
663
663
  }
664
664
 
665
665
  function calculateExpirationDate(dateStr, ttlDays) {
666
+ // Validate inputs
667
+ if (!dateStr || typeof dateStr !== 'string') {
668
+ return null; // Invalid date string
669
+ }
670
+
671
+ if (ttlDays === undefined || ttlDays === null || isNaN(Number(ttlDays))) {
672
+ return null; // Invalid TTL days
673
+ }
674
+
666
675
  const base = new Date(dateStr);
667
- base.setDate(base.getDate() + ttlDays);
676
+
677
+ // Check if date is valid (invalid dates have NaN getTime())
678
+ if (isNaN(base.getTime())) {
679
+ return null; // Invalid date
680
+ }
681
+
682
+ base.setDate(base.getDate() + Number(ttlDays));
683
+
684
+ // Double-check the result is still valid
685
+ if (isNaN(base.getTime())) {
686
+ return null; // Resulting date is invalid
687
+ }
688
+
668
689
  return base;
669
690
  }
670
691
 
@@ -196,27 +196,17 @@ async function insertRowsWithMerge(datasetId, tableId, rows, keyFields, logger =
196
196
  logger.log('INFO', `[BigQuery] Loaded ${validRows.length} rows into temp table ${tempTableId} using LOAD JOB (free)`);
197
197
  }
198
198
 
199
- // Use MERGE to insert only new rows (SQL-native deduplication)
199
+ // Use MERGE to insert new rows or update existing rows (SQL-native deduplication/overwrite)
200
200
  // This is more efficient than checking in JavaScript
201
201
  const mergeConditions = keyFields.map(f => `target.${f} = source.${f}`).join(' AND ');
202
- const mergeQuery = `
203
- MERGE \`${tablePath}\` AS target
204
- USING \`${tempTablePath}\` AS source
205
- ON ${mergeConditions}
206
- WHEN NOT MATCHED THEN
207
- INSERT ROW
208
- `;
209
-
210
- await query(mergeQuery, {}, logger);
211
202
 
212
- // Get count of rows that were actually inserted (not matched = new rows)
213
- // We can't directly get this from MERGE, so we'll query the temp table
214
- // and subtract what already exists
215
- const [existingBefore] = await query(`SELECT COUNT(*) as cnt FROM \`${tablePath}\``, {}, logger);
216
- const countBefore = existingBefore[0]?.cnt || 0;
203
+ // Build UPDATE clause - update all non-key fields
204
+ const allFields = schema.map(f => f.name);
205
+ const nonKeyFields = allFields.filter(f => !keyFields.includes(f));
206
+ const updateClause = nonKeyFields.map(f => `${f} = source.${f}`).join(', ');
217
207
 
218
- // Actually, MERGE doesn't return row count directly
219
- // Let's use a different approach - query what was inserted
208
+ // Count rows that will be inserted (don't exist in target) vs updated (already exist)
209
+ // Query BEFORE the MERGE to get accurate counts
220
210
  const [insertedCountResult] = await query(`
221
211
  SELECT COUNT(*) as inserted
222
212
  FROM \`${tempTablePath}\` AS source
@@ -227,12 +217,30 @@ async function insertRowsWithMerge(datasetId, tableId, rows, keyFields, logger =
227
217
  `, {}, logger);
228
218
 
229
219
  const rowsInserted = insertedCountResult[0]?.inserted || 0;
220
+ const rowsUpdated = validRows.length - rowsInserted;
221
+
222
+ // Now perform the MERGE (inserts new rows, updates existing rows)
223
+ const mergeQuery = `
224
+ MERGE \`${tablePath}\` AS target
225
+ USING \`${tempTablePath}\` AS source
226
+ ON ${mergeConditions}
227
+ WHEN MATCHED THEN
228
+ UPDATE SET ${updateClause}
229
+ WHEN NOT MATCHED THEN
230
+ INSERT ROW
231
+ `;
232
+
233
+ await query(mergeQuery, {}, logger);
230
234
 
231
235
  // Drop temp table
232
236
  await tempTable.delete();
233
237
 
234
238
  if (logger) {
235
- logger.log('INFO', `[BigQuery] MERGE completed: ${rowsInserted} new rows inserted into ${tablePath} (${validRows.length - rowsInserted} duplicates skipped via SQL)`);
239
+ if (rowsUpdated > 0) {
240
+ logger.log('INFO', `[BigQuery] MERGE completed: ${rowsInserted} new rows inserted, ${rowsUpdated} existing rows updated in ${tablePath}`);
241
+ } else {
242
+ logger.log('INFO', `[BigQuery] MERGE completed: ${rowsInserted} new rows inserted into ${tablePath} (${validRows.length - rowsInserted} duplicates skipped via SQL)`);
243
+ }
236
244
  }
237
245
 
238
246
  return rowsInserted;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.715",
3
+ "version": "1.0.716",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [