@@ -509,6 +509,17 @@ class BaseMigrator {
509509 return { skip : true } ;
510510 }
511511
512+ if ( this . isNumericStringTypeError ( error ) ) {
513+ const fieldTypeMap = this . extractNumericFieldTypesFromError ( error ) ;
514+ const sanitized = this . convertNumericStringFields ( upsertData , fieldTypeMap ) ;
515+
516+ if ( sanitized . modified ) {
517+ const fieldSummary = sanitized . convertedFields . join ( ', ' ) || 'numeric fields' ;
518+ this . manager . logger . warn ( `${ this . modelName } ${ recordLabel } : converted string inputs for ${ fieldSummary } ; retrying upsert.` ) ;
519+ return { retryData : sanitized . data } ;
520+ }
521+ }
522+
512523 return null ;
513524 }
514525
@@ -522,6 +533,267 @@ class BaseMigrator {
522533 return message . includes ( 'Unable to fit integer value' ) ;
523534 }
524535
536+ /**
537+ * Determine if the Prisma error was triggered by passing string values to numeric fields
538+ * @param {Error } error
539+ * @returns {boolean }
540+ */
541+ isNumericStringTypeError ( error ) {
542+ if ( ! error ) {
543+ return false ;
544+ }
545+
546+ const message = error . message || '' ;
547+ const numericTypeMismatchPattern = / E x p e c t e d \s + (?: F l o a t | I n t | D e c i m a l | B i g I n t | D o u b l e | R e a l | N u m e r i c ) [ ^ , ] * , \s * p r o v i d e d \s + S t r i n g / i;
548+
549+ if ( numericTypeMismatchPattern . test ( message ) ) {
550+ return true ;
551+ }
552+
553+ if ( Prisma ?. PrismaClientValidationError && error instanceof Prisma . PrismaClientValidationError ) {
554+ return numericTypeMismatchPattern . test ( message ) ;
555+ }
556+
557+ return false ;
558+ }
559+
560+ /**
561+ * Extract field names and expected numeric types from a Prisma validation error message
562+ * @param {Error } error
563+ * @returns {Map<string, string> }
564+ */
565+ extractNumericFieldTypesFromError ( error ) {
566+ const message = error ?. message || '' ;
567+ const fieldTypeMap = new Map ( ) ;
568+ const argumentPattern = / A r g u m e n t \s + (?: [ " ' ` ] ) ? ( [ A - Z a - z 0 - 9 _ . \[ \] ] + ) (?: [ " ' ` ] ) ? \s * : \s * I n v a l i d v a l u e p r o v i d e d \. ? \s * E x p e c t e d \s + ( [ A - Z a - z 0 - 9 ] + ) [ ^ , ] * , \s * p r o v i d e d \s + S t r i n g / gi;
569+ let match ;
570+
571+ while ( ( match = argumentPattern . exec ( message ) ) !== null ) {
572+ const rawPath = match [ 1 ] ;
573+ const expectedType = ( match [ 2 ] || '' ) . toUpperCase ( ) ;
574+ const field = this . normalizeErrorFieldPath ( rawPath ) ;
575+
576+ if ( field && ! fieldTypeMap . has ( field ) ) {
577+ fieldTypeMap . set ( field , expectedType ) ;
578+ }
579+ }
580+
581+ return fieldTypeMap ;
582+ }
583+
584+ /**
585+ * Normalize a Prisma error argument path to the relevant field name
586+ * @param {string } rawPath
587+ * @returns {string|null }
588+ */
589+ normalizeErrorFieldPath ( rawPath ) {
590+ if ( ! rawPath || typeof rawPath !== 'string' ) {
591+ return null ;
592+ }
593+
594+ const segments = rawPath
595+ . replace ( / \[ \d + \] / g, '.' ) // Treat array indices as segment separators
596+ . split ( '.' )
597+ . map ( segment => segment . trim ( ) )
598+ . filter ( Boolean ) ;
599+
600+ if ( ! segments . length ) {
601+ return null ;
602+ }
603+
604+ return segments [ segments . length - 1 ] ;
605+ }
606+
607+ /**
608+ * Convert string representations of numeric values to their proper numeric types
609+ * @param {Object } upsertData
610+ * @param {Map<string, string> } fieldTypeMap
611+ * @returns {{data: Object, modified: boolean, convertedFields: string[]} }
612+ */
613+ convertNumericStringFields ( upsertData , fieldTypeMap ) {
614+ if ( ! upsertData || ! ( fieldTypeMap instanceof Map ) ) {
615+ return { data : upsertData , modified : false , convertedFields : [ ] } ;
616+ }
617+
618+ if ( ! fieldTypeMap . size ) {
619+ return { data : upsertData , modified : false , convertedFields : [ ] } ;
620+ }
621+
622+ const targetMap = new Map ( ) ;
623+ for ( const [ field , type ] of fieldTypeMap . entries ( ) ) {
624+ if ( typeof field === 'string' && field ) {
625+ targetMap . set ( field , ( type || '' ) . toUpperCase ( ) ) ;
626+ }
627+ }
628+
629+ if ( ! targetMap . size ) {
630+ return { data : upsertData , modified : false , convertedFields : [ ] } ;
631+ }
632+
633+ const convertedFields = new Set ( ) ;
634+ const convertValue = ( value ) => {
635+ if ( value === null || value === undefined ) {
636+ return value ;
637+ }
638+
639+ if ( value instanceof Date ) {
640+ return value ;
641+ }
642+
643+ if ( Prisma ?. Decimal && value instanceof Prisma . Decimal ) {
644+ return value ;
645+ }
646+
647+ if ( Array . isArray ( value ) ) {
648+ let changed = false ;
649+ const result = value . map ( item => {
650+ const converted = convertValue ( item ) ;
651+ if ( converted !== item ) {
652+ changed = true ;
653+ }
654+ return converted ;
655+ } ) ;
656+
657+ return changed ? result : value ;
658+ }
659+
660+ if ( typeof value === 'object' ) {
661+ let changed = false ;
662+ const result = { } ;
663+
664+ for ( const [ key , entry ] of Object . entries ( value ) ) {
665+ let newEntry = entry ;
666+ const expectedType = targetMap . get ( key ) ;
667+
668+ if ( expectedType && typeof entry === 'string' ) {
669+ const converted = this . tryConvertNumericString ( entry , expectedType ) ;
670+ if ( converted !== null ) {
671+ newEntry = converted ;
672+ changed = true ;
673+ convertedFields . add ( `${ key } (${ expectedType } )` ) ;
674+ }
675+ }
676+
677+ const nested = convertValue ( newEntry ) ;
678+ if ( nested !== newEntry ) {
679+ newEntry = nested ;
680+ changed = true ;
681+ }
682+
683+ result [ key ] = newEntry ;
684+ }
685+
686+ return changed ? result : value ;
687+ }
688+
689+ return value ;
690+ } ;
691+
692+ const transformedWhere = convertValue ( upsertData . where ) ;
693+ const transformedUpdate = convertValue ( upsertData . update ) ;
694+ const transformedCreate = convertValue ( upsertData . create ) ;
695+
696+ const modified =
697+ transformedWhere !== upsertData . where ||
698+ transformedUpdate !== upsertData . update ||
699+ transformedCreate !== upsertData . create ;
700+
701+ if ( ! modified ) {
702+ return { data : upsertData , modified : false , convertedFields : [ ] } ;
703+ }
704+
705+ return {
706+ data : {
707+ ...upsertData ,
708+ where : transformedWhere ,
709+ update : transformedUpdate ,
710+ create : transformedCreate
711+ } ,
712+ modified : true ,
713+ convertedFields : Array . from ( convertedFields )
714+ } ;
715+ }
716+
717+ /**
718+ * Attempt to convert a numeric-looking string into the proper numeric type
719+ * @param {string } value
720+ * @param {string } expectedType
721+ * @returns {number|BigInt|Prisma.Decimal|null }
722+ */
723+ tryConvertNumericString ( value , expectedType ) {
724+ if ( typeof value !== 'string' ) {
725+ return null ;
726+ }
727+
728+ const trimmed = value . trim ( ) ;
729+ if ( ! trimmed . length ) {
730+ return null ;
731+ }
732+
733+ const type = ( expectedType || '' ) . toUpperCase ( ) ;
734+ const integerPattern = / ^ [ + - ] ? \d + $ / ;
735+ const floatPattern = / ^ [ + - ] ? (?: \d + ( \. \d + ) ? | \. \d + ) (?: [ e E ] [ + - ] ? \d + ) ? $ / ;
736+
737+ if ( type === 'BIGINT' ) {
738+ if ( ! integerPattern . test ( trimmed ) ) {
739+ return null ;
740+ }
741+ try {
742+ return BigInt ( trimmed ) ;
743+ } catch ( err ) {
744+ return null ;
745+ }
746+ }
747+
748+ if ( type === 'INT' || type === 'SMALLINT' ) {
749+ if ( ! integerPattern . test ( trimmed ) ) {
750+ return null ;
751+ }
752+ const intValue = Number ( trimmed ) ;
753+ if ( ! Number . isFinite ( intValue ) || ! Number . isInteger ( intValue ) ) {
754+ return null ;
755+ }
756+ return intValue ;
757+ }
758+
759+ if ( type === 'FLOAT' || type === 'DOUBLE' || type === 'REAL' ) {
760+ if ( ! floatPattern . test ( trimmed ) ) {
761+ return null ;
762+ }
763+ const floatValue = Number ( trimmed ) ;
764+ return Number . isFinite ( floatValue ) ? floatValue : null ;
765+ }
766+
767+ if ( type === 'DECIMAL' || type === 'NUMERIC' ) {
768+ if ( ! floatPattern . test ( trimmed ) ) {
769+ return null ;
770+ }
771+ if ( Prisma ?. Decimal ) {
772+ try {
773+ return new Prisma . Decimal ( trimmed ) ;
774+ } catch ( err ) {
775+ // Fallback to native number if Decimal instantiation fails
776+ }
777+ }
778+ const decimalValue = Number ( trimmed ) ;
779+ return Number . isFinite ( decimalValue ) ? decimalValue : null ;
780+ }
781+
782+ if ( integerPattern . test ( trimmed ) ) {
783+ const intValue = Number ( trimmed ) ;
784+ if ( Number . isFinite ( intValue ) && Number . isInteger ( intValue ) ) {
785+ return intValue ;
786+ }
787+ }
788+
789+ if ( floatPattern . test ( trimmed ) ) {
790+ const floatValue = Number ( trimmed ) ;
791+ return Number . isFinite ( floatValue ) ? floatValue : null ;
792+ }
793+
794+ return null ;
795+ }
796+
525797 /**
526798 * Sanitize integer overflow values from the upsert payload
527799 * @param {Object } upsertData
0 commit comments