@@ -160,35 +160,27 @@ export class MigrationGenerator {
160160 ) ;
161161
162162 // Update fields
163- const existingFields = model . fields . filter ( ( field ) => {
163+ const rawExistingFields = model . fields . filter ( ( field ) => {
164+ if ( ! field . generateAs ) {
165+ return false ;
166+ }
167+
164168 const col = this . getColumn ( model . name , field . kind === 'relation' ? `${ field . name } Id` : field . name ) ;
165169 if ( ! col ) {
166170 return false ;
167171 }
168172
169- if ( ( ! field . nonNull && ! col . is_nullable ) || ( field . nonNull && col . is_nullable ) ) {
173+ if ( col . generation_expression !== field . generateAs ) {
170174 return true ;
171175 }
172176
173- if ( ! field . kind || field . kind === 'primitive' ) {
174- if ( field . type === 'Int' ) {
175- if ( col . data_type !== 'integer' ) {
176- return true ;
177- }
178- }
179- if ( field . type === 'Float' ) {
180- if ( field . double ) {
181- if ( col . data_type !== 'double precision' ) {
182- return true ;
183- }
184- } else if ( col . data_type !== 'numeric' ) {
185- return true ;
186- }
187- }
188- }
189-
190- return false ;
177+ return this . hasChanged ( model , field ) ;
191178 } ) ;
179+ if ( rawExistingFields . length ) {
180+ this . updateFieldsRaw ( model , rawExistingFields , up , down ) ;
181+ }
182+
183+ const existingFields = model . fields . filter ( ( field ) => ! field . generateAs && this . hasChanged ( model , field ) ) ;
192184 this . updateFields ( model , existingFields , up , down ) ;
193185 }
194186
@@ -375,6 +367,10 @@ export class MigrationGenerator {
375367 for ( const field of fields ) {
376368 alter . push ( ( ) => this . column ( field , { setNonNull : field . defaultValue !== undefined } ) ) ;
377369
370+ if ( field . generateAs ) {
371+ continue ;
372+ }
373+
378374 // If the field is not nullable but has no default, write placeholder code
379375 if ( field . nonNull && field . defaultValue === undefined ) {
380376 updates . push ( ( ) => this . writer . write ( `${ field . name } : 'TODO',` ) . newLine ( ) ) ;
@@ -405,13 +401,63 @@ export class MigrationGenerator {
405401
406402 down . push ( ( ) => {
407403 this . alterTable ( model . name , ( ) => {
408- for ( const { kind, name } of fields ) {
404+ for ( const { kind, name } of fields . toReversed ( ) ) {
409405 this . dropColumn ( kind === 'relation' ? `${ name } Id` : name ) ;
410406 }
411407 } ) ;
412408 } ) ;
413409 }
414410
411+ private updateFieldsRaw ( model : EntityModel , fields : EntityField [ ] , up : Callbacks , down : Callbacks ) {
412+ if ( ! fields . length ) {
413+ return ;
414+ }
415+
416+ up . push ( ( ) => {
417+ this . alterTableRaw ( model . name , ( ) => {
418+ for ( const [ index , field ] of fields . entries ( ) ) {
419+ this . columnRaw ( field , { alter : true } , index ) ;
420+ }
421+ } ) ;
422+ } ) ;
423+
424+ down . push ( ( ) => {
425+ this . alterTableRaw ( model . name , ( ) => {
426+ for ( const [ index , field ] of fields . entries ( ) ) {
427+ this . columnRaw ( field , { alter : true } , index ) ;
428+ }
429+ } ) ;
430+ } ) ;
431+
432+ if ( isUpdatableModel ( model ) ) {
433+ const updatableFields = fields . filter ( isUpdatableField ) ;
434+ if ( ! updatableFields . length ) {
435+ return ;
436+ }
437+
438+ up . push ( ( ) => {
439+ this . alterTable ( `${ model . name } Revision` , ( ) => {
440+ for ( const [ index , field ] of updatableFields . entries ( ) ) {
441+ this . columnRaw ( field , { alter : true } , index ) ;
442+ }
443+ } ) ;
444+ } ) ;
445+
446+ down . push ( ( ) => {
447+ this . alterTable ( `${ model . name } Revision` , ( ) => {
448+ for ( const [ index , field ] of updatableFields . entries ( ) ) {
449+ this . columnRaw (
450+ field ,
451+ { alter : true } ,
452+ index ,
453+ summonByName ( this . columns [ model . name ] , field . kind === 'relation' ? `${ field . name } Id` : field . name ) ,
454+ ) ;
455+ }
456+ } ) ;
457+ } ) ;
458+ }
459+ }
460+
415461 private updateFields ( model : EntityModel , fields : EntityField [ ] , up : Callbacks , down : Callbacks ) {
416462 if ( ! fields . length ) {
417463 return ;
@@ -572,6 +618,12 @@ export class MigrationGenerator {
572618 . blankLine ( ) ;
573619 }
574620
621+ private alterTableRaw ( table : string , block : ( ) => void ) {
622+ this . writer . write ( `await knex.raw('ALTER TABLE "${ table } "` ) ;
623+ block ( ) ;
624+ this . writer . write ( `');` ) . newLine ( ) . blankLine ( ) ;
625+ }
626+
575627 private alterTable ( table : string , block : ( ) => void ) {
576628 return this . writer
577629 . write ( `await knex.schema.alterTable('${ table } ', (table) => ` )
@@ -605,29 +657,125 @@ export class MigrationGenerator {
605657 return value ;
606658 }
607659
660+ private columnRaw (
661+ { name, ...field } : EntityField ,
662+ { setNonNull = true , alter = false } = { } ,
663+ index : number ,
664+ toColumn ?: Column ,
665+ ) {
666+ const nonNull = ( ) => {
667+ if ( setNonNull ) {
668+ if ( toColumn ) {
669+ if ( toColumn . is_nullable ) {
670+ return false ;
671+ }
672+
673+ return true ;
674+ }
675+ if ( field . nonNull ) {
676+ return true ;
677+ }
678+
679+ return false ;
680+ }
681+ } ;
682+ const kind = field . kind ;
683+ if ( field . generateAs ) {
684+ let type = '' ;
685+ switch ( kind ) {
686+ case undefined :
687+ case 'primitive' :
688+ switch ( field . type ) {
689+ case 'Float' :
690+ type = `decimal(${ field . precision ?? 'undefined' } , ${ field . scale ?? 'undefined' } )` ;
691+ break ;
692+ default :
693+ throw new Error ( `Generated columns of kind ${ kind } and type ${ field . type } are not supported yet.` ) ;
694+ }
695+ break ;
696+ default :
697+ throw new Error ( `Generated columns of kind ${ kind } are not supported yet.` ) ;
698+ }
699+ if ( index ) {
700+ this . writer . write ( `,` ) ;
701+ }
702+ if ( alter ) {
703+ this . writer . write ( ` ALTER COLUMN "${ name } " TYPE ${ type } ` ) ;
704+ if ( setNonNull ) {
705+ if ( nonNull ( ) ) {
706+ this . writer . write ( `, ALTER COLUMN "${ name } " SET NOT NULL` ) ;
707+ } else {
708+ this . writer . write ( `, ALTER COLUMN "${ name } " DROP NOT NULL` ) ;
709+ }
710+ }
711+ this . writer . write ( `, ALTER COLUMN "${ name } " SET EXPRESSION AS (${ field . generateAs } )` ) ;
712+ } else {
713+ this . writer . write (
714+ `${ alter ? 'ALTER' : 'ADD' } COLUMN "${ name } " ${ type } ${ nonNull ( ) ? ' not null' : '' } GENERATED ALWAYS AS (${ field . generateAs } ) STORED` ,
715+ ) ;
716+ }
717+
718+ return ;
719+ }
720+
721+ throw new Error ( `Only generated columns can be created with columnRaw` ) ;
722+ }
723+
608724 private column (
609725 { name, primary, list, ...field } : EntityField ,
610726 { setUnique = true , setNonNull = true , alter = false , foreign = true , setDefault = true } = { } ,
611727 toColumn ?: Column ,
612728 ) {
613- const col = ( what ?: string ) => {
614- if ( what ) {
615- this . writer . write ( what ) ;
616- }
729+ const nonNull = ( ) => {
617730 if ( setNonNull ) {
618731 if ( toColumn ) {
619732 if ( toColumn . is_nullable ) {
620- this . writer . write ( `.nullable()` ) ;
621- } else {
622- this . writer . write ( '.notNullable()' ) ;
623- }
624- } else {
625- if ( field . nonNull ) {
626- this . writer . write ( `.notNullable()` ) ;
627- } else {
628- this . writer . write ( '.nullable()' ) ;
733+ return false ;
629734 }
735+
736+ return true ;
630737 }
738+ if ( field . nonNull ) {
739+ return true ;
740+ }
741+
742+ return false ;
743+ }
744+ } ;
745+ const kind = field . kind ;
746+ if ( field . generateAs ) {
747+ let type = '' ;
748+ switch ( kind ) {
749+ case undefined :
750+ case 'primitive' :
751+ switch ( field . type ) {
752+ case 'Float' :
753+ type = `decimal(${ field . precision ?? 'undefined' } , ${ field . scale ?? 'undefined' } )` ;
754+ break ;
755+ default :
756+ throw new Error ( `Generated columns of kind ${ kind } and type ${ field . type } are not supported yet.` ) ;
757+ }
758+ break ;
759+ default :
760+ throw new Error ( `Generated columns of kind ${ kind } are not supported yet.` ) ;
761+ }
762+ this . writer . write (
763+ `table.specificType('${ name } ', '${ type } ${ nonNull ( ) ? ' not null' : '' } GENERATED ALWAYS AS (${ field . generateAs } ) STORED')` ,
764+ ) ;
765+ if ( alter ) {
766+ this . writer . write ( '.alter()' ) ;
767+ }
768+ this . writer . write ( ';' ) . newLine ( ) ;
769+
770+ return ;
771+ }
772+
773+ const col = ( what ?: string ) => {
774+ if ( what ) {
775+ this . writer . write ( what ) ;
776+ }
777+ if ( setNonNull ) {
778+ this . writer . write ( nonNull ( ) ? '.notNullable()' : '.nullable()' ) ;
631779 }
632780 if ( setDefault && field . defaultValue !== undefined ) {
633781 this . writer . write ( `.defaultTo(${ this . value ( field . defaultValue ) } )` ) ;
@@ -642,7 +790,6 @@ export class MigrationGenerator {
642790 }
643791 this . writer . write ( ';' ) . newLine ( ) ;
644792 } ;
645- const kind = field . kind ;
646793 switch ( kind ) {
647794 case undefined :
648795 case 'primitive' :
@@ -716,6 +863,44 @@ export class MigrationGenerator {
716863 private getColumn ( tableName : string , columnName : string ) {
717864 return this . columns [ tableName ] . find ( ( col ) => col . name === columnName ) ;
718865 }
866+
867+ private hasChanged ( model : EntityModel , field : EntityField ) {
868+ const col = this . getColumn ( model . name , field . kind === 'relation' ? `${ field . name } Id` : field . name ) ;
869+ if ( ! col ) {
870+ return false ;
871+ }
872+
873+ if ( field . generateAs ) {
874+ if ( col . generation_expression !== field . generateAs ) {
875+ throw new Error (
876+ `Column ${ col . name } has specific type ${ col . generation_expression } but expected ${ field . generateAs } ` ,
877+ ) ;
878+ }
879+ }
880+
881+ if ( ( ! field . nonNull && ! col . is_nullable ) || ( field . nonNull && col . is_nullable ) ) {
882+ return true ;
883+ }
884+
885+ if ( ! field . kind || field . kind === 'primitive' ) {
886+ if ( field . type === 'Int' ) {
887+ if ( col . data_type !== 'integer' ) {
888+ return true ;
889+ }
890+ }
891+ if ( field . type === 'Float' ) {
892+ if ( field . double ) {
893+ if ( col . data_type !== 'double precision' ) {
894+ return true ;
895+ }
896+ } else if ( col . data_type !== 'numeric' ) {
897+ return true ;
898+ }
899+ }
900+ }
901+
902+ return false ;
903+ }
719904}
720905
721906export const getMigrationDate = ( ) => {
0 commit comments