@@ -736,6 +736,94 @@ public function orWhere($key, $value = null, ?bool $escape = null)
736736 return $ this ->whereHaving ('QBWhere ' , $ key , $ value , 'OR ' , $ escape );
737737 }
738738
739+ /**
740+ * Generates a WHERE clause that compares two columns.
741+ *
742+ * @param non-empty-string $first First column name
743+ * @param non-empty-string|null $operator Comparison operator, or second column name when $second is null
744+ * @param non-empty-string|null $second Second column name
745+ * @param bool|null $escape Whether to protect identifiers
746+ *
747+ * @return $this
748+ *
749+ * @throws InvalidArgumentException
750+ */
751+ public function whereColumn (string $ first , ?string $ operator = null , ?string $ second = null , ?bool $ escape = null )
752+ {
753+ return $ this ->whereColumnHaving ('QBWhere ' , $ first , $ operator , $ second , 'AND ' , $ escape );
754+ }
755+
756+ /**
757+ * Generates an OR WHERE clause that compares two columns.
758+ *
759+ * @param non-empty-string $first First column name
760+ * @param non-empty-string|null $operator Comparison operator, or second column name when $second is null
761+ * @param non-empty-string|null $second Second column name
762+ * @param bool|null $escape Whether to protect identifiers
763+ *
764+ * @return $this
765+ *
766+ * @throws InvalidArgumentException
767+ */
768+ public function orWhereColumn (string $ first , ?string $ operator = null , ?string $ second = null , ?bool $ escape = null )
769+ {
770+ return $ this ->whereColumnHaving ('QBWhere ' , $ first , $ operator , $ second , 'OR ' , $ escape );
771+ }
772+
773+ /**
774+ * @used-by whereColumn()
775+ * @used-by orWhereColumn()
776+ *
777+ * @param 'QBHaving'|'QBWhere' $qbKey
778+ * @param non-empty-string $first First column name
779+ * @param non-empty-string|null $operator Comparison operator, or second column name when $second is null
780+ * @param non-empty-string|null $second Second column name
781+ * @param non-empty-string $type
782+ * @param bool|null $escape Whether to protect identifiers
783+ *
784+ * @return $this
785+ *
786+ * @throws InvalidArgumentException
787+ */
788+ protected function whereColumnHaving (string $ qbKey , string $ first , ?string $ operator = null , ?string $ second = null , string $ type = 'AND ' , ?bool $ escape = null )
789+ {
790+ if ($ second === null ) {
791+ $ second = $ operator ;
792+ $ operator = '= ' ;
793+ } elseif ($ operator === null ) {
794+ $ operator = '= ' ;
795+ }
796+
797+ $ first = trim ($ first );
798+ $ operator = trim ($ operator );
799+ $ second = trim ((string ) $ second );
800+
801+ if ($ first === '' || $ second === '' ) {
802+ throw new InvalidArgumentException (sprintf ('%s() expects $first and $second to be non-empty strings ' , debug_backtrace (0 , 2 )[1 ]['function ' ]));
803+ }
804+
805+ if (! in_array ($ operator , ['= ' , '!= ' , '<> ' , '< ' , '> ' , '<= ' , '>= ' ], true )) {
806+ throw new InvalidArgumentException (sprintf ('%s() expects $operator to be one of: =, !=, <>, <, >, <=, >= ' , debug_backtrace (0 , 2 )[1 ]['function ' ]));
807+ }
808+
809+ if (! is_bool ($ escape )) {
810+ $ escape = $ this ->db ->protectIdentifiers ;
811+ }
812+
813+ $ prefix = $ this ->{$ qbKey } === [] ? $ this ->groupGetType ('' ) : $ this ->groupGetType ($ type );
814+
815+ $ this ->{$ qbKey }[] = [
816+ 'columnComparison ' => true ,
817+ 'condition ' => $ prefix ,
818+ 'escape ' => $ escape ,
819+ 'first ' => $ first ,
820+ 'operator ' => $ operator ,
821+ 'second ' => $ second ,
822+ ];
823+
824+ return $ this ;
825+ }
826+
739827 /**
740828 * @used-by where()
741829 * @used-by orWhere()
@@ -1383,6 +1471,7 @@ protected function groupEndPrepare(string $clause = 'QBWhere')
13831471 * @used-by _like()
13841472 * @used-by whereHaving()
13851473 * @used-by _whereIn()
1474+ * @used-by whereColumnHaving()
13861475 * @used-by havingGroupStart()
13871476 */
13881477 protected function groupGetType (string $ type ): string
@@ -3114,6 +3203,12 @@ protected function compileWhereHaving(string $qbKey): string
31143203 continue ;
31153204 }
31163205
3206+ if (($ qbkey ['columnComparison ' ] ?? false ) === true ) {
3207+ $ qbkey = $ this ->compileColumnComparison ($ qbkey );
3208+
3209+ continue ;
3210+ }
3211+
31173212 if ($ qbkey ['escape ' ] === false ) {
31183213 $ qbkey = $ qbkey ['condition ' ];
31193214
@@ -3177,6 +3272,19 @@ protected function compileWhereHaving(string $qbKey): string
31773272 return '' ;
31783273 }
31793274
3275+ /**
3276+ * @param array{columnComparison: true, condition: string, escape: bool, first: string, operator: string, second: string} $condition
3277+ */
3278+ private function compileColumnComparison (array $ condition ): string
3279+ {
3280+ if ($ condition ['escape ' ]) {
3281+ $ condition ['first ' ] = $ this ->db ->protectIdentifiers ($ condition ['first ' ], false , true );
3282+ $ condition ['second ' ] = $ this ->db ->protectIdentifiers ($ condition ['second ' ], false , true );
3283+ }
3284+
3285+ return $ condition ['condition ' ] . $ condition ['first ' ] . ' ' . $ condition ['operator ' ] . ' ' . $ condition ['second ' ];
3286+ }
3287+
31803288 /**
31813289 * Escapes identifiers in GROUP BY statements at execution time.
31823290 *
0 commit comments