@@ -7,6 +7,7 @@ import type {
77} from './types' ;
88import { FilterOperators } from './types' ;
99import { applyPlugins } from './utils' ;
10+ import { mapScreenResolutionToDeviceType , type DeviceType } from './screen-resolution-to-device-type' ;
1011
1112export class SimpleQueryBuilder {
1213 private config : SimpleQueryConfig ;
@@ -23,6 +24,60 @@ export class SimpleQueryBuilder {
2324 this . websiteDomain = websiteDomain ;
2425 }
2526
27+ private getDeviceTypeFilterCondition ( deviceType : DeviceType ) : string {
28+ // Create SQL condition that matches the same logic as mapScreenResolutionToDeviceType
29+ // This replicates the heuristics from screen-resolution-to-device-type.ts in SQL
30+
31+ // First, get common/known resolutions for exact matches
32+ const commonResolutions : Record < string , DeviceType > = {
33+ '896x414' : 'mobile' , '844x390' : 'mobile' , '932x430' : 'mobile' , '800x360' : 'mobile' ,
34+ '780x360' : 'mobile' , '736x414' : 'mobile' , '667x375' : 'mobile' , '640x360' : 'mobile' , '568x320' : 'mobile' ,
35+ '1366x1024' : 'tablet' , '1280x800' : 'tablet' , '1180x820' : 'tablet' , '1024x768' : 'tablet' , '1280x720' : 'tablet' ,
36+ '1366x768' : 'laptop' , '1440x900' : 'laptop' , '1536x864' : 'laptop' ,
37+ '1920x1080' : 'desktop' , '2560x1440' : 'desktop' , '3840x2160' : 'desktop' ,
38+ '3440x1440' : 'ultrawide' , '3840x1600' : 'ultrawide' , '5120x1440' : 'ultrawide' ,
39+ } ;
40+
41+ const exactMatches = Object . entries ( commonResolutions )
42+ . filter ( ( [ _ , type ] ) => type === deviceType )
43+ . map ( ( [ resolution , _ ] ) => `'${ resolution } '` )
44+ . join ( ', ' ) ;
45+
46+ // SQL for parsing resolution dimensions
47+ const widthExpr = "toFloat64(substring(screen_resolution, 1, position(screen_resolution, 'x') - 1))" ;
48+ const heightExpr = "toFloat64(substring(screen_resolution, position(screen_resolution, 'x') + 1))" ;
49+ const longSideExpr = `greatest(${ widthExpr } , ${ heightExpr } )` ;
50+ const shortSideExpr = `least(${ widthExpr } , ${ heightExpr } )` ;
51+ const aspectExpr = `${ longSideExpr } / ${ shortSideExpr } ` ;
52+
53+ // Device type heuristics (matching screen-resolution-to-device-type.ts logic)
54+ const heuristicCondition = ( ( ) => {
55+ switch ( deviceType ) {
56+ case 'mobile' :
57+ return `(${ shortSideExpr } <= 480)` ;
58+ case 'tablet' :
59+ return `(${ shortSideExpr } <= 900 AND ${ shortSideExpr } > 480)` ;
60+ case 'laptop' :
61+ return `(${ longSideExpr } <= 1600 AND ${ shortSideExpr } > 900)` ;
62+ case 'desktop' :
63+ return `(${ longSideExpr } <= 3000 AND ${ longSideExpr } > 1600)` ;
64+ case 'ultrawide' :
65+ return `(${ aspectExpr } >= 2.0 AND ${ longSideExpr } >= 2560)` ;
66+ case 'watch' :
67+ return `(${ longSideExpr } <= 400 AND ${ aspectExpr } >= 0.85 AND ${ aspectExpr } <= 1.15)` ;
68+ case 'unknown' :
69+ default :
70+ return '1 = 0' ; // Never matches
71+ }
72+ } ) ( ) ;
73+
74+ // Combine exact matches and heuristics
75+ if ( exactMatches ) {
76+ return `(screen_resolution IN (${ exactMatches } ) OR ${ heuristicCondition } )` ;
77+ }
78+ return heuristicCondition ;
79+ }
80+
2681 private buildFilter ( filter : Filter , index : number ) {
2782 if (
2883 this . config . allowedFilters &&
@@ -94,6 +149,18 @@ export class SimpleQueryBuilder {
94149 } ;
95150 }
96151
152+ // Special handling for device_type filters - convert to screen_resolution filters
153+ if ( filter . field === 'device_type' && typeof filter . value === 'string' ) {
154+ const deviceType = filter . value as DeviceType ;
155+ const condition = this . getDeviceTypeFilterCondition ( deviceType ) ;
156+
157+ // Return the condition directly without parameters since it's self-contained
158+ return {
159+ clause : condition ,
160+ params : { } ,
161+ } ;
162+ }
163+
97164 if ( filter . op === 'like' ) {
98165 return {
99166 clause : `${ filter . field } ${ operator } {${ key } :String}` ,
0 commit comments