codeant-ai-for-open-source[bot] commented on code in PR #39461:
URL: https://github.com/apache/superset/pull/39461#discussion_r3411616837


##########
superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx:
##########
@@ -883,602 +835,693 @@ const mapStateToProps = (state: RootState) => ({
 const connector = connect(mapStateToProps, mapDispatchToProps);
 type PropsFromRedux = ConnectedProps<typeof connector>;
 
-type DatasourceEditorProps = DatasourceEditorOwnProps &
-  PropsFromRedux & {
-    theme?: SupersetTheme;
-  };
-
-class DatasourceEditor extends PureComponent<
-  DatasourceEditorProps,
-  DatasourceEditorState
-> {
-  private isComponentMounted: boolean;
+type DatasourceEditorProps = DatasourceEditorOwnProps & PropsFromRedux;
+
+function DatasourceEditor({
+  datasource: propsDatasource,
+  onChange = () => {},
+  addSuccessToast,
+  addDangerToast,
+  setIsEditing = () => {},
+  database,
+  runQuery,
+  resetQuery,
+  formatQuery: formatQueryAction,
+}: DatasourceEditorProps) {
+  const theme = useTheme();
+  const isComponentMounted = useRef(false);
+  const isInitialMount = useRef(true);
+  const prevPropsDatasourceRef = useRef(propsDatasource);
+  const isSyncingColumnsFromProps = useRef(false);
+  const abortControllers = useRef<AbortControllers>({
+    formatQuery: null,
+    formatSql: null,
+    syncMetadata: null,
+    fetchUsageData: null,
+  });
+
+  // Initialize datasource state with transformed owners and metrics
+  const [datasource, setDatasource] = useState<DatasourceObject>(() => ({
+    ...propsDatasource,
+    owners: propsDatasource.owners.map(owner => ({
+      value: owner.value || owner.id,
+      label: owner.label || `${owner.first_name} ${owner.last_name}`,
+    })),
+    metrics: propsDatasource.metrics?.map(metric => {
+      const {
+        certified_by: certifiedByMetric,
+        certification_details: certificationDetails,
+      } = metric;
+      const {
+        certification: {
+          details = undefined,
+          certified_by: certifiedBy = undefined,
+        } = {},
+        warning_markdown: warningMarkdown,
+      } = JSON.parse(metric.extra || '{}') || {};
+      return {
+        ...metric,
+        certification_details: certificationDetails || details,
+        warning_markdown: warningMarkdown || '',
+        certified_by: certifiedBy || certifiedByMetric,
+      };
+    }),
+  }));
 
-  private abortControllers: AbortControllers;
+  const [errors, setErrors] = useState<string[]>([]);
+  const [isSqla] = useState(
+    propsDatasource.datasource_type === 'table' ||
+      propsDatasource.type === 'table',
+  );
+  const [isEditMode, setIsEditMode] = useState(false);
+  const [databaseColumns, setDatabaseColumns] = useState<Column[]>(
+    propsDatasource.columns.filter(col => !col.expression),
+  );
+  const [calculatedColumns, setCalculatedColumns] = useState<Column[]>(
+    propsDatasource.columns.filter(col => !!col.expression),
+  );
+  const [folders, setFolders] = useState<DatasourceFolder[]>(
+    propsDatasource.folders || [],
+  );
+  const [folderCount, setFolderCount] = useState(() => {
+    const savedFolders = propsDatasource.folders || [];
+    const savedCount = countAllFolders(savedFolders);
+    const hasDefaultsSaved = savedFolders.some(f => isDefaultFolder(f.uuid));
+    return savedCount + (hasDefaultsSaved ? 0 : DEFAULT_FOLDERS_COUNT);
+  });
+  const [metadataLoading, setMetadataLoading] = useState(false);
+  const [activeTabKey, setActiveTabKey] = useState(TABS_KEYS.SOURCE);
+  const [datasourceType, setDatasourceType] = useState(
+    propsDatasource.sql
+      ? DATASOURCE_TYPES.virtual.key
+      : DATASOURCE_TYPES.physical.key,
+  );
+  const [usageCharts, setUsageCharts] = useState<ChartUsageData[]>([]);
+  const [usageChartsCount, setUsageChartsCount] = useState(0);
+  const [metricSearchTerm, setMetricSearchTerm] = useState('');
+  const [columnSearchTerm, setColumnSearchTerm] = useState('');
+  const [calculatedColumnSearchTerm, setCalculatedColumnSearchTerm] =
+    useState('');
+
+  const findDuplicates = useCallback(
+    <T,>(arr: T[], accessor: (obj: T) => string): string[] => {
+      const seen: Record<string, null> = {};
+      const dups: string[] = [];
+      arr.forEach((obj: T) => {
+        const item = accessor(obj);
+        if (item in seen) {
+          dups.push(item);
+        } else {
+          seen[item] = null;
+        }
+      });
+      return dups;
+    },
+    [],
+  );
 
-  static defaultProps = {
-    onChange: () => {},
-    setIsEditing: () => {},
-  };
+  const validate = useCallback(
+    (callback: (validationErrors: string[]) => void) => {
+      let validationErrors: string[] = [];
+      let dups: string[];
 
-  constructor(props: DatasourceEditorProps) {
-    super(props);
-    this.state = {
-      datasource: {
-        ...props.datasource,
-        owners: props.datasource.owners.map(owner => {
-          const ownerName =
-            owner.label || `${owner.first_name} ${owner.last_name}`;
-          return {
-            value: owner.value || owner.id,
-            label: OwnerSelectLabel({
-              name: typeof ownerName === 'string' ? ownerName : '',
-              email: owner.email,
-            }),
-            [OWNER_TEXT_LABEL_PROP]:
-              typeof ownerName === 'string' ? ownerName : '',
-            [OWNER_EMAIL_PROP]: owner.email ?? '',
-          };
-        }),
-        metrics: props.datasource.metrics?.map(metric => {
-          const {
-            certified_by: certifiedByMetric,
-            certification_details: certificationDetails,
-          } = metric;
-          const {
-            certification: {
-              details = undefined,
-              certified_by: certifiedBy = undefined,
-            } = {},
-            warning_markdown: warningMarkdown,
-          } = JSON.parse(metric.extra || '{}') || {};
-          return {
-            ...metric,
-            certification_details: certificationDetails || details,
-            warning_markdown: warningMarkdown || '',
-            certified_by: certifiedBy || certifiedByMetric,
-          };
-        }),
-      },
-      errors: [],
-      isSqla:
-        props.datasource.datasource_type === 'table' ||
-        props.datasource.type === 'table',
-      isEditMode: false,
-      databaseColumns: props.datasource.columns.filter(col => !col.expression),
-      calculatedColumns: props.datasource.columns.filter(
-        col => !!col.expression,
-      ),
-      folders: props.datasource.folders || [],
-      folderCount: (() => {
-        const savedFolders = props.datasource.folders || [];
-        const savedCount = countAllFolders(savedFolders);
-        const hasDefaultsSaved = savedFolders.some(f =>
-          isDefaultFolder(f.uuid),
-        );
-        return savedCount + (hasDefaultsSaved ? 0 : DEFAULT_FOLDERS_COUNT);
-      })(),
-      metadataLoading: false,
-      activeTabKey: TABS_KEYS.SOURCE,
-      datasourceType: props.datasource.sql
-        ? DATASOURCE_TYPES.virtual.key
-        : DATASOURCE_TYPES.physical.key,
-      usageCharts: [],
-      usageChartsCount: 0,
-      metricSearchTerm: '',
-      columnSearchTerm: '',
-      calculatedColumnSearchTerm: '',
-    };
+      // Looking for duplicate column_name
+      dups = findDuplicates(datasource.columns, obj => obj.column_name);

Review Comment:
   **Suggestion:** Column duplicate validation is reading `datasource.columns`, 
but column edits are now stored in `databaseColumns`/`calculatedColumns` and 
`datasource.columns` is not kept in sync during editing. This can miss real 
duplicates (or report stale ones), so invalid column states can pass validation 
and enable save incorrectly. Validate against the merged live column state 
instead of `datasource.columns`. [incorrect variable usage]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   - ❌ Dataset editor allows duplicate column names to be saved.
   - ⚠️ Save button not disabled despite structural column errors.
   - ⚠️ Front-end validation diverges from backend dataset constraints.
   ```
   </details>
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Open the dataset edit modal from Explore, which renders `DatasourceModal` 
and nests
   `DatasourceEditor`
   
(`superset-frontend/src/explore/components/controls/DatasourceControl/index.tsx:18-24`
 and
   
`superset-frontend/src/components/Datasource/DatasourceModal/index.tsx:25-32`).
   
   2. In `DatasourceEditor`, go to the "Columns" or "Calculated columns" tab; 
edits there
   update `databaseColumns` / `calculatedColumns` via `setColumns`
   (`DatasourceEditor.tsx:1096-1106`) but do not mutate `datasource.columns`, 
which was only
   initialized from props in state setup (`DatasourceEditor.tsx:864-889`).
   
   3. Trigger any change that causes validation to run (e.g. editing a column, 
which fires
   the `[databaseColumns, calculatedColumns]` effect calling 
`validateAndChange` at
   `DatasourceEditor.tsx:1110-1121`); inside `validate`, duplicate detection 
still reads
   `datasource.columns` (`DatasourceEditor.tsx:943-951`), so it checks the 
original columns,
   not the live edited ones, and will miss duplicate names created in the UI.
   
   4. The editor then calls `onChangeInternal`, which merges `databaseColumns` 
and
   `calculatedColumns` into `columns` and passes that to the parent via
   `onChange(newDatasource, validationErrors)` 
(`DatasourceEditor.tsx:999-1024`);
   `DatasourceModal` updates its `currentDatasource` and `errors` from this 
callback
   (`DatasourceModal/index.tsx:9-18`), but because validationErrors did not 
include the newly
   introduced duplicate column names, the "Save" button remains enabled
   (`DatasourceModal/index.tsx:53-62`), allowing an invalid duplicate-column 
dataset to be
   saved.
   ```
   </details>
   
   [Fix in 
Cursor](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=e56592802f5f465ab7dd96a432808c94&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
 | [Fix in VSCode 
Claude](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=e56592802f5f465ab7dd96a432808c94&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
   
   *(Use Cmd/Ctrl + Click for best experience)*
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** 
superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx
   **Line:** 949:949
   **Comment:**
        *Incorrect Variable Usage: Column duplicate validation is reading 
`datasource.columns`, but column edits are now stored in 
`databaseColumns`/`calculatedColumns` and `datasource.columns` is not kept in 
sync during editing. This can miss real duplicates (or report stale ones), so 
invalid column states can pass validation and enable save incorrectly. Validate 
against the merged live column state instead of `datasource.columns`.
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   Once fix is implemented, also check other comments on the same PR, and ask 
user if the user wants to fix the rest of the comments as well. if said yes, 
then fetch all the comments validate the correctness and implement a minimal fix
   ```
   </details>
   <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F39461&comment_hash=ba6e262112b60eb167947feaa5d3e3171eac24104cb5e835223cc06471533bbb&reaction=like'>👍</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F39461&comment_hash=ba6e262112b60eb167947feaa5d3e3171eac24104cb5e835223cc06471533bbb&reaction=dislike'>👎</a>



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to