2222
2323from metadata .generated .schema .entity .data .database import Database
2424from metadata .generated .schema .entity .data .databaseSchema import DatabaseSchema
25+ from metadata .generated .schema .entity .data .storedProcedure import Language
2526from metadata .generated .schema .entity .data .table import (
2627 Column ,
2728 Constraint ,
4546 POLYGON ,
4647)
4748from metadata .ingestion .source .database .postgres .metadata import PostgresSource
48- from metadata .ingestion .source .database .postgres .queries import POSTGRES_GET_TABLE_NAMES
49+ from metadata .ingestion .source .database .postgres .models import PostgresStoredProcedure
50+ from metadata .ingestion .source .database .postgres .queries import (
51+ POSTGRES_GET_FUNCTIONS ,
52+ POSTGRES_GET_STORED_PROCEDURES ,
53+ POSTGRES_GET_TABLE_NAMES ,
54+ )
4955from metadata .ingestion .source .database .postgres .usage import PostgresUsageSource
5056from metadata .ingestion .source .database .postgres .utils import get_postgres_version
5157
@@ -360,22 +366,24 @@ def test_get_stored_procedures(self):
360366 mock_engine = MagicMock ()
361367 self .postgres_source .engine = mock_engine
362368
363- # Mock rows
369+ # Mock rows. The `language` value mirrors what the real
370+ # POSTGRES_GET_STORED_PROCEDURES query returns via `pg_language.lanname`
371+ # (lowercase, e.g. "sql", "plpgsql").
364372 row1 = MagicMock ()
365373 row1 ._mapping = {
366374 "procedure_name" : "sp_include" ,
367375 "schema_name" : "test_schema" ,
368376 "definition" : "def1" ,
369- "language" : "SQL " ,
370- "procedure_type" : "PROCEDURE " ,
377+ "language" : "sql " ,
378+ "procedure_type" : "StoredProcedure " ,
371379 }
372380 row2 = MagicMock ()
373381 row2 ._mapping = {
374382 "procedure_name" : "sp_exclude" ,
375383 "schema_name" : "test_schema" ,
376384 "definition" : "def2" ,
377- "language" : "SQL " ,
378- "procedure_type" : "PROCEDURE " ,
385+ "language" : "sql " ,
386+ "procedure_type" : "StoredProcedure " ,
379387 }
380388
381389 # PostgreSQL get_stored_procedures calls _get_stored_procedures_internal twice
@@ -395,6 +403,59 @@ def test_get_stored_procedures(self):
395403
396404 self .assertEqual (len (results ), 1 )
397405 self .assertEqual (results [0 ].name , "sp_include" )
406+ # The language column selected by the query must be parsed into the model
407+ self .assertEqual (results [0 ].language , "sql" )
408+
409+ def _yield_language (self , language : str | None , procedure_type : str ) -> Language | None :
410+ """Build a PostgresStoredProcedure (via the same model_validate(dict) path the
411+ production connector uses) and return the language mapped by
412+ yield_stored_procedure into the CreateStoredProcedureRequest."""
413+ row_mapping = {
414+ "procedure_name" : "proc" ,
415+ "schema_name" : "test_schema" ,
416+ "definition" : "SELECT 1" ,
417+ "language" : language ,
418+ "procedure_type" : procedure_type ,
419+ }
420+ stored_procedure = PostgresStoredProcedure .model_validate (row_mapping )
421+ request = next (iter (self .postgres_source .yield_stored_procedure (stored_procedure ))).right
422+ assert request is not None
423+ return request .storedProcedureCode .language
424+
425+ def test_yield_stored_procedure_maps_sql_language (self ):
426+ """A Postgres `sql` routine (the common LANGUAGE sql case) maps to Language.SQL,
427+ for both stored procedures and functions."""
428+ self .assertEqual (
429+ self ._yield_language ("sql" , "StoredProcedure" ),
430+ Language .SQL ,
431+ )
432+ self .assertEqual (
433+ self ._yield_language ("sql" , "Function" ),
434+ Language .SQL ,
435+ )
436+
437+ def test_yield_stored_procedure_unmapped_language_stays_none (self ):
438+ """Postgres languages with no Language enum member (plpgsql, c, internal, ...)
439+ must NOT be mislabelled as SQL -- they stay None (honest "unknown")."""
440+ self .assertIsNone (self ._yield_language ("plpgsql" , "StoredProcedure" ))
441+ self .assertIsNone (self ._yield_language ("c" , "Function" ))
442+ self .assertIsNone (self ._yield_language ("internal" , "StoredProcedure" ))
443+
444+ def test_stored_procedure_queries_select_language_column (self ):
445+ """Both Postgres SP/Function queries must select pg_language.lanname and join
446+ pg_language, otherwise the language is silently null (regression guard)."""
447+ for query in (POSTGRES_GET_STORED_PROCEDURES , POSTGRES_GET_FUNCTIONS ):
448+ self .assertIn ("pg_language.lanname AS language" , query )
449+ self .assertIn ("JOIN pg_language ON pg_proc.prolang = pg_language.oid" , query )
450+
451+ def test_postgres_does_not_reuse_mssql_language_map (self ):
452+ """The Postgres connector must not depend on the MSSQL-specific
453+ STORED_PROC_LANGUAGE_MAP (which is keyed on uppercase SQL/EXTERNAL and does
454+ not cover Postgres' sql/plpgsql vocabulary)."""
455+ from metadata .ingestion .source .database .postgres import metadata as postgres_metadata
456+
457+ self .assertFalse (hasattr (postgres_metadata , "STORED_PROC_LANGUAGE_MAP" ))
458+ self .assertTrue (hasattr (postgres_metadata , "POSTGRES_STORED_PROC_LANGUAGE_MAP" ))
398459
399460 def test_get_version_info (self ):
400461 mock_engine = MagicMock ()
0 commit comments