1+ import re
2+
13import sqlalchemy as sa
24from sqlalchemy import exists
35from sqlalchemy .ext .hybrid import hybrid_property
46from sqlalchemy .dialects .postgresql import JSONB
5- from sqlalchemy import ForeignKeyConstraint
7+ from sqlalchemy import ForeignKeyConstraint , func
68from sqlalchemy .ext .associationproxy import association_proxy
7-
89from sqlalchemy .ext .mutable import MutableDict
9- from sqlalchemy .orm import relationship , backref
10- from sqlalchemy .sql import func
10+ from sqlalchemy .orm import relationship , backref , validates , aliased
1111import shortuuid
1212
1313from .migration_types import TSVector
1414from ..database import db
1515
16+ SEMVER_REGEX = r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$" # noqa E501
17+
1618
1719def _check_type (x ):
1820 """check annotation key type"""
@@ -193,6 +195,9 @@ class BaseStudy(BaseMixin, db.Model):
193195 versions = relationship (
194196 "Study" , backref = backref ("base_study" ), passive_deletes = True
195197 )
198+ pipeline_study_results = relationship (
199+ "PipelineStudyResult" , backref = backref ("base_study" ), passive_deletes = True
200+ )
196201
197202 __table_args__ = (
198203 db .CheckConstraint (level .in_ (["group" , "meta" ])),
@@ -263,6 +268,71 @@ def update_has_images_and_points(self):
263268 self .has_images = self .images_exist
264269 self .has_coordinates = self .points_exist
265270
271+ def display_features (self , pipelines = None ):
272+ """
273+ Display pipeline features for the base study.
274+ Only loads and returns features if pipelines are explicitly specified.
275+
276+ Args:
277+ pipelines (list, optional): List of pipeline names to display features from.
278+ If None or empty, returns empty dict.
279+ """
280+ if not pipelines :
281+ return {}
282+
283+ # Create aliases for the tables
284+ PipelineAlias = aliased (Pipeline )
285+ PipelineConfigAlias = aliased (PipelineConfig )
286+ PipelineStudyResultAlias = aliased (PipelineStudyResult )
287+
288+ # Get latest results subquery
289+ latest_results = (
290+ db .session .query (
291+ PipelineStudyResultAlias .base_study_id ,
292+ PipelineAlias .name .label ("pipeline_name" ),
293+ func .max (PipelineStudyResultAlias .date_executed ).label ("max_date" ),
294+ )
295+ .join (
296+ PipelineConfigAlias ,
297+ PipelineStudyResultAlias .config_id == PipelineConfigAlias .id ,
298+ )
299+ .join (PipelineAlias , PipelineConfigAlias .pipeline_id == PipelineAlias .id )
300+ .filter (PipelineStudyResultAlias .base_study_id == self .id )
301+ .filter (PipelineAlias .name .in_ (pipelines ))
302+ .group_by (PipelineStudyResultAlias .base_study_id , PipelineAlias .name )
303+ .subquery ()
304+ )
305+
306+ # Main query joining with latest results
307+ query = (
308+ db .session .query (
309+ PipelineStudyResultAlias .result_data ,
310+ PipelineAlias .name .label ("pipeline_name" ),
311+ )
312+ .join (
313+ PipelineConfigAlias ,
314+ PipelineStudyResultAlias .config_id == PipelineConfigAlias .id ,
315+ )
316+ .join (PipelineAlias , PipelineConfigAlias .pipeline_id == PipelineAlias .id )
317+ .join (
318+ latest_results ,
319+ (
320+ PipelineStudyResultAlias .base_study_id
321+ == latest_results .c .base_study_id
322+ )
323+ & (PipelineAlias .name == latest_results .c .pipeline_name )
324+ & (PipelineStudyResultAlias .date_executed == latest_results .c .max_date ),
325+ )
326+ )
327+
328+ # Execute query and build response
329+ results = query .all ()
330+ features = {}
331+ for result in results :
332+ features [result .pipeline_name ] = result .result_data
333+
334+ return features
335+
266336
267337class Study (BaseMixin , db .Model ):
268338 __tablename__ = "studies"
@@ -544,7 +614,6 @@ class Pipeline(BaseMixin, db.Model):
544614
545615 name = db .Column (db .String )
546616 description = db .Column (db .String )
547- version = db .Column (db .String )
548617 study_dependent = db .Column (db .Boolean , default = False )
549618 ace_compatible = db .Column (db .Boolean , default = False )
550619 pubget_compatible = db .Column (db .Boolean , default = False )
@@ -557,55 +626,39 @@ class PipelineConfig(BaseMixin, db.Model):
557626 pipeline_id = db .Column (
558627 db .Text , db .ForeignKey ("pipelines.id" , ondelete = "CASCADE" ), index = True
559628 )
629+ version = db .Column (db .String )
560630 config = db .Column (JSONB )
631+ executed_at = db .Column (
632+ db .DateTime (timezone = True )
633+ ) # when the pipeline was executed on the filesystem (not when it was ingested)
561634 config_hash = db .Column (db .String , index = True )
562635 pipeline = relationship (
563636 "Pipeline" , backref = backref ("configs" , passive_deletes = True )
564637 )
565638
639+ @validates ("version" )
640+ def validate_version (self , key , value ):
641+ if not re .match (SEMVER_REGEX , value ):
642+ raise ValueError (f"Invalid version format: { value } " )
643+ return value
566644
567- class PipelineRun (BaseMixin , db .Model ):
568- __tablename__ = "pipeline_runs"
569645
570- pipeline_id = db .Column (
571- db . Text , db . ForeignKey ( "pipelines.id" , ondelete = "CASCADE" ), index = True
572- )
646+ class PipelineStudyResult ( BaseMixin , db .Model ):
647+ __tablename__ = "pipeline_study_results"
648+
573649 config_id = db .Column (
574650 db .Text , db .ForeignKey ("pipeline_configs.id" , ondelete = "CASCADE" ), index = True
575651 )
576- config = relationship (
577- "PipelineConfig" , backref = backref ("runs" , passive_deletes = True )
578- )
579- run_index = db .Column (db .Integer ())
580-
581-
582- class PipelineRunResult (BaseMixin , db .Model ):
583- __tablename__ = "pipeline_run_results"
584-
585- run_id = db .Column (
586- db .Text , db .ForeignKey ("pipeline_runs.id" , ondelete = "CASCADE" ), index = True
587- )
588652 base_study_id = db .Column (db .Text , db .ForeignKey ("base_studies.id" ), index = True )
589653 date_executed = db .Column (db .DateTime (timezone = True ))
590- data = db .Column (JSONB )
654+ result_data = db .Column (JSONB )
591655 file_inputs = db .Column (JSONB )
592- run = relationship ("PipelineRun" , backref = backref ("results" , passive_deletes = True ))
593-
594-
595- class PipelineRunResultVote (BaseMixin , db .Model ):
596- __tablename__ = "pipeline_run_result_votes"
597-
598- run_result_id = db .Column (
599- db .Text ,
600- db .ForeignKey ("pipeline_run_results.id" , ondelete = "CASCADE" ),
601- index = True ,
656+ status = db .Column (
657+ db .Enum ("SUCCESS" , "FAILURE" , "ERROR" , "UNKNOWN" , name = "status_enum" )
602658 )
603- user_id = db .Column (db .Text , db .ForeignKey ("users.external_id" ), index = True )
604- accurate = db .Column (db .Boolean )
605- run_result = relationship (
606- "PipelineRunResult" , backref = backref ("votes" , passive_deletes = True )
659+ config = relationship (
660+ "PipelineConfig" , backref = backref ("results" , passive_deletes = True )
607661 )
608- user = relationship ("User" , backref = backref ("votes" , passive_deletes = True ))
609662
610663
611664# from . import event_listeners # noqa E402
0 commit comments