@@ -13,9 +13,12 @@ import {
1313 DataModelFactory ,
1414 EnumFactory ,
1515} from '@zenstackhq/language/factory' ;
16+ import { AstUtils , type Reference , type AstNode , type CstNode } from 'langium' ;
17+ import { lowerCaseFirst } from '@zenstackhq/common-helpers' ;
1618import type { PullOptions } from '../db' ;
1719import type { Cascade , IntrospectedEnum , IntrospectedTable , IntrospectionProvider } from './provider' ;
1820import { getAttributeRef , getDbName , getEnumRef } from './utils' ;
21+ import { resolveNameCasing } from './casing' ;
1922import { CliError } from '../../cli-error' ;
2023
2124export function syncEnums ( {
@@ -73,81 +76,22 @@ export function syncEnums({
7376 model . declarations . push ( factory . get ( { $container : model } ) ) ;
7477 }
7578 } else {
79+ // For providers that don't support native enums (e.g., SQLite), carry over
80+ // enum declarations from the existing schema as-is by deep-cloning the AST nodes.
81+ // A dummy buildReference is used since we don't need cross-reference resolution.
82+ const dummyBuildReference = ( _node : AstNode , _property : string , _refNode : CstNode | undefined , refText : string ) : Reference < AstNode > =>
83+ ( { $refText : refText } ) as Reference < AstNode > ;
84+
7685 oldModel . declarations
7786 . filter ( ( d ) => isEnum ( d ) )
7887 . forEach ( ( d ) => {
79- const factory = new EnumFactory ( ) . setName ( d . name ) ;
80- // Copy enum-level comments
81- if ( d . comments ?. length ) {
82- factory . update ( { comments : [ ...d . comments ] } ) ;
83- }
84- // Copy enum-level attributes (@@map, @@schema, etc.)
85- // Re-parent attributes to the new factory node
86- if ( d . attributes ?. length ) {
87- const reparentedAttrs = d . attributes . map ( ( attr ) => ( { ...attr , $container : factory . node } ) ) ;
88- factory . update ( { attributes : reparentedAttrs } ) ;
89- }
90- // Copy fields with their attributes and comments
91- d . fields . forEach ( ( v ) => {
92- factory . addField ( ( builder ) => {
93- builder . setName ( v . name ) ;
94- // Copy field-level comments
95- if ( v . comments ?. length ) {
96- v . comments . forEach ( ( c ) => {
97- builder . addComment ( c ) ;
98- } ) ;
99- }
100- // Copy field-level attributes (@map, etc.)
101- // Re-parent attributes to the new builder node
102- if ( v . attributes ?. length ) {
103- const reparentedAttrs = v . attributes . map ( ( attr ) => ( { ...attr , $container : builder . node } ) ) ;
104- builder . update ( { attributes : reparentedAttrs } ) ;
105- }
106- return builder ;
107- } ) ;
108- } ) ;
109- model . declarations . push ( factory . get ( { $container : model } ) ) ;
88+ const copy = AstUtils . copyAstNode ( d , dummyBuildReference ) ;
89+ ( copy as { $container : unknown } ) . $container = model ;
90+ model . declarations . push ( copy ) ;
11091 } ) ;
11192 }
11293}
11394
114- function resolveNameCasing ( casing : 'pascal' | 'camel' | 'snake' | 'none' , originalName : string ) {
115- let name = originalName ;
116- const fieldPrefix = / [ 0 - 9 ] / g. test ( name . charAt ( 0 ) ) ? '_' : '' ;
117-
118- switch ( casing ) {
119- case 'pascal' :
120- name = toPascalCase ( originalName ) ;
121- break ;
122- case 'camel' :
123- name = toCamelCase ( originalName ) ;
124- break ;
125- case 'snake' :
126- name = toSnakeCase ( originalName ) ;
127- break ;
128- }
129-
130- return {
131- modified : name !== originalName || fieldPrefix !== '' ,
132- name : `${ fieldPrefix } ${ name } ` ,
133- } ;
134- }
135-
136- function toPascalCase ( str : string ) : string {
137- return str . replace ( / [ _ \- ] + ( \w ) / g, ( _ , c ) => c . toUpperCase ( ) ) . replace ( / ^ \w / , ( c ) => c . toUpperCase ( ) ) ;
138- }
139-
140- function toCamelCase ( str : string ) : string {
141- return str . replace ( / [ _ \- ] + ( \w ) / g, ( _ , c ) => c . toUpperCase ( ) ) . replace ( / ^ \w / , ( c ) => c . toLowerCase ( ) ) ;
142- }
143-
144- function toSnakeCase ( str : string ) : string {
145- return str
146- . replace ( / [ - ] + / g, '_' )
147- . replace ( / ( [ a - z 0 - 9 ] ) ( [ A - Z ] ) / g, '$1_$2' )
148- . toLowerCase ( ) ;
149- }
150-
15195export type Relation = {
15296 schema : string ;
15397 table : string ;
@@ -185,24 +129,10 @@ export function syncTable({
185129 const modelIdAttribute = getAttributeRef ( '@@id' , services ) ;
186130 const uniqueAttribute = getAttributeRef ( '@unique' , services ) ;
187131 const modelUniqueAttribute = getAttributeRef ( '@@unique' , services ) ;
188- const relationAttribute = getAttributeRef ( '@relation' , services ) ;
189132 const fieldMapAttribute = getAttributeRef ( '@map' , services ) ;
190133 const tableMapAttribute = getAttributeRef ( '@@map' , services ) ;
191134 const modelindexAttribute = getAttributeRef ( '@@index' , services ) ;
192135
193- if (
194- ! idAttribute ||
195- ! uniqueAttribute ||
196- ! relationAttribute ||
197- ! fieldMapAttribute ||
198- ! tableMapAttribute ||
199- ! modelIdAttribute ||
200- ! modelUniqueAttribute ||
201- ! modelindexAttribute
202- ) {
203- throw new CliError ( 'Cannot find required attributes in the model.' ) ;
204- }
205-
206136 const relations : Relation [ ] = [ ] ;
207137 const { name, modified } = resolveNameCasing ( options . modelCasing , table . name ) ;
208138 const multiPk = table . columns . filter ( ( c ) => c . pk ) . length > 1 ;
@@ -483,19 +413,25 @@ export function syncRelation({
483413
484414 const relationName = `${ relation . table } ${ similarRelations > 0 ? `_${ relation . column } ` : '' } To${ relation . references . table } ` ;
485415
486- const sourceNameFromReference = sourceField . name . toLowerCase ( ) . endsWith ( 'id' ) ? `${ resolveNameCasing ( "camel" , sourceField . name . slice ( 0 , - 2 ) ) . name } ${ relation . type === 'many' ? 's' : '' } ` : undefined ;
416+ // Derive a relation field name from the FK scalar field: if the field ends with "Id",
417+ // strip the suffix and use the remainder (e.g., "authorId" -> "author").
418+ const sourceNameFromReference = sourceField . name . toLowerCase ( ) . endsWith ( 'id' ) ? `${ resolveNameCasing ( options . fieldCasing , sourceField . name . slice ( 0 , - 2 ) ) . name } ${ relation . type === 'many' ? 's' : '' } ` : undefined ;
487419
420+ // Check if the derived name would clash with an existing field
488421 const sourceFieldFromReference = sourceModel . fields . find ( ( f ) => f . name === sourceNameFromReference ) ;
489422
423+ // Determine the relation field name:
424+ // - For ambiguous relations (multiple FKs to the same table), include the source column for disambiguation.
425+ // - Otherwise, prefer the name derived from the FK field (if no clash), falling back to the target model name.
490426 let { name : sourceFieldName } = resolveNameCasing (
491427 options . fieldCasing ,
492428 similarRelations > 0
493- ? `${ fieldPrefix } ${ sourceModel . name . charAt ( 0 ) . toLowerCase ( ) } ${ sourceModel . name . slice ( 1 ) } _${ relation . column } `
494- : `${ ( ! sourceFieldFromReference ? sourceNameFromReference : undefined ) || resolveNameCasing ( "camel" , targetModel . name ) . name } ${ relation . type === 'many' ? 's' : '' } ` ,
429+ ? `${ fieldPrefix } ${ lowerCaseFirst ( sourceModel . name ) } _${ relation . column } `
430+ : `${ ( ! sourceFieldFromReference ? sourceNameFromReference : undefined ) || lowerCaseFirst ( resolveNameCasing ( options . fieldCasing , targetModel . name ) . name ) } ${ relation . type === 'many' ? 's' : '' } ` ,
495431 ) ;
496432
497433 if ( sourceModel . fields . find ( ( f ) => f . name === sourceFieldName ) ) {
498- sourceFieldName = `${ sourceFieldName } To${ targetModel . name . charAt ( 0 ) . toLowerCase ( ) } ${ targetModel . name . slice ( 1 ) } _${ relation . references . column } ` ;
434+ sourceFieldName = `${ sourceFieldName } To${ lowerCaseFirst ( targetModel . name ) } _${ relation . references . column } ` ;
499435 }
500436
501437 const sourceFieldFactory = new DataFieldFactory ( )
@@ -549,8 +485,8 @@ export function syncRelation({
549485 const { name : oppositeFieldName } = resolveNameCasing (
550486 options . fieldCasing ,
551487 similarRelations > 0
552- ? `${ oppositeFieldPrefix } ${ sourceModel . name . charAt ( 0 ) . toLowerCase ( ) } ${ sourceModel . name . slice ( 1 ) } _${ relation . column } `
553- : `${ resolveNameCasing ( "camel" , sourceModel . name ) . name } ${ relation . references . type === 'many' ? 's' : '' } ` ,
488+ ? `${ oppositeFieldPrefix } ${ lowerCaseFirst ( sourceModel . name ) } _${ relation . column } `
489+ : `${ lowerCaseFirst ( resolveNameCasing ( options . fieldCasing , sourceModel . name ) . name ) } ${ relation . references . type === 'many' ? 's' : '' } ` ,
554490 ) ;
555491
556492 const targetFieldFactory = new DataFieldFactory ( )
0 commit comments