Skip to content

Commit fe25a83

Browse files
Fixes #32690: Populate Postgres stored procedure language from pg_language (#32752)
Co-authored-by: detail-app[bot] <180357370+detail-app[bot]@users.noreply.github.com>
1 parent 2127ce0 commit fe25a83

4 files changed

Lines changed: 88 additions & 10 deletions

File tree

ingestion/src/metadata/ingestion/source/database/postgres/metadata.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,11 @@
6060
PgMatviewMixin,
6161
ischema_names,
6262
)
63-
from metadata.ingestion.source.database.mssql.models import STORED_PROC_LANGUAGE_MAP
6463
from metadata.ingestion.source.database.multi_db_source import MultiDBSource
65-
from metadata.ingestion.source.database.postgres.models import PostgresStoredProcedure
64+
from metadata.ingestion.source.database.postgres.models import (
65+
POSTGRES_STORED_PROC_LANGUAGE_MAP,
66+
PostgresStoredProcedure,
67+
)
6668
from metadata.ingestion.source.database.postgres.queries import (
6769
POSTGRES_GET_ALL_TABLE_PG_POLICY,
6870
POSTGRES_GET_DB_NAMES,
@@ -302,7 +304,7 @@ def yield_stored_procedure(self, stored_procedure) -> Iterable[Either[CreateStor
302304
name=EntityName(stored_procedure.name),
303305
description=Markdown(stored_procedure.description) if stored_procedure.description else None,
304306
storedProcedureCode=StoredProcedureCode(
305-
language=STORED_PROC_LANGUAGE_MAP.get(stored_procedure.language),
307+
language=POSTGRES_STORED_PROC_LANGUAGE_MAP.get((stored_procedure.language or "").lower()),
306308
code=stored_procedure.definition,
307309
),
308310
databaseSchema=fqn.build(

ingestion/src/metadata/ingestion/source/database/postgres/models.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,24 @@
1414

1515
from pydantic import BaseModel, Field
1616

17+
from metadata.generated.schema.entity.data.storedProcedure import Language
18+
19+
# Postgres exposes the routine implementation language via pg_language.lanname
20+
# (e.g. sql, plpgsql, c, internal, plpython3u, ...). The OpenMetadata Language
21+
# enum only has SQL, Java, JavaScript, Python and External, so only `sql` has a
22+
# direct mapping here. Other languages are deliberately left as None (honest
23+
# "unknown") rather than mislabelled as SQL until the enum is extended.
24+
POSTGRES_STORED_PROC_LANGUAGE_MAP: dict[str, Language] = {
25+
"sql": Language.SQL,
26+
}
27+
1728

1829
class PostgresStoredProcedure(BaseModel):
1930
"""Postgres stored procedure list query results"""
2031

2132
name: str = Field(alias="procedure_name")
2233
schema: str = Field(alias="schema_name")
2334
definition: str
24-
language: str | None = None
35+
language: str | None = Field(None, alias="language")
2536
procedure_type: str | None = Field(None, alias="procedure_type")
2637
description: str | None = Field(None, alias="description")

ingestion/src/metadata/ingestion/source/database/postgres/queries.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,10 +230,12 @@
230230
proargtypes AS argument_types,
231231
prorettype::regtype AS return_type,
232232
prosrc AS definition,
233+
pg_language.lanname AS language,
233234
'StoredProcedure' as procedure_type,
234235
obj_description(pg_proc.oid, 'pg_proc') AS description
235236
FROM pg_proc
236237
JOIN pg_namespace ON pg_proc.pronamespace = pg_namespace.oid
238+
JOIN pg_language ON pg_proc.prolang = pg_language.oid
237239
WHERE prokind = 'p'
238240
and pg_namespace.nspname = '{schema_name}';
239241
"""
@@ -245,11 +247,13 @@
245247
proargtypes AS argument_types,
246248
prorettype :: regtype AS return_type,
247249
prosrc AS definition,
250+
pg_language.lanname AS language,
248251
'Function' as procedure_type,
249252
obj_description(pg_proc.oid, 'pg_proc') AS description
250253
FROM
251254
pg_proc
252255
JOIN pg_namespace ON pg_proc.pronamespace = pg_namespace.oid
256+
JOIN pg_language ON pg_proc.prolang = pg_language.oid
253257
WHERE
254258
prokind = 'f'
255259
and pg_namespace.nspname = '{schema_name}';

ingestion/tests/unit/topology/database/test_postgres.py

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
from metadata.generated.schema.entity.data.database import Database
2424
from metadata.generated.schema.entity.data.databaseSchema import DatabaseSchema
25+
from metadata.generated.schema.entity.data.storedProcedure import Language
2526
from metadata.generated.schema.entity.data.table import (
2627
Column,
2728
Constraint,
@@ -45,7 +46,12 @@
4546
POLYGON,
4647
)
4748
from 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+
)
4955
from metadata.ingestion.source.database.postgres.usage import PostgresUsageSource
5056
from 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

Comments
 (0)