@@ -71,8 +71,12 @@ def __new__(mcs, name: str, bases: tuple[type, ...], attrs: dict[str, Any]) -> "
7171 if isinstance (base , ModelMeta ):
7272 kls .model_fields .update (base .model_fields )
7373
74- # Fill model fields from itself
75- kls .model_fields .update ({field_name : field_obj for field_name , field_obj in attrs .items () if isinstance (field_obj , BaseField )})
74+ # Fill model fields from itself. Each field is "late bound" to its
75+ # declared attribute name here: the field object had no name when it
76+ # was constructed, so we hand it the name at class creation time
77+ for field_name , field_obj in attrs .items ():
78+ if isinstance (field_obj , BaseField ):
79+ kls .model_fields [field_name ] = field_obj .bind (field_name )
7680
7781 # Register a real table (a table with valid `model_name`) to
7882 # the metaclass `table` registry. After all the tables are
@@ -99,19 +103,77 @@ class ModelTable:
99103 def __init__ (self , table_name : str , table_fields : dict [str , "BaseField" ]) -> None :
100104 self .table_name = table_name
101105 self .table_fields = table_fields
106+ self .primary_key = next (
107+ (field_name for field_name , field in table_fields .items () if field .primary_key ),
108+ None ,
109+ )
110+
111+ def ddl (self ) -> str :
112+ """Build a simple CREATE TABLE statement for the schema."""
113+ columns = []
114+ for field_name , field in self .table_fields .items ():
115+ sql = field .column_definition (field_name )
116+ if field .primary_key :
117+ sql = f"{ sql } PRIMARY KEY"
118+ if field .default is not None and not field .primary_key :
119+ sql = f"{ sql } DEFAULT { field .default !r} "
120+ if not field .nullable and not field .primary_key :
121+ sql = f"{ sql } NOT NULL"
122+ columns .append (sql )
123+ return f"CREATE TABLE { self .table_name } ({ ', ' .join (columns )} );"
102124
103125
104126class BaseField (ABC ):
105- """Base field."""
127+ """Base field.
128+
129+ A field carries its declared attribute name in `name`. It does not
130+ know that name until the metaclass calls `bind` at class creation
131+ time, which is the classic "late binding" metaclass trick.
132+ """
133+
134+ name : str | None
135+ primary_key : bool
136+ nullable : bool
137+ default : Any
138+
139+ def __init__ (self , * , primary_key : bool = False , nullable : bool = True , default : Any = None ) -> None :
140+ self .name = None
141+ self .primary_key = primary_key
142+ self .nullable = nullable
143+ self .default = default
144+
145+ def bind (self , name : str ) -> "BaseField" :
146+ """Bind this field to its declared attribute name at runtime."""
147+ self .name = name
148+ return self
149+
150+ def column_definition (self , field_name : str ) -> str :
151+ """Return the column SQL definition for this type."""
152+ raise NotImplementedError
106153
107154
108155class CharField (BaseField ):
109156 """Character field."""
110157
158+ max_length : int
159+
160+ def __init__ (self , * , max_length : int = 255 , primary_key : bool = False , nullable : bool = True , default : Any = None ) -> None :
161+ super ().__init__ (primary_key = primary_key , nullable = nullable , default = default )
162+ self .max_length = max_length
163+
164+ def column_definition (self , field_name : str ) -> str :
165+ return f"{ field_name } VARCHAR({ self .max_length } )"
166+
111167
112168class IntegerField (BaseField ):
113169 """Integer field."""
114170
171+ def __init__ (self , * , primary_key : bool = False , nullable : bool = True , default : Any = None ) -> None :
172+ super ().__init__ (primary_key = primary_key , nullable = nullable , default = default )
173+
174+ def column_definition (self , field_name : str ) -> str :
175+ return f"{ field_name } INTEGER"
176+
115177
116178class BaseModel (metaclass = ModelMeta ):
117179 """Base model.
@@ -125,15 +187,15 @@ class BaseModel(metaclass=ModelMeta):
125187 """
126188
127189 __abstract__ = True # This is NOT a real table
128- row_id = IntegerField ()
190+ row_id = IntegerField (primary_key = True )
129191
130192
131193class UserModel (BaseModel ):
132194 """User model."""
133195
134196 __table_name__ = "user_rocks" # This is a custom table name
135197 username = CharField ()
136- password = CharField ()
198+ password = CharField (nullable = False , default = "guest" )
137199 age = CharField ()
138200 sex = CharField ()
139201
@@ -158,14 +220,49 @@ def main() -> None:
158220 assert "username" in UserModel .model_fields
159221 assert "address" in AddressModel .model_fields
160222
161- # Real models are registered at runtime with `ModelMeta`
162- assert UserModel .is_registered
163- assert AddressModel .is_registered
223+ # Each field is late-bound to its declared attribute name at runtime
224+ assert UserModel .model_fields ["username" ].name == "username"
225+ assert UserModel .model_fields ["password" ].name == "password"
226+ assert AddressModel .model_fields ["state" ].name == "state"
227+
228+ # Inherited fields keep the name they were bound with in the base class
229+ assert UserModel .model_fields ["row_id" ].name == "row_id"
230+ assert AddressModel .model_fields ["row_id" ].name == "row_id"
231+
232+ # Primary keys are tracked on the field and the generated table metadata
233+ assert UserModel .model_fields ["row_id" ].primary_key is True
234+ assert AddressModel .model_fields ["row_id" ].primary_key is True
235+ assert UserModel .model_table is not None
236+ assert AddressModel .model_table is not None
237+ assert UserModel .model_table .primary_key == "row_id"
238+ assert AddressModel .model_table .primary_key == "row_id"
239+
240+ # A field built by hand and not yet bound has no name yet
241+ assert IntegerField ().name is None
242+
243+ # Char fields can carry a max length, which is used in generated SQL
244+ username_field = UserModel .model_fields ["username" ]
245+ address_field = AddressModel .model_fields ["address" ]
246+ assert isinstance (username_field , CharField )
247+ assert isinstance (address_field , CharField )
248+ assert username_field .max_length == 255
249+ assert address_field .max_length == 255
164250
165251 # Real models have a `ModelTable` that can be used for DB setup
166252 assert isinstance (ModelMeta .tables [UserModel .model_name ], ModelTable )
167253 assert isinstance (ModelMeta .tables [AddressModel .model_name ], ModelTable )
168254
255+ # A table can generate a simple CREATE TABLE statement from its fields
256+ assert UserModel .model_table is not None
257+ assert AddressModel .model_table is not None
258+ assert UserModel .model_table .ddl () == (
259+ "CREATE TABLE user_rocks "
260+ "(row_id INTEGER PRIMARY KEY, username VARCHAR(255), password VARCHAR(255) DEFAULT 'guest' NOT NULL, age VARCHAR(255), sex VARCHAR(255));"
261+ )
262+ assert AddressModel .model_table .ddl () == (
263+ "CREATE TABLE address (row_id INTEGER PRIMARY KEY, user_id INTEGER, address VARCHAR(255), state VARCHAR(255), zip_code VARCHAR(255));"
264+ )
265+
169266 # Base model is given special treatment at runtime
170267 assert not BaseModel .is_registered
171268 assert BaseModel .model_name is None
0 commit comments