@@ -1003,6 +1003,232 @@ ALTER ROLE agent RESET restrict_to_user_objects;
10031003----
10041004COMPLETE 0
10051005
1006+ # =============================================================================
1007+ # Data product view semantics (mz_mcp_data_products / _details)
1008+ #
1009+ # The RBAC cases above exercise privilege filtering. These cases pin the
1010+ # view logic itself: which objects qualify, how the description is chosen,
1011+ # and how column types map into the JSON Schema. They run as a
1012+ # fully-privileged owner so the object set is deterministic and independent
1013+ # of the agent-role grants above.
1014+ # =============================================================================
1015+
1016+ statement ok
1017+ CREATE SCHEMA dp_semantics;
1018+
1019+ statement ok
1020+ SET search_path = dp_semantics;
1021+
1022+ # A materialized view qualifies as a data product with no index required.
1023+ statement ok
1024+ CREATE MATERIALIZED VIEW mv_plain AS SELECT 1 AS id;
1025+
1026+ # A plain (non-indexed) view does NOT qualify: reading it would trigger a
1027+ # full recompute.
1028+ statement ok
1029+ CREATE VIEW v_unindexed AS SELECT 1 AS id;
1030+
1031+ # A view qualifies only once it has an index.
1032+ statement ok
1033+ CREATE VIEW v_indexed AS SELECT 1 AS id, 'a' AS label;
1034+
1035+ statement ok
1036+ CREATE INDEX v_indexed_idx ON v_indexed (id);
1037+
1038+ query T
1039+ SELECT object_name FROM mz_internal.mz_mcp_data_products
1040+ WHERE object_name LIKE '%"dp_semantics"%' ORDER BY object_name;
1041+ ----
1042+ "materialize"."dp_semantics"."mv_plain"
1043+ "materialize"."dp_semantics"."v_indexed"
1044+
1045+ # Multiple indexes on one object must not duplicate its row (SELECT DISTINCT).
1046+ statement ok
1047+ CREATE INDEX v_indexed_idx2 ON v_indexed (label);
1048+
1049+ query T
1050+ SELECT object_name FROM mz_internal.mz_mcp_data_products
1051+ WHERE object_name = '"materialize"."dp_semantics"."v_indexed"';
1052+ ----
1053+ "materialize"."dp_semantics"."v_indexed"
1054+
1055+ # -----------------------------------------------------------------------------
1056+ # Description precedence: index comment wins over object comment (COALESCE).
1057+ # -----------------------------------------------------------------------------
1058+
1059+ # No comments: description is NULL.
1060+ query T
1061+ SELECT description FROM mz_internal.mz_mcp_data_products
1062+ WHERE object_name = '"materialize"."dp_semantics"."mv_plain"';
1063+ ----
1064+ NULL
1065+
1066+ # Object comment only: description is the object comment.
1067+ statement ok
1068+ COMMENT ON MATERIALIZED VIEW mv_plain IS 'the object comment';
1069+
1070+ query T
1071+ SELECT description FROM mz_internal.mz_mcp_data_products
1072+ WHERE object_name = '"materialize"."dp_semantics"."mv_plain"';
1073+ ----
1074+ the object comment
1075+
1076+ # Index comment present: it takes precedence over the object comment. Uses a
1077+ # dedicated single-index view so the COALESCE precedence is unambiguous (with
1078+ # multiple indexes the DISTINCT can surface one row per differing description).
1079+ statement ok
1080+ CREATE VIEW v_desc AS SELECT 1 AS id;
1081+
1082+ statement ok
1083+ CREATE INDEX v_desc_idx ON v_desc (id);
1084+
1085+ statement ok
1086+ COMMENT ON VIEW v_desc IS 'object comment on view';
1087+
1088+ statement ok
1089+ COMMENT ON INDEX v_desc_idx IS 'index comment wins';
1090+
1091+ query T
1092+ SELECT description FROM mz_internal.mz_mcp_data_products
1093+ WHERE object_name = '"materialize"."dp_semantics"."v_desc"';
1094+ ----
1095+ index comment wins
1096+
1097+ # -----------------------------------------------------------------------------
1098+ # JSON Schema type mapping in mz_mcp_data_product_details.
1099+ # -----------------------------------------------------------------------------
1100+
1101+ statement ok
1102+ CREATE MATERIALIZED VIEW mv_types AS
1103+ SELECT
1104+ 1::int AS c_int,
1105+ 1.5::double AS c_double,
1106+ true AS c_bool,
1107+ 'x'::text AS c_text,
1108+ '2024-01-01'::date AS c_date,
1109+ '{}'::jsonb AS c_jsonb,
1110+ '\x00'::bytea AS c_bytea,
1111+ '12:34:56'::time AS c_time,
1112+ '2024-01-01 12:00:00'::timestamp AS c_ts,
1113+ '2024-01-01 12:00:00+00'::timestamptz AS c_tstz,
1114+ '00000000-0000-0000-0000-000000000000'::uuid AS c_uuid;
1115+
1116+ # Numeric columns map to JSON Schema type "number".
1117+ query T
1118+ SELECT schema->'properties'->'c_int'->>'type' FROM mz_internal.mz_mcp_data_product_details
1119+ WHERE object_name = '"materialize"."dp_semantics"."mv_types"';
1120+ ----
1121+ number
1122+
1123+ query T
1124+ SELECT schema->'properties'->'c_double'->>'type' FROM mz_internal.mz_mcp_data_product_details
1125+ WHERE object_name = '"materialize"."dp_semantics"."mv_types"';
1126+ ----
1127+ number
1128+
1129+ # Boolean maps to "boolean".
1130+ query T
1131+ SELECT schema->'properties'->'c_bool'->>'type' FROM mz_internal.mz_mcp_data_product_details
1132+ WHERE object_name = '"materialize"."dp_semantics"."mv_types"';
1133+ ----
1134+ boolean
1135+
1136+ # text falls through to the "string" default.
1137+ query T
1138+ SELECT schema->'properties'->'c_text'->>'type' FROM mz_internal.mz_mcp_data_product_details
1139+ WHERE object_name = '"materialize"."dp_semantics"."mv_types"';
1140+ ----
1141+ string
1142+
1143+ # date is a string with a format hint.
1144+ query TT
1145+ SELECT schema->'properties'->'c_date'->>'type', schema->'properties'->'c_date'->>'format'
1146+ FROM mz_internal.mz_mcp_data_product_details
1147+ WHERE object_name = '"materialize"."dp_semantics"."mv_types"';
1148+ ----
1149+ string date
1150+
1151+ # jsonb maps to a nested "object".
1152+ query T
1153+ SELECT schema->'properties'->'c_jsonb'->>'type' FROM mz_internal.mz_mcp_data_product_details
1154+ WHERE object_name = '"materialize"."dp_semantics"."mv_types"';
1155+ ----
1156+ object
1157+
1158+ # bytea is the most distinctive branch: string with the two extra content
1159+ # hints so a JSON Schema consumer knows to Base64-decode the value.
1160+ query TTT
1161+ SELECT
1162+ schema->'properties'->'c_bytea'->>'type',
1163+ schema->'properties'->'c_bytea'->>'contentEncoding',
1164+ schema->'properties'->'c_bytea'->>'contentMediaType'
1165+ FROM mz_internal.mz_mcp_data_product_details
1166+ WHERE object_name = '"materialize"."dp_semantics"."mv_types"';
1167+ ----
1168+ string base64 application/octet-stream
1169+
1170+ # time is a string with format=time.
1171+ query TT
1172+ SELECT schema->'properties'->'c_time'->>'type', schema->'properties'->'c_time'->>'format'
1173+ FROM mz_internal.mz_mcp_data_product_details
1174+ WHERE object_name = '"materialize"."dp_semantics"."mv_types"';
1175+ ----
1176+ string time
1177+
1178+ # timestamp and timestamptz both match the `ilike 'timestamp%'` branch and
1179+ # map to string with format=date-time.
1180+ query TT
1181+ SELECT schema->'properties'->'c_ts'->>'type', schema->'properties'->'c_ts'->>'format'
1182+ FROM mz_internal.mz_mcp_data_product_details
1183+ WHERE object_name = '"materialize"."dp_semantics"."mv_types"';
1184+ ----
1185+ string date-time
1186+
1187+ query TT
1188+ SELECT schema->'properties'->'c_tstz'->>'type', schema->'properties'->'c_tstz'->>'format'
1189+ FROM mz_internal.mz_mcp_data_product_details
1190+ WHERE object_name = '"materialize"."dp_semantics"."mv_types"';
1191+ ----
1192+ string date-time
1193+
1194+ # uuid is a string with format=uuid.
1195+ query TT
1196+ SELECT schema->'properties'->'c_uuid'->>'type', schema->'properties'->'c_uuid'->>'format'
1197+ FROM mz_internal.mz_mcp_data_product_details
1198+ WHERE object_name = '"materialize"."dp_semantics"."mv_types"';
1199+ ----
1200+ string uuid
1201+
1202+ # -----------------------------------------------------------------------------
1203+ # Column comments surface as the property description; absent otherwise.
1204+ # -----------------------------------------------------------------------------
1205+
1206+ statement ok
1207+ COMMENT ON COLUMN mv_types.c_int IS 'the primary id';
1208+
1209+ query T
1210+ SELECT schema->'properties'->'c_int'->>'description' FROM mz_internal.mz_mcp_data_product_details
1211+ WHERE object_name = '"materialize"."dp_semantics"."mv_types"';
1212+ ----
1213+ the primary id
1214+
1215+ # A column with no comment has no description key (jsonb_strip_nulls removes it).
1216+ query T
1217+ SELECT schema->'properties'->'c_double' ? 'description' FROM mz_internal.mz_mcp_data_product_details
1218+ WHERE object_name = '"materialize"."dp_semantics"."mv_types"';
1219+ ----
1220+ false
1221+
1222+ # -----------------------------------------------------------------------------
1223+ # Cleanup for this section.
1224+ # -----------------------------------------------------------------------------
1225+
1226+ statement ok
1227+ SET search_path = public;
1228+
1229+ statement ok
1230+ DROP SCHEMA dp_semantics CASCADE;
1231+
10061232# =============================================================================
10071233# Cleanup
10081234# =============================================================================
0 commit comments