@@ -7,6 +7,7 @@ class DataRetentionJob < ApplicationJob
77 DEFAULT_RETENTION_DAYS = 31
88 FREE_PLAN_RETENTION_DAYS = 5
99 BATCH_SIZE = 50_000 # Larger batches since we have indexes on occurred_at
10+ PURGEABLE_TABLES = %w[ events performance_events ] . freeze
1011
1112 # Delete events and performance events based on plan-specific retention.
1213 # Runs daily via Sidekiq Cron.
@@ -70,24 +71,20 @@ def delete_old_performance_events_for_accounts(cutoff_date, account_ids)
7071 delete_in_batches_for_accounts ( "performance_events" , cutoff_date , account_ids )
7172 end
7273
73- # Efficient batch deletion using raw SQL (global — all accounts)
7474 def delete_in_batches ( table_name , cutoff_date )
75+ validate_table_name! ( table_name )
7576 total_deleted = 0
7677 conn = ActiveRecord ::Base . connection
77- sanitized_table = conn . quote_table_name ( table_name )
78- sanitized_cutoff = conn . quote ( cutoff_date . utc )
78+ quoted_table = conn . quote_table_name ( table_name )
7979
8080 loop do
81- sql = <<-SQL . squish
82- DELETE FROM #{ sanitized_table }
83- WHERE ctid IN (
84- SELECT ctid FROM #{ sanitized_table }
85- WHERE occurred_at < #{ sanitized_cutoff }
86- LIMIT #{ BATCH_SIZE }
87- )
88- SQL
89-
90- result = ActiveRecord ::Base . connection . execute ( sql )
81+ sql = ActiveRecord ::Base . sanitize_sql_array ( [
82+ "DELETE FROM #{ quoted_table } WHERE ctid IN (SELECT ctid FROM #{ quoted_table } WHERE occurred_at < ? LIMIT ?)" ,
83+ cutoff_date . utc ,
84+ BATCH_SIZE
85+ ] )
86+
87+ result = conn . execute ( sql )
9188 deleted_count = result . cmd_tuples
9289 total_deleted += deleted_count
9390
@@ -100,27 +97,27 @@ def delete_in_batches(table_name, cutoff_date)
10097 total_deleted
10198 end
10299
103- # Efficient batch deletion scoped to specific accounts (via project_id join)
104100 def delete_in_batches_for_accounts ( table_name , cutoff_date , account_ids )
101+ validate_table_name! ( table_name )
105102 total_deleted = 0
106103 conn = ActiveRecord ::Base . connection
107- sanitized_table = conn . quote_table_name ( table_name )
108- sanitized_cutoff = conn . quote ( cutoff_date . utc )
109- sanitized_ids = account_ids . map { |id | conn . quote ( id ) } . join ( ", " )
104+ quoted_table = conn . quote_table_name ( table_name )
110105
111106 loop do
112- sql = <<-SQL . squish
113- DELETE FROM #{ sanitized_table }
114- WHERE ctid IN (
115- SELECT #{ sanitized_table } .ctid FROM #{ sanitized_table }
116- INNER JOIN projects ON projects.id = #{ sanitized_table } .project_id
117- WHERE #{ sanitized_table } .occurred_at < #{ sanitized_cutoff }
118- AND projects.account_id IN (#{ sanitized_ids } )
119- LIMIT #{ BATCH_SIZE }
120- )
121- SQL
122-
123- result = ActiveRecord ::Base . connection . execute ( sql )
107+ sql = ActiveRecord ::Base . sanitize_sql_array ( [
108+ "DELETE FROM #{ quoted_table } WHERE ctid IN (" \
109+ "SELECT #{ quoted_table } .ctid FROM #{ quoted_table } " \
110+ "INNER JOIN projects ON projects.id = #{ quoted_table } .project_id " \
111+ "WHERE #{ quoted_table } .occurred_at < ? " \
112+ "AND projects.account_id IN (?) " \
113+ "LIMIT ?" \
114+ ")" ,
115+ cutoff_date . utc ,
116+ account_ids ,
117+ BATCH_SIZE
118+ ] )
119+
120+ result = conn . execute ( sql )
124121 deleted_count = result . cmd_tuples
125122 total_deleted += deleted_count
126123
@@ -132,4 +129,8 @@ def delete_in_batches_for_accounts(table_name, cutoff_date, account_ids)
132129
133130 total_deleted
134131 end
132+
133+ def validate_table_name! ( table_name )
134+ raise ArgumentError , "Unknown table: #{ table_name } " unless PURGEABLE_TABLES . include? ( table_name )
135+ end
135136end
0 commit comments