@@ -905,13 +905,35 @@ export class SearchManager {
905905 }
906906
907907 return new Brackets ( ( q : WhereExpression ) => {
908+ const convertGlobToLike = ( str : string ) : string => {
909+ // Match either an escaped character \\(.) or any unescaped SQL/glob special character
910+ return str . replace ( / \\ ( .) | ( [ * ? % _ ] ) / g, ( _ , escaped , special ) => {
911+ if ( escaped ) {
912+ if ( escaped === '*' || escaped === '?' ) return escaped ; // \* -> *, \? -> ?
913+ if ( escaped === '\\' ) return '\\\\' ; // \\ -> \\
914+ return '\\' + escaped ; // e.g., \% -> \%
915+ }
916+
917+ // Handle unescaped special characters
918+ if ( special === '*' ) return '%' ;
919+ if ( special === '?' ) return '_' ;
920+ return '\\' + special ; // % -> \%, _ -> \_
921+ } ) ;
922+ } ;
923+
908924 const createMatchString = ( str : string ) : string => {
909925 if (
910926 ( query as TextSearch ) . matchType ===
911927 TextSearchQueryMatchTypes . exact_match
912928 ) {
913929 return str ;
914930 }
931+ if (
932+ ( query as TextSearch ) . matchType ===
933+ TextSearchQueryMatchTypes . globMatch
934+ ) {
935+ return convertGlobToLike ( str ) ;
936+ }
915937 // MySQL uses C escape syntax in strings, details:
916938 // https://stackoverflow.com/questions/14926386/how-to-search-for-slash-in-mysql-and-why-escaping-not-required-for-wher
917939 if ( Config . Database . type === DatabaseType . mysql ) {
@@ -921,11 +943,19 @@ export class SearchManager {
921943 return `%${ str } %` ;
922944 } ;
923945
924- const LIKE = ( query as TextSearch ) . negate ? 'NOT LIKE' : 'LIKE' ;
946+ const isGlob = ( query as TextSearch ) . matchType === TextSearchQueryMatchTypes . globMatch ;
925947 // if the expression is negated, we use AND instead of OR as nowhere should that match
926948 const whereFN = ( query as TextSearch ) . negate ? 'andWhere' : 'orWhere' ;
927949 const whereFNRev = ( query as TextSearch ) . negate ? 'orWhere' : 'andWhere' ;
928950
951+ const getLikeExpr = ( fieldName : string , paramName : string ) : string => {
952+ const op = ( query as TextSearch ) . negate ? 'NOT LIKE' : 'LIKE' ;
953+ if ( isGlob ) {
954+ return `${ fieldName } ${ op } :${ paramName } ${ queryId } ESCAPE '\\' COLLATE ${ SQL_COLLATE } ` ;
955+ }
956+ return `${ fieldName } ${ op } :${ paramName } ${ queryId } COLLATE ${ SQL_COLLATE } ` ;
957+ } ;
958+
929959 const textParam : { [ key : string ] : unknown } = { } ;
930960 textParam [ 'text' + queryId ] = createMatchString (
931961 ( query as TextSearch ) . value
@@ -942,7 +972,7 @@ export class SearchManager {
942972 const alias = aliases [ 'directory' ] ?? 'directory' ;
943973 textParam [ 'fullPath' + queryId ] = createMatchString ( dirPathStr ) ;
944974 q [ whereFN ] (
945- `${ alias } .path ${ LIKE } :fullPath ${ queryId } COLLATE ` + SQL_COLLATE ,
975+ getLikeExpr ( `${ alias } .path` , 'fullPath' ) ,
946976 textParam
947977 ) ;
948978
@@ -953,15 +983,15 @@ export class SearchManager {
953983 directoryPath . name
954984 ) ;
955985 dq [ whereFNRev ] (
956- `${ alias } .name ${ LIKE } : dirName${ queryId } COLLATE ${ SQL_COLLATE } ` ,
986+ getLikeExpr ( `${ alias } .name` , ' dirName' ) ,
957987 textParam
958988 ) ;
959989 if ( dirPathStr . includes ( '/' ) ) {
960990 textParam [ 'parentName' + queryId ] = createMatchString (
961991 directoryPath . parent
962992 ) ;
963993 dq [ whereFNRev ] (
964- `${ alias } .path ${ LIKE } : parentName${ queryId } COLLATE ${ SQL_COLLATE } ` ,
994+ getLikeExpr ( `${ alias } .path` , ' parentName' ) ,
965995 textParam
966996 ) ;
967997 }
@@ -975,7 +1005,7 @@ export class SearchManager {
9751005 query . type === SearchQueryTypes . file_name
9761006 ) {
9771007 q [ whereFN ] (
978- ` media.name ${ LIKE } : text${ queryId } COLLATE ${ SQL_COLLATE } ` ,
1008+ getLikeExpr ( ' media.name' , ' text' ) ,
9791009 textParam
9801010 ) ;
9811011 }
@@ -985,7 +1015,7 @@ export class SearchManager {
9851015 query . type === SearchQueryTypes . caption
9861016 ) {
9871017 q [ whereFN ] (
988- ` media.metadata.caption ${ LIKE } : text${ queryId } COLLATE ${ SQL_COLLATE } ` ,
1018+ getLikeExpr ( ' media.metadata.caption' , ' text' ) ,
9891019 textParam
9901020 ) ;
9911021 }
@@ -995,13 +1025,13 @@ export class SearchManager {
9951025 query . type === SearchQueryTypes . position
9961026 ) {
9971027 q [ whereFN ] (
998- ` media.metadata.positionData.country ${ LIKE } : text${ queryId } COLLATE ${ SQL_COLLATE } ` ,
1028+ getLikeExpr ( ' media.metadata.positionData.country' , ' text' ) ,
9991029 textParam
10001030 ) [ whereFN ] (
1001- ` media.metadata.positionData.state ${ LIKE } : text${ queryId } COLLATE ${ SQL_COLLATE } ` ,
1031+ getLikeExpr ( ' media.metadata.positionData.state' , ' text' ) ,
10021032 textParam
10031033 ) [ whereFN ] (
1004- ` media.metadata.positionData.city ${ LIKE } : text${ queryId } COLLATE ${ SQL_COLLATE } ` ,
1034+ getLikeExpr ( ' media.metadata.positionData.city' , ' text' ) ,
10051035 textParam
10061036 ) ;
10071037 }
@@ -1010,44 +1040,39 @@ export class SearchManager {
10101040 const matchArrayField = ( fieldName : string ) : void => {
10111041 q [ whereFN ] (
10121042 new Brackets ( ( qbr ) : void => {
1013- if (
1014- ( query as TextSearch ) . matchType !==
1015- TextSearchQueryMatchTypes . exact_match
1016- ) {
1043+ const isExact = ( query as TextSearch ) . matchType === TextSearchQueryMatchTypes . exact_match ;
1044+
1045+ if ( ! isExact && ! isGlob ) {
10171046 qbr [ whereFN ] (
1018- ` ${ fieldName } ${ LIKE } : text${ queryId } COLLATE ${ SQL_COLLATE } ` ,
1047+ getLikeExpr ( fieldName , ' text' ) ,
10191048 textParam
10201049 ) ;
10211050 } else {
10221051 qbr [ whereFN ] (
10231052 new Brackets ( ( qb ) : void => {
1024- textParam [ 'CtextC' + queryId ] = `%,${
1025- ( query as TextSearch ) . value
1026- } ,%`;
1027- textParam [ 'Ctext' + queryId ] = `%,${
1028- ( query as TextSearch ) . value
1029- } `;
1030- textParam [ 'textC' + queryId ] = `${
1031- ( query as TextSearch ) . value
1032- } ,%`;
1033- textParam [ 'text_exact' + queryId ] = `${
1034- ( query as TextSearch ) . value
1035- } `;
1053+ const globPattern = isGlob ? convertGlobToLike ( ( query as TextSearch ) . value ) : ( query as TextSearch ) . value ;
1054+ const esc = isGlob ? " ESCAPE '\\'" : "" ;
1055+ const op = ( query as TextSearch ) . negate ? 'NOT LIKE' : 'LIKE' ;
1056+
1057+ textParam [ 'CtextC' + queryId ] = `%,${ globPattern } ,%` ;
1058+ textParam [ 'Ctext' + queryId ] = `%,${ globPattern } ` ;
1059+ textParam [ 'textC' + queryId ] = `${ globPattern } ,%` ;
1060+ textParam [ 'text_exact' + queryId ] = `${ globPattern } ` ;
10361061
10371062 qb [ whereFN ] (
1038- `${ fieldName } ${ LIKE } :CtextC${ queryId } COLLATE ${ SQL_COLLATE } ` ,
1063+ `${ fieldName } ${ op } :CtextC${ queryId } ${ esc } COLLATE ${ SQL_COLLATE } ` ,
10391064 textParam
10401065 ) ;
10411066 qb [ whereFN ] (
1042- `${ fieldName } ${ LIKE } :Ctext${ queryId } COLLATE ${ SQL_COLLATE } ` ,
1067+ `${ fieldName } ${ op } :Ctext${ queryId } ${ esc } COLLATE ${ SQL_COLLATE } ` ,
10431068 textParam
10441069 ) ;
10451070 qb [ whereFN ] (
1046- `${ fieldName } ${ LIKE } :textC${ queryId } COLLATE ${ SQL_COLLATE } ` ,
1071+ `${ fieldName } ${ op } :textC${ queryId } ${ esc } COLLATE ${ SQL_COLLATE } ` ,
10471072 textParam
10481073 ) ;
10491074 qb [ whereFN ] (
1050- `${ fieldName } ${ LIKE } :text_exact${ queryId } COLLATE ${ SQL_COLLATE } ` ,
1075+ `${ fieldName } ${ op } :text_exact${ queryId } ${ esc } COLLATE ${ SQL_COLLATE } ` ,
10511076 textParam
10521077 ) ;
10531078 } )
0 commit comments