@@ -264,6 +264,10 @@ def _read_unscored_data(self, con):
264264
265265 def _build_score_sql (self , con ):
266266 """Build SQL fragment for score columns in unscored files."""
267+ # Skip if exclude_feature_var is enabled
268+ if self .config .exclude_feature_var :
269+ return ""
270+
267271 score_sql = ""
268272 if check_sqlite_table (con , "FEATURE_MS1" ):
269273 score_sql = write_scores_sql_command (
@@ -1516,7 +1520,23 @@ def _write_parquet(self) -> None:
15161520 def _convert_to_split_parquet (self ) -> None :
15171521 """Convert OSW to split parquet format"""
15181522 conn = duckdb .connect (":memory:" )
1519- load_sqlite_scanner (conn )
1523+
1524+ try :
1525+ load_sqlite_scanner (conn )
1526+ except Exception as scanner_error :
1527+ # If sqlite_scanner fails to load (e.g., in containers without internet),
1528+ # provide helpful guidance but continue with fallback
1529+ if "Failed to download extension" in str (scanner_error ) or "Connection timed out" in str (scanner_error ):
1530+ click .echo (
1531+ "Warning: sqlite_scanner extension could not be loaded (likely in container without internet access).\n "
1532+ "To fix: Set DUCKDB_EXTENSION_DIRECTORY environment variable to a directory with pre-downloaded extensions.\n "
1533+ "Or pre-download extensions on your host with: "
1534+ "python3 -c 'import duckdb; duckdb.connect(\" :memory:\" ).execute(\" LOAD sqlite_scanner\" )'\n "
1535+ "Continuing with alternative method..." ,
1536+ err = True
1537+ )
1538+ else :
1539+ raise
15201540
15211541 try :
15221542 # Prepare column information
@@ -1533,7 +1553,23 @@ def _convert_to_split_parquet(self) -> None:
15331553 def _convert_to_single_parquet (self ) -> None :
15341554 """Convert OSW to single parquet file"""
15351555 conn = duckdb .connect (":memory:" )
1536- load_sqlite_scanner (conn )
1556+
1557+ try :
1558+ load_sqlite_scanner (conn )
1559+ except Exception as scanner_error :
1560+ # If sqlite_scanner fails to load (e.g., in containers without internet),
1561+ # provide helpful guidance but continue with fallback
1562+ if "Failed to download extension" in str (scanner_error ) or "Connection timed out" in str (scanner_error ):
1563+ click .echo (
1564+ "Warning: sqlite_scanner extension could not be loaded (likely in container without internet access).\n "
1565+ "To fix: Set DUCKDB_EXTENSION_DIRECTORY environment variable to a directory with pre-downloaded extensions.\n "
1566+ "Or pre-download extensions on your host with: "
1567+ "python3 -c 'import duckdb; duckdb.connect(\" :memory:\" ).execute(\" LOAD sqlite_scanner\" )'\n "
1568+ "Continuing with alternative method..." ,
1569+ err = True
1570+ )
1571+ else :
1572+ raise
15371573
15381574 try :
15391575 # Prepare column information
@@ -1614,9 +1650,45 @@ def _prepare_column_info(self, conn) -> dict:
16141650 column_info ["score_peptide_contexts" ] = self ._check_contexts (
16151651 sql_conn , "SCORE_PEPTIDE"
16161652 )
1653+
1654+ # Create necessary indices to speed up joins
1655+ logger .info ("Creating indices for faster export" )
1656+ self ._create_export_indices (sql_conn )
16171657
16181658 return column_info
16191659
1660+ def _create_export_indices (self , sql_conn : sqlite3 .Connection ) -> None :
1661+ """Create indices to optimize join performance during export"""
1662+ indices_to_create = [
1663+ ("PRECURSOR_PEPTIDE_MAPPING" , "PRECURSOR_ID" , "idx_ppm_precursor_id" ),
1664+ ("PRECURSOR_PEPTIDE_MAPPING" , "PEPTIDE_ID" , "idx_ppm_peptide_id" ),
1665+ ("PEPTIDE_PROTEIN_MAPPING" , "PEPTIDE_ID" , "idx_pprotm_peptide_id" ),
1666+ ("PEPTIDE_PROTEIN_MAPPING" , "PROTEIN_ID" , "idx_pprotm_protein_id" ),
1667+ ("PEPTIDE_GENE_MAPPING" , "PEPTIDE_ID" , "idx_pgm_peptide_id" ),
1668+ ("PEPTIDE_GENE_MAPPING" , "GENE_ID" , "idx_pgm_gene_id" ),
1669+ ("FEATURE" , "PRECURSOR_ID" , "idx_feat_precursor_id" ),
1670+ ("FEATURE" , "RUN_ID" , "idx_feat_run_id" ),
1671+ ("FEATURE_MS1" , "FEATURE_ID" , "idx_feat_ms1_feature_id" ),
1672+ ("FEATURE_MS2" , "FEATURE_ID" , "idx_feat_ms2_feature_id" ),
1673+ ("FEATURE_TRANSITION" , "FEATURE_ID" , "idx_feat_trans_feature_id" ),
1674+ ("FEATURE_TRANSITION" , "TRANSITION_ID" , "idx_feat_trans_trans_id" ),
1675+ ("TRANSITION_PRECURSOR_MAPPING" , "TRANSITION_ID" , "idx_tpm_transition_id" ),
1676+ ("TRANSITION_PRECURSOR_MAPPING" , "PRECURSOR_ID" , "idx_tpm_precursor_id" ),
1677+ ("TRANSITION_PEPTIDE_MAPPING" , "TRANSITION_ID" , "idx_tpeptm_transition_id" ),
1678+ ("TRANSITION_PEPTIDE_MAPPING" , "PEPTIDE_ID" , "idx_tpeptm_peptide_id" ),
1679+ ]
1680+
1681+ for table , column , index_name in indices_to_create :
1682+ try :
1683+ sql_conn .execute (
1684+ f"CREATE INDEX IF NOT EXISTS { index_name } ON { table } ({ column } )"
1685+ )
1686+ except sqlite3 .OperationalError as e :
1687+ logger .debug (f"Could not create index { index_name } : { e } " )
1688+
1689+ sql_conn .commit ()
1690+ logger .debug ("Indices created for export optimization" )
1691+
16201692 def _export_split_by_run (self , conn , column_info : dict ) -> None :
16211693 """Export data split by run into separate directories"""
16221694 os .makedirs (self .config .outfile , exist_ok = True )
@@ -1707,30 +1779,24 @@ def _export_combined(self, conn, column_info: dict) -> None:
17071779 self ._export_alignment_data (conn )
17081780
17091781 def _export_single_file (self , conn , column_info : dict ) -> None :
1710- """Export all data to a single parquet file"""
1711- # Create temp table with combined schema
1712- logger .debug ("Creating temporary table for combined export" )
1713- self ._create_temp_table (conn , column_info )
1782+ """Export all data to a single parquet file using streaming (UNION ALL)"""
1783+ logger .info (f"Exporting combined data to { self .config .outfile } " )
17141784
1715- # Insert precursor data
1716- logger .debug ("Inserting precursor data into temp table" )
1785+ # Build precursor query
17171786 precursor_query = self ._build_combined_precursor_query (conn , column_info )
1718- # print(precursor_query)
1719- conn .execute (f"INSERT INTO temp_table { precursor_query } " )
17201787
1721- # Insert transition data if requested
1788+ # Build combined query
17221789 if self .config .include_transition_data :
1723- logger .debug ("Inserting transition data into temp table " )
1790+ logger .debug ("Including transition data in export " )
17241791 transition_query = self ._build_combined_transition_query (column_info )
1725- conn .execute (f"INSERT INTO temp_table { transition_query } " )
1792+ # Combine queries with UNION ALL - this streams directly to parquet
1793+ combined_query = f"{ precursor_query } \n UNION ALL\n { transition_query } "
17261794 else :
1727- logger .info (
1728- "Skipping transition data export (include_transition_data=False)"
1729- )
1795+ logger .info ("Skipping transition data export (include_transition_data=False)" )
1796+ combined_query = precursor_query
17301797
1731- # Export to parquet
1732- logger .info (f"Exporting combined data to { self .config .outfile } " )
1733- self ._execute_copy_query (conn , "SELECT * FROM temp_table" , self .config .outfile )
1798+ # Stream directly to parquet file without intermediate temp table
1799+ self ._execute_copy_query (conn , combined_query , self .config .outfile )
17341800
17351801 # Export alignment data if exists
17361802 if column_info ["feature_ms2_alignment_exists" ]:
@@ -1775,44 +1841,16 @@ def _register_peptide_ipf_map(self, conn: duckdb.DuckDBPyConnection) -> None:
17751841 )
17761842
17771843 def _create_unimod_to_codename_peptide_id_mapping_table (self ) -> None :
1778- """Create peptide unimod to codename mapping table in SQLite database."""
1844+ """Create peptide unimod to codename mapping table in SQLite database.
1845+
1846+ Processes peptides in chunks to reduce memory footprint for large datasets.
1847+ """
17791848 logger .info (
17801849 "Generating peptide unimod to codename mapping and storing in SQLite"
17811850 )
17821851
17831852 with sqlite3 .connect (self .config .infile ) as sql_conn :
1784- # First get the peptide table and process it with pyopenms
1785- peptide_df = pd .read_sql_query (
1786- "SELECT ID, MODIFIED_SEQUENCE FROM PEPTIDE" , sql_conn
1787- )
1788-
1789- peptide_df ["codename" ] = peptide_df ["MODIFIED_SEQUENCE" ].apply (
1790- unimod_to_codename
1791- )
1792-
1793- # Create the merged mapping
1794- unimod_mask = peptide_df ["MODIFIED_SEQUENCE" ].str .contains ("UniMod" )
1795- merged_df = pd .merge (
1796- peptide_df [unimod_mask ][["codename" , "ID" ]],
1797- peptide_df [~ unimod_mask ][["codename" , "ID" ]],
1798- on = "codename" ,
1799- suffixes = ("_unimod" , "_codename" ),
1800- how = "outer" ,
1801- )
1802-
1803- # Fill NaN values in the 'ID_codename' column with the 'ID_unimod' values
1804- merged_df ["ID_codename" ] = merged_df ["ID_codename" ].fillna (
1805- merged_df ["ID_unimod" ]
1806- )
1807- # Fill NaN values in the 'ID_unimod' column with the 'ID_codename' values
1808- merged_df ["ID_unimod" ] = merged_df ["ID_unimod" ].fillna (
1809- merged_df ["ID_codename" ]
1810- )
1811-
1812- merged_df ["ID_unimod" ] = merged_df ["ID_unimod" ].astype (int )
1813- merged_df ["ID_codename" ] = merged_df ["ID_codename" ].astype (int )
1814-
1815- # Create the UNIMOD_TO_CODENAME_PEPTIDE_ID_MAPPING table in SQLite
1853+ # Create the mapping table first
18161854 sql_conn .execute (
18171855 """
18181856 CREATE TABLE IF NOT EXISTS UNIMOD_TO_CODENAME_PEPTIDE_ID_MAPPING (
@@ -1824,14 +1862,73 @@ def _create_unimod_to_codename_peptide_id_mapping_table(self) -> None:
18241862 """
18251863 )
18261864 sql_conn .execute ("DELETE FROM UNIMOD_TO_CODENAME_PEPTIDE_ID_MAPPING" )
1865+ sql_conn .commit ()
18271866
1828- # Insert the data into SQLite table
1829- merged_df [["ID_unimod" , "ID_codename" , "codename" ]].to_sql (
1830- "UNIMOD_TO_CODENAME_PEPTIDE_ID_MAPPING" ,
1831- sql_conn ,
1832- if_exists = "append" ,
1833- index = False ,
1834- )
1867+ # Get total count for progress tracking
1868+ total_count = sql_conn .execute (
1869+ "SELECT COUNT(*) FROM PEPTIDE"
1870+ ).fetchone ()[0 ]
1871+ logger .info (f"Processing { total_count } peptides in chunks" )
1872+
1873+ # Process peptides in chunks to reduce memory footprint
1874+ chunk_size = 50000 # Process 50k peptides at a time
1875+ processed = 0
1876+
1877+ while processed < total_count :
1878+ # Fetch chunk of peptides
1879+ peptide_chunk = pd .read_sql_query (
1880+ f"""SELECT ID, MODIFIED_SEQUENCE FROM PEPTIDE
1881+ LIMIT { chunk_size } OFFSET { processed } """ ,
1882+ sql_conn ,
1883+ )
1884+
1885+ if peptide_chunk .empty :
1886+ break
1887+
1888+ # Process chunk
1889+ peptide_chunk ["codename" ] = peptide_chunk ["MODIFIED_SEQUENCE" ].apply (
1890+ unimod_to_codename
1891+ )
1892+
1893+ # Create mapping for this chunk
1894+ unimod_mask = peptide_chunk ["MODIFIED_SEQUENCE" ].str .contains ("UniMod" , na = False )
1895+ unimod_chunk = peptide_chunk [unimod_mask ][["codename" , "ID" ]].copy ()
1896+ unimod_chunk .columns = ["codename" , "ID_unimod" ]
1897+
1898+ codename_chunk = peptide_chunk [~ unimod_mask ][["codename" , "ID" ]].copy ()
1899+ codename_chunk .columns = ["codename" , "ID_codename" ]
1900+
1901+ # Merge on codename
1902+ merged_chunk = pd .merge (
1903+ unimod_chunk ,
1904+ codename_chunk ,
1905+ on = "codename" ,
1906+ how = "outer" ,
1907+ )
1908+
1909+ # Fill NaN values
1910+ merged_chunk ["ID_codename" ] = merged_chunk ["ID_codename" ].fillna (
1911+ merged_chunk ["ID_unimod" ]
1912+ )
1913+ merged_chunk ["ID_unimod" ] = merged_chunk ["ID_unimod" ].fillna (
1914+ merged_chunk ["ID_codename" ]
1915+ )
1916+
1917+ merged_chunk ["ID_unimod" ] = merged_chunk ["ID_unimod" ].astype (int )
1918+ merged_chunk ["ID_codename" ] = merged_chunk ["ID_codename" ].astype (int )
1919+
1920+ # Insert chunk into SQLite
1921+ merged_chunk [["ID_unimod" , "ID_codename" , "codename" ]].to_sql (
1922+ "UNIMOD_TO_CODENAME_PEPTIDE_ID_MAPPING" ,
1923+ sql_conn ,
1924+ if_exists = "append" ,
1925+ index = False ,
1926+ )
1927+
1928+ processed += len (peptide_chunk )
1929+ logger .debug (f"Processed { processed } /{ total_count } peptides" )
1930+
1931+ sql_conn .commit ()
18351932
18361933 # Create indices for better performance
18371934 sql_conn .execute (
@@ -1845,8 +1942,12 @@ def _create_unimod_to_codename_peptide_id_mapping_table(self) -> None:
18451942 )
18461943
18471944 sql_conn .commit ()
1945+
1946+ final_count = sql_conn .execute (
1947+ "SELECT COUNT(*) FROM UNIMOD_TO_CODENAME_PEPTIDE_ID_MAPPING"
1948+ ).fetchone ()[0 ]
18481949 logger .info (
1849- f"Successfully created UNIMOD_TO_CODENAME_PEPTIDE_ID_MAPPING table with { len ( merged_df ) } mappings"
1950+ f"Successfully created UNIMOD_TO_CODENAME_PEPTIDE_ID_MAPPING table with { final_count } mappings"
18501951 )
18511952
18521953 def _insert_precursor_peptide_ipf_map (self ) -> None :
@@ -2472,7 +2573,7 @@ def _export_alignment_data(self, conn, path: str = None) -> None:
24722573 has_score_alignment = check_sqlite_table (sql_conn , "SCORE_ALIGNMENT" )
24732574
24742575 if has_score_alignment :
2475- # Export with alignment scores
2576+ # Export with alignment scores - use ROW_NUMBER to get best score per feature
24762577 query = f"""
24772578 SELECT
24782579 FEATURE_MS2_ALIGNMENT.ALIGNMENT_ID,
@@ -2496,9 +2597,13 @@ def _export_alignment_data(self, conn, path: str = None) -> None:
24962597 SCORE_ALIGNMENT.QVALUE AS QVALUE
24972598 FROM sqlite_scan('{ self .config .infile } ', 'FEATURE_MS2_ALIGNMENT') AS FEATURE_MS2_ALIGNMENT
24982599 LEFT JOIN (
2499- SELECT FEATURE_ID, SCORE, PEP, QVALUE, MIN(QVALUE) as MIN_QVALUE
2500- FROM sqlite_scan('{ self .config .infile } ', 'SCORE_ALIGNMENT')
2501- GROUP BY FEATURE_ID
2600+ SELECT FEATURE_ID, SCORE, PEP, QVALUE
2601+ FROM (
2602+ SELECT FEATURE_ID, SCORE, PEP, QVALUE,
2603+ ROW_NUMBER() OVER (PARTITION BY FEATURE_ID ORDER BY QVALUE ASC) as rn
2604+ FROM sqlite_scan('{ self .config .infile } ', 'SCORE_ALIGNMENT')
2605+ ) t
2606+ WHERE rn = 1
25022607 ) AS SCORE_ALIGNMENT
25032608 ON FEATURE_MS2_ALIGNMENT.ALIGNED_FEATURE_ID = SCORE_ALIGNMENT.FEATURE_ID
25042609 """
0 commit comments