@@ -49,24 +49,41 @@ public static class PostgresSchema
4949
5050 private static string Col ( MetaField f ) => f . DbColumn ?? f . Name ;
5151
52- /// <summary>CREATE TABLE for a writable entity (columns + PK + NOT NULL + UNIQUE).</summary>
53- public static string CreateTable ( MetaObject entity )
52+ /// <summary>
53+ /// CREATE TABLE for a writable entity: scalar columns + object-field storage
54+ /// (@storage flattened → prefixed columns; else a single jsonb column) + PK +
55+ /// NOT NULL + UNIQUE indexes + FOREIGN KEY constraints (from enforced
56+ /// identity.reference children).
57+ /// </summary>
58+ public static string CreateTable ( MetaObject entity , MetaRoot root )
5459 {
5560 var table = entity . DbTable ?? entity . Name ;
5661 var pk = entity . PrimaryIdentity ( ) ;
5762 var pkCols = ( pk ? . Fields ?? [ ] ) . ToHashSet ( StringComparer . Ordinal ) ;
5863
5964 var lines = new List < string > ( ) ;
60- foreach ( var f in entity . Fields ( ) . Where ( f => CSharpNaming . ScalarFor ( f . SubType ) is not null ) )
65+ foreach ( var f in entity . Fields ( ) )
6166 {
62- var notNull = CSharpNaming . IsRequired ( entity , f ) ? " NOT NULL" : string . Empty ;
63- lines . Add ( $ " { Col ( f ) } { PgType ( f ) } { notNull } ") ;
67+ if ( CSharpNaming . ScalarFor ( f . SubType ) is not null )
68+ {
69+ var notNull = CSharpNaming . IsRequired ( entity , f ) ? " NOT NULL" : string . Empty ;
70+ lines . Add ( $ " { Col ( f ) } { PgType ( f ) } { notNull } ") ;
71+ }
72+ else if ( f . SubType == FIELD_SUBTYPE_OBJECT )
73+ {
74+ lines . AddRange ( ObjectFieldColumns ( entity , f , root ) ) ;
75+ }
6476 }
6577 if ( pk is not null && pk . Fields . Count > 0 )
6678 {
6779 var cols = entity . Fields ( ) . Where ( f => pkCols . Contains ( f . Name ) ) . Select ( Col ) ;
6880 lines . Add ( $ " PRIMARY KEY ({ string . Join ( ", " , cols ) } )") ;
6981 }
82+ // FOREIGN KEY constraints from enforced identity.reference children.
83+ foreach ( var fk in entity . ReferenceIdentities ( ) . Where ( r => r . Enforce ) )
84+ {
85+ if ( ForeignKeyClause ( entity , fk , root ) is { } clause ) lines . Add ( clause ) ;
86+ }
7087
7188 var sb = new StringBuilder ( ) ;
7289 sb . AppendLine ( $ "CREATE TABLE { table } (") ;
@@ -82,6 +99,55 @@ public static string CreateTable(MetaObject entity)
8299 return sb . ToString ( ) ;
83100 }
84101
102+ // Columns for an object-typed field. @storage flattened expands each nested
103+ // scalar field as "{parentCol}_{nestedCol}" (the parent emits no column of its
104+ // own); every other storage (jsonb / subdocument / absent) collapses to one
105+ // jsonb column — a relational target can't materialize a document store inline.
106+ private static IEnumerable < string > ObjectFieldColumns ( MetaObject entity , MetaField f , MetaRoot root )
107+ {
108+ if ( f . Storage == STORAGE_FLATTENED && f . ObjectRef is { } oref &&
109+ root . FindObject ( StripPkg ( oref ) ) is { } nested )
110+ {
111+ var prefix = Col ( f ) + "_" ;
112+ foreach ( var nf in nested . Fields ( ) . Where ( n => CSharpNaming . ScalarFor ( n . SubType ) is not null ) )
113+ {
114+ var notNull = CSharpNaming . IsRequired ( nested , nf ) ? " NOT NULL" : string . Empty ;
115+ yield return $ " { prefix } { Col ( nf ) } { PgType ( nf ) } { notNull } ";
116+ }
117+ yield break ;
118+ }
119+ var topNotNull = CSharpNaming . IsRequired ( entity , f ) ? " NOT NULL" : string . Empty ;
120+ yield return $ " { Col ( f ) } jsonb{ topNotNull } ";
121+ }
122+
123+ // A table-level FOREIGN KEY constraint for an enforced reference identity.
124+ // FK columns keep the reference's declared @fields order; target columns are
125+ // the explicit dotted @references fields (multi-col) or the target's primary
126+ // identity (single-col / bare form), falling back to "id".
127+ private static string ? ForeignKeyClause ( MetaObject entity , MetaReferenceIdentity fk , MetaRoot root )
128+ {
129+ if ( fk . TargetEntity is not { } targetName || fk . Fields . Count == 0 ) return null ;
130+ var target = root . FindObject ( StripPkg ( targetName ) ) ;
131+ if ( target is null ) return null ;
132+
133+ var fkCols = fk . Fields . Select ( js => ResolveColumn ( entity , js ) ) . ToList ( ) ;
134+ var refCols = fk . TargetFields . Count > 1
135+ ? fk . TargetFields . Select ( tf => ResolveColumn ( target , tf ) ) . ToList ( )
136+ : [ ResolveColumn ( target , ResolvedTargetPkField ( target , fk ) ) ] ;
137+
138+ var name = $ "{ entity . DbTable ?? entity . Name } _{ fkCols [ 0 ] } _fk";
139+ var refTable = target . DbTable ?? target . Name ;
140+ return $ " CONSTRAINT { name } FOREIGN KEY ({ string . Join ( ", " , fkCols ) } ) " +
141+ $ "REFERENCES { refTable } ({ string . Join ( ", " , refCols ) } )";
142+ }
143+
144+ // The single target column a reference resolves to: the lone explicit @references
145+ // field, else the target's primary-identity first field, else "id".
146+ private static string ResolvedTargetPkField ( MetaObject target , MetaReferenceIdentity fk ) =>
147+ fk . TargetFields . Count == 1
148+ ? fk . TargetFields [ 0 ]
149+ : target . PrimaryIdentity ( ) ? . Fields . FirstOrDefault ( ) ?? "id" ;
150+
85151 /// <summary>
86152 /// CREATE VIEW for a projection. Derives the SELECT from field origins:
87153 /// passthrough (single base) → a plain column; aggregate (@agg/@of/@via) → a
@@ -242,7 +308,7 @@ public static string BuildSchema(MetaRoot root, Action<string>? warn = null)
242308 foreach ( var e in root . Objects ( ) . Where ( o => o . IsEntity ( ) && ! o . IsReadOnlyProjection ( ) )
243309 . OrderBy ( o => o . Name , StringComparer . Ordinal ) )
244310 {
245- sb . AppendLine ( CreateTable ( e ) ) ;
311+ sb . AppendLine ( CreateTable ( e , root ) ) ;
246312 }
247313 foreach ( var p in root . Objects ( ) . Where ( o => o . IsReadOnlyProjection ( ) )
248314 . OrderBy ( o => o . Name , StringComparer . Ordinal ) )
0 commit comments