2323
2424# This is a minimal object/database wrapper for ducktools.classbuilder
2525# Execute the class to see examples of the methods that will be generated
26+ # There are a lot of features that would be needed for a *general* version of this
27+ # This only implements the required features for ducktools-env's use case
2628
2729import itertools
2830
@@ -132,7 +134,7 @@ class SQLMeta(SlotMakerMeta):
132134 TABLE_NAME : str
133135 VALID_FIELDS : dict [str , SQLAttribute ]
134136 COMPUTED_FIELDS : set [str ]
135- PRIMARY_KEY : str
137+ PK_NAME : str
136138 STR_LIST_COLUMNS : set [str ]
137139 BOOL_COLUMNS : set [str ]
138140
@@ -181,20 +183,25 @@ def __init_subclass__(
181183 primary_key = None
182184 for name , field in fields .items ():
183185 if field .primary_key :
186+ if primary_key is not None :
187+ raise AttributeError ("sqlclass *must* have **only** one primary key" )
184188 primary_key = name
185- break
186189
187190 if primary_key is None :
188191 raise AttributeError ("sqlclass *must* have one primary key" )
189192
190- if sum (1 for f in fields .values () if f .primary_key ) > 1 :
191- raise AttributeError ("sqlclass *must* have **only** one primary key" )
192-
193- cls .PRIMARY_KEY = primary_key
193+ cls .PK_NAME = primary_key
194194 cls .TABLE_NAME = caps_to_snake (cls .__name__ )
195195
196196 super ().__init_subclass__ (** kwargs )
197197
198+ @property
199+ def primary_key (self ):
200+ """
201+ Get the actual value of the primary key on an instance.
202+ """
203+ return getattr (self , self .PK_NAME )
204+
198205 @classmethod
199206 def create_table (cls , con ):
200207 sql_field_list = []
@@ -256,7 +263,7 @@ def _select_query(cls, cursor, filters: dict[str, MAPPED_TYPES] | None = None):
256263 search_condition = ""
257264
258265 cursor .row_factory = cls .row_factory
259- result = cursor .execute (f"SELECT * FROM { cls .TABLE_NAME } { search_condition } " , filters )
266+ result = cursor .execute (f"SELECT * FROM { cls .TABLE_NAME } { search_condition } " , filters )
260267 return result
261268
262269 @classmethod
@@ -302,7 +309,7 @@ def select_like(cls, con, filters: dict[str, MAPPED_TYPES] | None = None):
302309 try :
303310 cursor .row_factory = cls .row_factory
304311 result = cursor .execute (
305- f"SELECT * FROM { cls .TABLE_NAME } { search_condition } " ,
312+ f"SELECT * FROM { cls .TABLE_NAME } { search_condition } " ,
306313 filters
307314 )
308315 rows = result .fetchall ()
@@ -313,13 +320,13 @@ def select_like(cls, con, filters: dict[str, MAPPED_TYPES] | None = None):
313320
314321 @classmethod
315322 def max_pk (cls , con ):
316- statement = f"SELECT MAX({ cls .PRIMARY_KEY } ) from { cls .TABLE_NAME } "
323+ statement = f"SELECT MAX({ cls .PK_NAME } ) FROM { cls .TABLE_NAME } "
317324 result = con .execute (statement )
318325 return result .fetchone ()[0 ]
319326
320327 @classmethod
321328 def row_from_pk (cls , con , pk_value ):
322- return cls .select_row (con , filters = {cls .PRIMARY_KEY : pk_value })
329+ return cls .select_row (con , filters = {cls .PK_NAME : pk_value })
323330
324331 def insert_row (self , con ):
325332 columns = ", " .join (
@@ -338,16 +345,22 @@ def insert_row(self, con):
338345 with con :
339346 result = con .execute (sql_statement , processed_values )
340347
341- if getattr (self , self .PRIMARY_KEY ) is None :
342- setattr (self , self .PRIMARY_KEY , result .lastrowid )
348+ if getattr (self , self .PK_NAME ) is None :
349+ setattr (self , self .PK_NAME , result .lastrowid )
343350
344351 if self .COMPUTED_FIELDS :
345352 row = self .row_from_pk (con , result .lastrowid )
346353 for field in self .COMPUTED_FIELDS :
347354 setattr (self , field , getattr (row , field ))
348355
349356 def update_row (self , con , columns : list [str ]):
350- if self .PRIMARY_KEY is None :
357+ """
358+ Update the values in the database for this 'row'
359+
360+ :param con: SQLContext
361+ :param columns: list of the columns to update from this class.
362+ """
363+ if self .primary_key is None :
351364 raise AttributeError ("Primary key has not yet been set" )
352365
353366 if invalid_columns := (set (columns ) - self .VALID_FIELDS .keys ()):
@@ -360,22 +373,28 @@ def update_row(self, con, columns: list[str]):
360373 }
361374
362375 set_columns = ", " .join (f"{ name } = :{ name } " for name in columns )
363- search_condition = f"{ self .PRIMARY_KEY } = :{ self .PRIMARY_KEY } "
376+ search_condition = f"{ self .PK_NAME } = :{ self .PK_NAME } "
364377
365378 with con :
366- con .execute (
379+ result = con .execute (
367380 f"UPDATE { self .TABLE_NAME } SET { set_columns } WHERE { search_condition } " ,
368381 processed_values ,
369382 )
370383
384+ # Computed rows may need to be updated
385+ if self .COMPUTED_FIELDS :
386+ row = self .row_from_pk (con , self .primary_key )
387+ for field in self .COMPUTED_FIELDS :
388+ setattr (self , field , getattr (row , field ))
389+
371390 def delete_row (self , con ):
372- if self .PRIMARY_KEY is None :
391+ if self .primary_key is None :
373392 raise AttributeError ("Primary key has not yet been set" )
374393
375- pk_filter = {self .PRIMARY_KEY : getattr ( self , self . PRIMARY_KEY ) }
394+ pk_filter = {self .PK_NAME : self . primary_key }
376395
377396 with con :
378397 con .execute (
379- f"DELETE FROM { self .TABLE_NAME } WHERE { self .PRIMARY_KEY } = :{ self .PRIMARY_KEY } " ,
398+ f"DELETE FROM { self .TABLE_NAME } WHERE { self .PK_NAME } = :{ self .PK_NAME } " ,
380399 pk_filter ,
381400 )
0 commit comments