11import { loadValueConfig , resolveValueTier as resolveValueTierFn } from './value_config.js' ;
22import { classifyUsers } from './engine.js' ;
33import type { DbClient } from '../db/client.js' ;
4- import { sql } from 'drizzle-orm' ;
5- import { usersPg , usersSq , classificationHistoryPg , classificationHistorySq } from '../db/schema.js' ;
4+ import { sql , gte } from 'drizzle-orm' ;
5+ import { usersPg , usersSq , classificationHistoryPg , classificationHistorySq , dailyUsagePg , dailyUsageSq } from '../db/schema.js' ;
66
77export type ClassifyOptions = {
88 valueConfigPath : string ;
@@ -35,69 +35,59 @@ export async function runClassify(
3535 const valueConfig = loadValueConfig ( options . valueConfigPath ) ;
3636 const resolveValueTier = ( team : string | null ) => resolveValueTierFn ( team , valueConfig ) ;
3737
38- // Read phase: get 30 days of daily_usage aggregates
39- const usageQuery = isSqlite
40- ? `SELECT github_login, SUM(credits) as credits
41- FROM daily_usage
42- WHERE usage_date >= date('now', '-30 days')
43- GROUP BY github_login`
44- : `SELECT github_login, SUM(credits::numeric) as credits
45- FROM daily_usage
46- WHERE usage_date >= CURRENT_DATE - INTERVAL '30 days'
47- GROUP BY github_login` ;
48-
49- const usageRows = await ( isSqlite
50- ? Promise . resolve ( db . all ( sql . raw ( usageQuery ) ) as Array < { github_login : string ; credits : string } > )
51- : db . execute ( sql . raw ( usageQuery ) ) . then ( ( r : any ) => r . rows as Array < { github_login : string ; credits : string } > ) ) ;
38+ const dailyUsageTable = isSqlite ? dailyUsageSq : dailyUsagePg ;
39+ const usersTable = isSqlite ? usersSq : usersPg ;
40+
41+ // Calculate threshold date (30 days ago) in JS as YYYY-MM-DD
42+ const thresholdDate = new Date ( ) ;
43+ thresholdDate . setDate ( thresholdDate . getDate ( ) - 30 ) ;
44+ const dateString = thresholdDate . toISOString ( ) . slice ( 0 , 10 ) ;
45+
46+ // Read phase: get 30 days of daily_usage aggregates using Drizzle
47+ const usageRows = await db
48+ . select ( {
49+ github_login : dailyUsageTable . githubLogin ,
50+ credits : sql < number > `SUM(${ dailyUsageTable . credits } )` . mapWith ( Number ) ,
51+ } )
52+ . from ( dailyUsageTable )
53+ . where ( gte ( dailyUsageTable . usageDate , dateString ) )
54+ . groupBy ( dailyUsageTable . githubLogin ) ;
5255
5356 if ( usageRows . length === 0 ) {
5457 throw new Error ( 'No daily_usage data found. Run `burnrate etl` first.' ) ;
5558 }
5659
57- // Check for 30 distinct days
58- const distinctDaysQuery = isSqlite
59- ? `SELECT COUNT(DISTINCT usage_date) as days FROM daily_usage WHERE usage_date >= date('now', '-30 days')`
60- : `SELECT COUNT(DISTINCT usage_date) as days FROM daily_usage WHERE usage_date >= CURRENT_DATE - INTERVAL '30 days'` ;
61-
62- const daysResult = await ( isSqlite
63- ? Promise . resolve ( db . all ( sql . raw ( distinctDaysQuery ) ) as Array < { days : number } > )
64- : db . execute ( sql . raw ( distinctDaysQuery ) ) . then ( ( r : any ) => r . rows as Array < { days : number } > ) ) ;
65-
66- if ( daysResult [ 0 ] . days < 30 ) {
67- throw new Error ( `Insufficient data: only ${ daysResult [ 0 ] . days } distinct days found, need 30.` ) ;
60+ // Check for 30 distinct days using Drizzle
61+ const daysResult = await db
62+ . select ( {
63+ days : sql < number > ` COUNT(DISTINCT ${ dailyUsageTable . usageDate } )` . mapWith ( Number ) ,
64+ } )
65+ . from ( dailyUsageTable )
66+ . where ( gte ( dailyUsageTable . usageDate , dateString ) ) ;
67+
68+ const distinctDays = daysResult [ 0 ] ?. days ?? 0 ;
69+ if ( distinctDays < 30 ) {
70+ throw new Error ( `Insufficient data: only ${ distinctDays } distinct days found, need 30.` ) ;
6871 }
6972
70- // Read current users
71- const usersQuery = `SELECT github_login, team, consumption_tier, value_tier, bucket_updated_at FROM users` ;
72- const usersRows = await ( isSqlite
73- ? Promise . resolve ( db . all ( sql . raw ( usersQuery ) ) as Array < {
74- github_login : string ;
75- team : string | null ;
76- consumption_tier : string | null ;
77- value_tier : string | null ;
78- bucket_updated_at : string | null ;
79- } > )
80- : db . execute ( sql . raw ( usersQuery ) ) . then ( ( r : any ) => r . rows as Array < {
81- github_login : string ;
82- team : string | null ;
83- consumption_tier : string | null ;
84- value_tier : string | null ;
85- bucket_updated_at : string | null ;
86- } > ) ) ;
73+ // Read current users using Drizzle
74+ const usersRows = await db
75+ . select ( {
76+ github_login : usersTable . githubLogin ,
77+ team : usersTable . team ,
78+ consumption_tier : usersTable . consumptionTier ,
79+ value_tier : usersTable . valueTier ,
80+ bucket_updated_at : usersTable . bucketUpdatedAt ,
81+ } )
82+ . from ( usersTable ) ;
8783
8884 // Classify
89- const userCredits = usageRows . map ( ( r : { github_login : string ; credits : string } ) => ( {
85+ const userCredits = usageRows . map ( ( r : any ) => ( {
9086 githubLogin : r . github_login ,
9187 totalCredits : Number ( r . credits ) ,
9288 } ) ) ;
9389
94- const currentUsers = usersRows . map ( ( r : {
95- github_login : string ;
96- team : string | null ;
97- consumption_tier : string | null ;
98- value_tier : string | null ;
99- bucket_updated_at : string | null ;
100- } ) => ( {
90+ const currentUsers = usersRows . map ( ( r : any ) => ( {
10191 githubLogin : r . github_login ,
10292 team : r . team ,
10393 consumptionTier : r . consumption_tier ,
@@ -108,33 +98,36 @@ export async function runClassify(
10898 const effectiveDate = new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) ;
10999 const result = classifyUsers ( userCredits , currentUsers , { resolveValueTier } , options . reason ) ;
110100
111- // Write phase: use transactions for PostgreSQL, batch for SQLite
101+ // Write phase: use transaction for SQLite and PostgreSQL
112102 if ( result . changes . length > 0 ) {
113103 const now = new Date ( ) . toISOString ( ) ;
114104
115105 if ( isSqlite ) {
116- // SQLite: batch updates (better-sqlite3 transactions require different setup)
117- for ( const change of result . changes ) {
118- await db . update ( usersSq )
119- . set ( {
120- consumptionTier : change . consumptionTierNew ,
121- valueTier : change . valueTierNew ,
122- bucketUpdatedAt : now ,
123- updatedAt : sql `CURRENT_TIMESTAMP` ,
124- } )
125- . where ( sql `${ usersSq . githubLogin } = ${ change . githubLogin } ` ) ;
126-
127- await db . insert ( classificationHistorySq )
128- . values ( {
129- effectiveDate,
130- githubLogin : change . githubLogin ,
131- consumptionTierOld : change . consumptionTierOld ,
132- consumptionTierNew : change . consumptionTierNew ,
133- valueTier : change . valueTierNew ,
134- reason : change . reason ,
135- } )
136- . onConflictDoNothing ( ) ;
137- }
106+ db . transaction ( ( tx : any ) => {
107+ for ( const change of result . changes ) {
108+ tx . update ( usersSq )
109+ . set ( {
110+ consumptionTier : change . consumptionTierNew ,
111+ valueTier : change . valueTierNew ,
112+ bucketUpdatedAt : now ,
113+ updatedAt : sql `CURRENT_TIMESTAMP` ,
114+ } )
115+ . where ( sql `${ usersSq . githubLogin } = ${ change . githubLogin } ` )
116+ . run ( ) ;
117+
118+ tx . insert ( classificationHistorySq )
119+ . values ( {
120+ effectiveDate,
121+ githubLogin : change . githubLogin ,
122+ consumptionTierOld : change . consumptionTierOld ,
123+ consumptionTierNew : change . consumptionTierNew ,
124+ valueTier : change . valueTierNew ,
125+ reason : change . reason ,
126+ } )
127+ . onConflictDoNothing ( )
128+ . run ( ) ;
129+ }
130+ } ) ;
138131 } else {
139132 await db . transaction ( async ( tx : any ) => {
140133 for ( const change of result . changes ) {
0 commit comments