@@ -5,6 +5,8 @@ import { registry } from '../../lang/registry.js';
55const TBL_PLACEHOLDER = Symbol . for ( 'tbl_placeholder' ) ;
66const GROUPING_META = Symbol . for ( 'grouping_meta' ) ;
77const WINDOW_META = Symbol . for ( 'window_meta' ) ;
8+ const hasExplicitTimezone = ( value ) => typeof value === 'string'
9+ && / (?: z | [ + - ] \d { 2 } (?: : ? \d { 2 } ) ? ) $ / i. test ( value . trim ( ) ) ;
810
911export class ExprEngine {
1012
@@ -196,17 +198,104 @@ export class ExprEngine {
196198 ) ;
197199 }
198200 // -----------
201+ if ( L === null || L === undefined ) return null ;
199202 const DT = dataType . value ( ) ;
203+ const asString = ( value ) => String ( value ) ;
204+ const asNumber = ( value ) => Number ( value ) ;
205+ const asIsoTimestamp = ( date ) => date . toISOString ( ) . slice ( 0 , 19 ) . replace ( 'T' , ' ' ) ;
206+ const asIsoTime = ( date ) => date . toISOString ( ) . slice ( 11 , 19 ) ;
207+ const normalizeBigInt = ( value ) => {
208+ if ( typeof value === 'bigint' ) return value ;
209+ if ( typeof value === 'number' ) return BigInt ( Math . trunc ( value ) ) ;
210+ if ( typeof value === 'string' ) return BigInt ( value . trim ( ) ) ;
211+ return BigInt ( value ) ;
212+ } ;
213+ const normalizeJson = ( value ) => {
214+ if ( typeof value !== 'string' ) return value ;
215+ return JSON . parse ( value ) ;
216+ } ;
217+ const normalizeBytea = ( value ) => {
218+ if ( value instanceof Uint8Array ) return value ;
219+ if ( typeof Buffer !== 'undefined' && value instanceof Buffer ) return value ;
220+ if ( value instanceof ArrayBuffer ) return new Uint8Array ( value ) ;
221+ if ( Array . isArray ( value ) ) return Uint8Array . from ( value ) ;
222+ if ( typeof value === 'string' ) return new TextEncoder ( ) . encode ( value ) ;
223+ return value ;
224+ } ;
225+ const normalizeDate = ( value ) => {
226+ if ( value instanceof Date ) return value . toISOString ( ) . slice ( 0 , 10 ) ;
227+ const str = String ( value ) . trim ( ) ;
228+ if ( / ^ \d { 4 } - \d { 2 } - \d { 2 } $ / . test ( str ) ) return str ;
229+ return new Date ( str ) . toISOString ( ) . slice ( 0 , 10 ) ;
230+ } ;
231+ const normalizeTime = ( value ) => {
232+ if ( value instanceof Date ) return asIsoTime ( value ) ;
233+ const str = String ( value ) . trim ( ) ;
234+ const match = str . match ( / \d { 2 } : \d { 2 } : \d { 2 } (?: \. \d { 1 , 6 } ) ? / ) ;
235+ if ( match ) return match [ 0 ] ;
236+ return asIsoTime ( new Date ( str ) ) ;
237+ } ;
238+ const normalizeTimestamp = ( value ) => {
239+ if ( value instanceof Date ) return asIsoTimestamp ( value ) ;
240+ const str = String ( value ) . trim ( ) ;
241+ if ( / ^ \d { 4 } - \d { 2 } - \d { 2 } [ t ] \d { 2 } : \d { 2 } : \d { 2 } (?: \. \d { 1 , 6 } ) ? $ / . test ( str ) ) {
242+ return str . replace ( 'T' , ' ' ) ;
243+ }
244+ return asIsoTimestamp ( new Date ( str ) ) ;
245+ } ;
246+ const normalizeTimestamptz = ( value ) => {
247+ if ( value instanceof Date ) return value . toISOString ( ) ;
248+ const str = String ( value ) . trim ( ) ;
249+ if ( / ^ \d { 4 } - \d { 2 } - \d { 2 } [ t ] \d { 2 } : \d { 2 } : \d { 2 } (?: \. \d { 1 , 6 } ) ? (?: z | [ + - ] \d { 2 } (?: : ? \d { 2 } ) ? ) $ / i. test ( str ) ) {
250+ return str ;
251+ }
252+ return new Date ( str ) . toISOString ( ) ;
253+ } ;
200254 switch ( DT ) {
201255 case 'SMALLINT' :
202256 case 'INT' :
203257 case 'INTEGER' :
204- case 'BIGINT' :
205258 case 'SERIAL' :
259+ return asNumber ( L ) ;
260+ case 'NUMERIC' :
261+ case 'DECIMAL' :
262+ case 'REAL' :
263+ case 'DOUBLE PRECISION' :
264+ return asNumber ( L ) ;
265+ case 'BIGINT' :
206266 case 'BIGSERIAL' :
207- return Number ( L ) ;
208- case 'TEXT' : return String ( L ) ;
209- case 'BOOLEAN' : return Boolean ( L ) ;
267+ return normalizeBigInt ( L ) ;
268+ case 'TEXT' :
269+ case 'VARCHAR' :
270+ case 'CHAR' :
271+ case 'UUID' :
272+ case 'INTERVAL' :
273+ return asString ( L ) ;
274+ case 'JSON' :
275+ case 'JSONB' :
276+ return normalizeJson ( L ) ;
277+ case 'ARRAY' :
278+ return typeof L === 'string' ? JSON . parse ( L ) : L ;
279+ case 'BYTEA' :
280+ return normalizeBytea ( L ) ;
281+ case 'DATE' :
282+ return normalizeDate ( L ) ;
283+ case 'TIME' :
284+ return normalizeTime ( L ) ;
285+ case 'TIMESTAMP' :
286+ return normalizeTimestamp ( L ) ;
287+ case 'TIMESTAMPTZ' :
288+ return normalizeTimestamptz ( L ) ;
289+ case 'BOOLEAN' :
290+ if ( typeof L === 'boolean' ) return L ;
291+ if ( typeof L === 'number' ) return L !== 0 ;
292+ if ( typeof L === 'bigint' ) return L !== 0n ;
293+ if ( typeof L === 'string' ) {
294+ const normalized = L . trim ( ) . toLowerCase ( ) ;
295+ if ( [ 'true' , 't' , '1' , 'yes' , 'y' , 'on' ] . includes ( normalized ) ) return true ;
296+ if ( [ 'false' , 'f' , '0' , 'no' , 'n' , 'off' ] . includes ( normalized ) ) return false ;
297+ }
298+ return Boolean ( L ) ;
210299 default : return L ;
211300 }
212301 }
@@ -547,14 +636,70 @@ export class ExprEngine {
547636
548637 case 'UPPER' : return String ( args [ 0 ] ?? '' ) . toUpperCase ( ) ;
549638
639+ case 'CONCAT' : return args . map ( ( arg ) => arg == null ? '' : String ( arg ) ) . join ( '' ) ;
640+
641+ case 'CONCAT_WS' : {
642+ const [ separator , ...rest ] = args ;
643+ return rest
644+ . filter ( ( arg ) => arg != null )
645+ . map ( ( arg ) => String ( arg ) )
646+ . join ( String ( separator ?? '' ) ) ;
647+ }
648+
649+ case 'TRIM' :
650+ return args [ 0 ] == null ? null : String ( args [ 0 ] ) . trim ( ) ;
651+
652+ case 'LTRIM' :
653+ return args [ 0 ] == null ? null : String ( args [ 0 ] ) . replace ( / ^ \s + / , '' ) ;
654+
655+ case 'RTRIM' :
656+ return args [ 0 ] == null ? null : String ( args [ 0 ] ) . replace ( / \s + $ / , '' ) ;
657+
550658 case 'LENGTH' : return args [ 0 ] == null ? null : String ( args [ 0 ] ) . length ;
551659
552660 case 'ABS' : return Math . abs ( Number ( args [ 0 ] ) ) ;
553661
662+ case 'ROUND' : {
663+ const value = Number ( args [ 0 ] ) ;
664+ const scale = args [ 1 ] == null ? 0 : Number ( args [ 1 ] ) ;
665+ const factor = 10 ** scale ;
666+ return Math . round ( value * factor ) / factor ;
667+ }
668+
669+ case 'FLOOR' : return Math . floor ( Number ( args [ 0 ] ) ) ;
670+
671+ case 'CEIL' :
672+ case 'CEILING' : return Math . ceil ( Number ( args [ 0 ] ) ) ;
673+
674+ case 'GREATEST' :
675+ return args . reduce ( ( max , cur ) => max == null || cur > max ? cur : max , null ) ;
676+
677+ case 'LEAST' :
678+ return args . reduce ( ( min , cur ) => min == null || cur < min ? cur : min , null ) ;
679+
554680 case 'COALESCE' : return args . reduce ( ( prev , cur ) => prev !== null ? prev : cur , null ) ;
555681
556682 case 'NULLIF' : return _eq ( args [ 0 ] , args [ 1 ] ) ? null : args [ 0 ] ;
557683
684+ case 'REPLACE' :
685+ return args [ 0 ] == null ? null : String ( args [ 0 ] ) . split ( String ( args [ 1 ] ?? '' ) ) . join ( String ( args [ 2 ] ?? '' ) ) ;
686+
687+ case 'SUBSTRING' :
688+ case 'SUBSTR' : {
689+ if ( args [ 0 ] == null ) return null ;
690+ const source = String ( args [ 0 ] ) ;
691+ const start = Math . max ( Number ( args [ 1 ] ?? 1 ) - 1 , 0 ) ;
692+ const length = args [ 2 ] == null ? undefined : Math . max ( Number ( args [ 2 ] ) , 0 ) ;
693+ return length === undefined ? source . slice ( start ) : source . slice ( start , start + length ) ;
694+ }
695+
696+ case 'POSITION' : {
697+ const needle = String ( args [ 0 ] ?? '' ) ;
698+ const haystack = String ( args [ 1 ] ?? '' ) ;
699+ const idx = haystack . indexOf ( needle ) ;
700+ return idx < 0 ? 0 : idx + 1 ;
701+ }
702+
558703 case 'JSON_BUILD_ARRAY' :
559704 case 'JSON_ARRAY' : return args ;
560705
@@ -577,31 +722,60 @@ export class ExprEngine {
577722
578723 case 'CURRENT_TIMESTAMP' : return new Date ( ) . toISOString ( ) ;
579724
725+ case 'MAKE_DATE' : {
726+ const [ year , month , day ] = args . map ( ( arg ) => Number ( arg ) ) ;
727+ return `${ String ( year ) . padStart ( 4 , '0' ) } -${ String ( month ) . padStart ( 2 , '0' ) } -${ String ( day ) . padStart ( 2 , '0' ) } ` ;
728+ }
729+
730+ case 'MAKE_TIME' : {
731+ const [ hour , minute , second ] = args . map ( ( arg ) => Number ( arg ) ) ;
732+ return `${ String ( hour ) . padStart ( 2 , '0' ) } :${ String ( minute ) . padStart ( 2 , '0' ) } :${ String ( second ) . padStart ( 2 , '0' ) } ` ;
733+ }
734+
735+ case 'MAKE_TIMESTAMP' : {
736+ const [ year , month , day , hour , minute , second ] = args . map ( ( arg ) => Number ( arg ) ) ;
737+ return `${ String ( year ) . padStart ( 4 , '0' ) } -${ String ( month ) . padStart ( 2 , '0' ) } -${ String ( day ) . padStart ( 2 , '0' ) } ${ String ( hour ) . padStart ( 2 , '0' ) } :${ String ( minute ) . padStart ( 2 , '0' ) } :${ String ( second ) . padStart ( 2 , '0' ) } ` ;
738+ }
739+
580740 case 'EXTRACT' :
581741 const [ field , date1 ] = args ;
582742 const dateObj1 = new Date ( date1 ) ;
583743 const fieldName = field . toUpperCase ( ) ;
744+ const useUTC1 = hasExplicitTimezone ( date1 ) ;
745+ const getYear1 = useUTC1 ? dateObj1 . getUTCFullYear ( ) : dateObj1 . getFullYear ( ) ;
746+ const getMonth1 = useUTC1 ? dateObj1 . getUTCMonth ( ) : dateObj1 . getMonth ( ) ;
747+ const getDay1 = useUTC1 ? dateObj1 . getUTCDate ( ) : dateObj1 . getDate ( ) ;
748+ const getHour1 = useUTC1 ? dateObj1 . getUTCHours ( ) : dateObj1 . getHours ( ) ;
749+ const getMinute1 = useUTC1 ? dateObj1 . getUTCMinutes ( ) : dateObj1 . getMinutes ( ) ;
750+ const getSecond1 = useUTC1 ? dateObj1 . getUTCSeconds ( ) : dateObj1 . getSeconds ( ) ;
584751 switch ( fieldName ) {
585- case 'YEAR' : return dateObj1 . getFullYear ( ) ;
586- case 'MONTH' : return dateObj1 . getMonth ( ) + 1 ;
587- case 'DAY' : return dateObj1 . getDate ( ) ;
588- case 'HOUR' : return dateObj1 . getHours ( ) ;
589- case 'MINUTE' : return dateObj1 . getMinutes ( ) ;
590- case 'SECOND' : return dateObj1 . getSeconds ( ) ;
752+ case 'YEAR' : return getYear1 ;
753+ case 'MONTH' : return getMonth1 + 1 ;
754+ case 'DAY' : return getDay1 ;
755+ case 'HOUR' : return getHour1 ;
756+ case 'MINUTE' : return getMinute1 ;
757+ case 'SECOND' : return getSecond1 ;
591758 default : throw new Error ( `Unsupported date/time field ${ field } ` ) ;
592759 }
593760
594761 case 'DATE_TRUNC' :
595762 const [ dateUnit , date2 ] = args ;
596763 const dateObj2 = new Date ( date2 ) ;
597764 const unitName = dateUnit . toUpperCase ( ) ;
765+ const useUTC2 = hasExplicitTimezone ( date2 ) ;
766+ const getYear2 = useUTC2 ? dateObj2 . getUTCFullYear ( ) : dateObj2 . getFullYear ( ) ;
767+ const getMonth2 = ( useUTC2 ? dateObj2 . getUTCMonth ( ) : dateObj2 . getMonth ( ) ) + 1 ;
768+ const getDay2 = useUTC2 ? dateObj2 . getUTCDate ( ) : dateObj2 . getDate ( ) ;
769+ const getHour2 = useUTC2 ? dateObj2 . getUTCHours ( ) : dateObj2 . getHours ( ) ;
770+ const getMinute2 = useUTC2 ? dateObj2 . getUTCMinutes ( ) : dateObj2 . getMinutes ( ) ;
771+ const getSecond2 = useUTC2 ? dateObj2 . getUTCSeconds ( ) : dateObj2 . getSeconds ( ) ;
598772 switch ( unitName ) {
599- case 'YEAR' : return `${ dateObj2 . getFullYear ( ) } -01-01` ;
600- case 'MONTH' : return `${ dateObj2 . getFullYear ( ) } -${ String ( dateObj2 . getMonth ( ) + 1 ) . padStart ( 2 , '0' ) } -01` ;
601- case 'DAY' : return `${ dateObj2 . getFullYear ( ) } -${ String ( dateObj2 . getMonth ( ) + 1 ) . padStart ( 2 , '0' ) } -${ String ( dateObj2 . getDate ( ) ) . padStart ( 2 , '0' ) } ` ;
602- case 'HOUR' : return `${ dateObj2 . getFullYear ( ) } -${ String ( dateObj2 . getMonth ( ) + 1 ) . padStart ( 2 , '0' ) } -${ String ( dateObj2 . getDate ( ) ) . padStart ( 2 , '0' ) } T${ String ( dateObj2 . getHours ( ) ) . padStart ( 2 , '0' ) } :00:00` ;
603- case 'MINUTE' : return `${ dateObj2 . getFullYear ( ) } -${ String ( dateObj2 . getMonth ( ) + 1 ) . padStart ( 2 , '0' ) } -${ String ( dateObj2 . getDate ( ) ) . padStart ( 2 , '0' ) } T${ String ( dateObj2 . getHours ( ) ) . padStart ( 2 , '0' ) } :${ String ( dateObj2 . getMinutes ( ) ) . padStart ( 2 , '0' ) } :00` ;
604- case 'SECOND' : return `${ dateObj2 . getFullYear ( ) } -${ String ( dateObj2 . getMonth ( ) + 1 ) . padStart ( 2 , '0' ) } -${ String ( dateObj2 . getDate ( ) ) . padStart ( 2 , '0' ) } T${ String ( dateObj2 . getHours ( ) ) . padStart ( 2 , '0' ) } :${ String ( dateObj2 . getMinutes ( ) ) . padStart ( 2 , '0' ) } :${ String ( dateObj2 . getSeconds ( ) ) . padStart ( 2 , '0' ) } ` ;
773+ case 'YEAR' : return `${ getYear2 } -01-01` ;
774+ case 'MONTH' : return `${ getYear2 } -${ String ( getMonth2 ) . padStart ( 2 , '0' ) } -01` ;
775+ case 'DAY' : return `${ getYear2 } -${ String ( getMonth2 ) . padStart ( 2 , '0' ) } -${ String ( getDay2 ) . padStart ( 2 , '0' ) } ` ;
776+ case 'HOUR' : return `${ getYear2 } -${ String ( getMonth2 ) . padStart ( 2 , '0' ) } -${ String ( getDay2 ) . padStart ( 2 , '0' ) } T${ String ( getHour2 ) . padStart ( 2 , '0' ) } :00:00` ;
777+ case 'MINUTE' : return `${ getYear2 } -${ String ( getMonth2 ) . padStart ( 2 , '0' ) } -${ String ( getDay2 ) . padStart ( 2 , '0' ) } T${ String ( getHour2 ) . padStart ( 2 , '0' ) } :${ String ( getMinute2 ) . padStart ( 2 , '0' ) } :00` ;
778+ case 'SECOND' : return `${ getYear2 } -${ String ( getMonth2 ) . padStart ( 2 , '0' ) } -${ String ( getDay2 ) . padStart ( 2 , '0' ) } T${ String ( getHour2 ) . padStart ( 2 , '0' ) } :${ String ( getMinute2 ) . padStart ( 2 , '0' ) } :${ String ( getSecond2 ) . padStart ( 2 , '0' ) } ` ;
605779 default : throw new Error ( `Unsupported date/time unit ${ dateUnit } ` ) ;
606780 }
607781
@@ -999,4 +1173,4 @@ function likeCompare(str, pattern) {
9991173 } catch {
10001174 return false ;
10011175 }
1002- }
1176+ }
0 commit comments