Repository: TobikoData/sqlmesh
Pull request #2084 (2024-02-05 22:26:00)

ibis example project
This demonstrates how ibis can be embedded within a sqlmesh project. The project is largely the same as the default `init` project, with the addition of `ibis_full_model.py` which is a Python model that uses ibis to replicate the model created by `full_model.sql` 

CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/TobikoData/sqlmesh?pullRequest=2084) <br/>Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla-assistant.io/TobikoData/sqlmesh?pullRequest=2084) before we can accept your contribution.<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla-assistant.io/check/TobikoData/sqlmesh?pullRequest=2084) it.</sub>

Patch:
diff --git a/docs/development.md b/docs/development.md
index da8f451e..3ec5ff2c 100644
--- a/docs/development.md
+++ b/docs/development.md
@@ -24,6 +24,10 @@ Run more comprehensive tests that run on each commit:
 ```bash
 make slow-test
 ```
+Run Airflow tests that will run when PR is merged to main:
+```bash
+make airflow-docker-test-with-env
+```
 Run docs server:
 ```bash
 make docs-serve
diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md
index 40c2f64a..b1e1b465 100644
--- a/docs/reference/configuration.md
+++ b/docs/reference/configuration.md
@@ -114,7 +114,7 @@ Most parameters are specific to the connection engine `type` - see [below](#engi
 | ------------------- | --------------------------------------------------------------------------------------------------------------------------- | :--: | :------: |
 | `type`              | The engine type name, listed in engine-specific configuration pages below.                                                  | str  |    Y     |
 | `concurrent_tasks`  | The maximum number of concurrent tasks that will be run by SQLMesh. (Default: 4 for engines that support concurrent tasks.) | int  |    N     |
-| `register_comments` | Whether SQLMesh should register model comments with the SQL engine (if the engine supports it).                             | bool |    N     |
+| `register_comments` | Whether SQLMesh should register model comments with the SQL engine (if the engine supports it). (Default: `true`; `false` for Snowflake engine.)                            | bool |    N     |
 
 #### Engine-specific
 
diff --git a/examples/ibis/audits/.gitkeep b/examples/ibis/audits/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/ibis/audits/assert_positive_order_ids.sql b/examples/ibis/audits/assert_positive_order_ids.sql
new file mode 100644
index 00000000..6e5fecad
--- /dev/null
+++ b/examples/ibis/audits/assert_positive_order_ids.sql
@@ -0,0 +1,8 @@
+AUDIT (
+  name assert_positive_order_ids,
+);
+
+SELECT *
+FROM @this_model
+WHERE
+  item_id < 0
diff --git a/examples/ibis/config.yaml b/examples/ibis/config.yaml
new file mode 100644
index 00000000..3898e88d
--- /dev/null
+++ b/examples/ibis/config.yaml
@@ -0,0 +1,8 @@
+gateways:
+    my_gateway:
+        connection:
+            type: duckdb
+            catalogs:
+                local: 'data/local.duckdb'
+model_defaults:
+    dialect: 'duckdb'
\ No newline at end of file
diff --git a/examples/ibis/constants.py b/examples/ibis/constants.py
new file mode 100644
index 00000000..adf74780
--- /dev/null
+++ b/examples/ibis/constants.py
@@ -0,0 +1,3 @@
+import os
+
+DB_PATH = os.path.join(os.path.dirname(__file__), "data/local.duckdb")
diff --git a/examples/ibis/data/.keep b/examples/ibis/data/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/ibis/macros/.gitkeep b/examples/ibis/macros/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/ibis/macros/__init__.py b/examples/ibis/macros/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/ibis/models/.gitkeep b/examples/ibis/models/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/ibis/models/full_model.sql b/examples/ibis/models/full_model.sql
new file mode 100644
index 00000000..023b94cc
--- /dev/null
+++ b/examples/ibis/models/full_model.sql
@@ -0,0 +1,15 @@
+MODEL (
+  name ibis.full_model,
+  kind FULL,
+  cron '@daily',
+  grain item_id,
+  audits [assert_positive_order_ids],
+);
+
+SELECT
+  item_id,
+  count(distinct id) AS num_orders,
+FROM
+    ibis.incremental_model
+GROUP BY item_id
+ORDER BY item_id
diff --git a/examples/ibis/models/ibis_full_model_python.py b/examples/ibis/models/ibis_full_model_python.py
new file mode 100644
index 00000000..e81d41f3
--- /dev/null
+++ b/examples/ibis/models/ibis_full_model_python.py
@@ -0,0 +1,40 @@
+import typing as t
+from datetime import datetime
+
+import ibis  # type: ignore
+import pandas as pd
+from constants import DB_PATH  # type: ignore
+
+from sqlmesh import ExecutionContext, model
+from sqlmesh.core.model import FullKind
+
+
+@model(
+    "ibis.ibis_full_model_python",
+    kind=FullKind(),
+    columns={
+        "item_id": "int",
+        "num_orders": "int",
+    },
+    audits=["assert_positive_order_ids"],
+    description="This model uses ibis to generate and run a query",
+)
+def execute(
+    context: ExecutionContext,
+    start: datetime,
+    end: datetime,
+    execution_time: datetime,
+    **kwargs: t.Any,
+) -> pd.DataFrame:
+    # connect ibis to database
+    con = ibis.duckdb.connect(DB_PATH)
+
+    # retrieve table
+    incremental_model = con.table("incremental_model", schema="ibis")
+
+    # build query
+    count = incremental_model.id.nunique()
+    aggregate = incremental_model.group_by("item_id").aggregate(num_orders=count)
+    query = aggregate.order_by("item_id")
+
+    return query.to_pandas()
diff --git a/examples/ibis/models/ibis_full_model_sql.py b/examples/ibis/models/ibis_full_model_sql.py
new file mode 100644
index 00000000..86728b58
--- /dev/null
+++ b/examples/ibis/models/ibis_full_model_sql.py
@@ -0,0 +1,28 @@
+import ibis  # type: ignore
+from ibis.expr.operations import Namespace, UnboundTable  # type: ignore
+
+from sqlmesh.core.macros import MacroEvaluator
+from sqlmesh.core.model import model
+
+
+@model(
+    "ibis.ibis_full_model_sql",
+    is_sql=True,
+    kind="FULL",
+    audits=["assert_positive_order_ids"],
+    description="This model uses ibis to generate a SQL string",
+)
+def entrypoint(evaluator: MacroEvaluator) -> str:
+    # create table reference
+    incremental_model = UnboundTable(
+        name="incremental_model",
+        schema={"id": "int32", "item_id": "int32", "ds": "varchar"},
+        namespace=Namespace(database="local", schema="ibis"),
+    ).to_expr()
+
+    # build query
+    count = incremental_model.id.nunique()
+    aggregate = incremental_model.group_by("item_id").aggregate(num_orders=count)
+    query = aggregate.order_by("item_id")
+
+    return ibis.to_sql(query)
diff --git a/examples/ibis/models/incremental_model.sql b/examples/ibis/models/incremental_model.sql
new file mode 100644
index 00000000..7399c47b
--- /dev/null
+++ b/examples/ibis/models/incremental_model.sql
@@ -0,0 +1,18 @@
+MODEL (
+    name ibis.incremental_model,
+    kind INCREMENTAL_BY_TIME_RANGE (
+        time_column ds
+    ),
+    start '2020-01-01',
+    cron '@daily',
+    grain [id, ds]
+);
+
+SELECT
+    id,
+    item_id,
+    ds,
+FROM
+    ibis.seed_model
+WHERE
+    ds between @start_ds and @end_ds
diff --git a/examples/ibis/models/seed_model.sql b/examples/ibis/models/seed_model.sql
new file mode 100644
index 00000000..f47a2455
--- /dev/null
+++ b/examples/ibis/models/seed_model.sql
@@ -0,0 +1,12 @@
+MODEL (
+    name ibis.seed_model,
+    kind SEED (
+        path '../seeds/seed_data.csv'
+    ),
+    columns (
+        id INTEGER,
+        item_id INTEGER,
+        ds TEXT
+    ),
+    grain [id, ds]
+);
diff --git a/examples/ibis/seeds/.gitkeep b/examples/ibis/seeds/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/ibis/seeds/seed_data.csv b/examples/ibis/seeds/seed_data.csv
new file mode 100644
index 00000000..ef6d930a
--- /dev/null
+++ b/examples/ibis/seeds/seed_data.csv
@@ -0,0 +1,8 @@
+id,item_id,ds
+1,2,2020-01-01
+2,1,2020-01-01
+3,3,2020-01-03
+4,1,2020-01-04
+5,1,2020-01-05
+6,1,2020-01-06
+7,1,2020-01-07
diff --git a/examples/ibis/tests/.gitkeep b/examples/ibis/tests/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/sqlmesh/core/config/feature_flag.py b/sqlmesh/core/config/feature_flag.py
new file mode 100644
index 00000000..b04dda2b
--- /dev/null
+++ b/sqlmesh/core/config/feature_flag.py
@@ -0,0 +1,9 @@
+from sqlmesh.utils.pydantic import PydanticModel
+
+
+class DbtFeatureFlag(PydanticModel):
+    scd_type_2_support: bool = True
+
+
+class FeatureFlag(PydanticModel):
+    dbt: DbtFeatureFlag = DbtFeatureFlag()
diff --git a/sqlmesh/core/config/root.py b/sqlmesh/core/config/root.py
index b95eaf0f..eead60c8 100644
--- a/sqlmesh/core/config/root.py
+++ b/sqlmesh/core/config/root.py
@@ -19,6 +19,7 @@ from sqlmesh.core.config.connection import (
     DuckDBConnectionConfig,
     connection_config_validator,
 )
+from sqlmesh.core.config.feature_flag import FeatureFlag
 from sqlmesh.core.config.format import FormatConfig
 from sqlmesh.core.config.gateway import GatewayConfig
 from sqlmesh.core.config.model import ModelDefaultsConfig
@@ -68,6 +69,7 @@ class Config(BaseConfig):
         log_limit: The default number of logs to keep.
         format: The formatting options for SQL code.
         ui: The UI configuration for SQLMesh.
+        feature_flags: Feature flags to enable/disable certain features.
     """
 
     gateways: t.Dict[str, GatewayConfig] = {"": GatewayConfig()}
@@ -103,6 +105,7 @@ class Config(BaseConfig):
     run: RunConfig = RunConfig()
     format: FormatConfig = FormatConfig()
     ui: UIConfig = UIConfig()
+    feature_flags: FeatureFlag = FeatureFlag()
 
     _FIELD_UPDATE_STRATEGY: t.ClassVar[t.Dict[str, UpdateStrategy]] = {
         "gateways": UpdateStrategy.KEY_UPDATE,
diff --git a/sqlmesh/core/engine_adapter/mssql.py b/sqlmesh/core/engine_adapter/mssql.py
index 875b8fa9..1988441e 100644
--- a/sqlmesh/core/engine_adapter/mssql.py
+++ b/sqlmesh/core/engine_adapter/mssql.py
@@ -4,6 +4,7 @@ from __future__ import annotations
 
 import typing as t
 
+import numpy as np
 import pandas as pd
 from pandas.api.types import is_datetime64_any_dtype  # type: ignore
 from sqlglot import exp
@@ -174,7 +175,7 @@ class MSSQLEngineAdapter(
                 columns_to_types_create = columns_to_types.copy()
                 self._convert_df_datetime(df, columns_to_types_create)
                 self.create_table(temp_table, columns_to_types_create)
-                rows: t.List[t.Tuple[t.Any, ...]] = list(df.itertuples(index=False, name=None))  # type: ignore
+                rows: t.List[t.Tuple[t.Any, ...]] = list(df.replace({np.nan: None}).itertuples(index=False, name=None))  # type: ignore
                 conn = self._connection_pool.get()
                 conn.bulk_copy(temp_table.sql(dialect=self.dialect), rows)
             return exp.select(*self._casted_columns(columns_to_types)).from_(temp_table)  # type: ignore
diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py
index ee0d9ae8..32a2efa5 100644
--- a/sqlmesh/core/model/definition.py
+++ b/sqlmesh/core/model/definition.py
@@ -1170,7 +1170,9 @@ class SeedModel(_SqlBasedModel):
             for column in date_or_time_columns:
                 df[column] = pd.to_datetime(df[column])
             df[bool_columns] = df[bool_columns].apply(lambda i: str_to_bool(str(i)))
-            df[string_columns] = df[string_columns].astype(str)
+            df[string_columns] = df[string_columns].mask(
+                cond=lambda x: x.notna(), other=df[string_columns].astype(str)  # type: ignore
+            )
             yield df
 
     def text_diff(self, other: Node) -> str:
diff --git a/sqlmesh/dbt/loader.py b/sqlmesh/dbt/loader.py
index b044a550..e06f3b70 100644
--- a/sqlmesh/dbt/loader.py
+++ b/sqlmesh/dbt/loader.py
@@ -17,6 +17,7 @@ from sqlmesh.core.macros import MacroRegistry
 from sqlmesh.core.model import Model, ModelCache
 from sqlmesh.dbt.basemodel import BMC, BaseModelConfig
 from sqlmesh.dbt.context import DbtContext
+from sqlmesh.dbt.model import ModelConfig
 from sqlmesh.dbt.profile import Profile
 from sqlmesh.dbt.project import Project
 from sqlmesh.dbt.target import TargetConfig
@@ -99,9 +100,19 @@ class DbtLoader(Loader):
             package_models: t.Dict[str, BaseModelConfig] = {**package.models, **package.seeds}
 
             for model in package_models.values():
+                if (
+                    not context.sqlmesh_config.feature_flags.dbt.scd_type_2_support
+                    and isinstance(model, ModelConfig)
+                    and model.model_kind(context).is_scd_type_2
+                ):
+                    logger.info(
+                        "Skipping loading Snapshot (SCD Type 2) models due to the feature flag disabling this feature"
+                    )
+                    continue
                 sqlmesh_model = cache.get_or_load_model(
                     model.path, loader=lambda: self._to_sqlmesh(model, context)
                 )
+
                 models[sqlmesh_model.fqn] = sqlmesh_model
 
         models.update(self._load_external_models())
diff --git a/sqlmesh/utils/git.py b/sqlmesh/utils/git.py
index 4634329d..9f9b7f27 100644
--- a/sqlmesh/utils/git.py
+++ b/sqlmesh/utils/git.py
@@ -11,13 +11,17 @@ class GitClient:
         self._work_dir = Path(repo)
 
     def list_untracked_files(self) -> t.List[Path]:
-        return self._execute_list_output(["ls-files", "--others", "--exclude-standard"])
+        return self._execute_list_output(
+            ["ls-files", "--others", "--exclude-standard"], self._work_dir
+        )
 
     def list_changed_files(self, target_branch: str = "main") -> t.List[Path]:
-        return self._execute_list_output(["diff", "--name-only", "--diff-filter=d", target_branch])
+        return self._execute_list_output(
+            ["diff", "--name-only", "--diff-filter=d", target_branch], self._git_root
+        )
 
-    def _execute_list_output(self, commands: t.List[str]) -> t.List[Path]:
-        return [(self._git_root / o).absolute() for o in self._execute(commands).split("\n") if o]
+    def _execute_list_output(self, commands: t.List[str], base_path: Path) -> t.List[Path]:
+        return [(base_path / o).absolute() for o in self._execute(commands).split("\n") if o]
 
     def _execute(self, commands: t.List[str]) -> str:
         result = subprocess.run(["git"] + commands, cwd=self._work_dir, stdout=subprocess.PIPE)
diff --git a/web/client/src/App.tsx b/web/client/src/App.tsx
index 5c620f5c..0ab118aa 100644
--- a/web/client/src/App.tsx
+++ b/web/client/src/App.tsx
@@ -77,11 +77,11 @@ export default function App(): JSX.Element {
         setPlanAction(
           new ModelPlanAction({ value: EnumPlanAction.RunningTask }),
         )
+      } else {
+        void getEnvironments().then(({ data }) => updateEnviroments(data))
       }
     })
 
-    void getEnvironments().then(({ data }) => updateEnviroments(data))
-
     return () => {
       channelPlanOverview.unsubscribe()
       channelPlanApply.unsubscribe()
diff --git a/web/client/src/library/components/editor/Editor.tsx b/web/client/src/library/components/editor/Editor.tsx
index a99bf86f..31981253 100644
--- a/web/client/src/library/components/editor/Editor.tsx
+++ b/web/client/src/library/components/editor/Editor.tsx
@@ -20,6 +20,8 @@ import { type KeyBinding } from '@codemirror/view'
 import { useStoreContext } from '@context/context'
 import { useIDE } from '~/library/pages/ide/context'
 import { ModelDirectory } from '@models/directory'
+import { type ModelSQLMeshModel } from '@models/sqlmesh-model'
+import { type Column } from '@api/client'
 
 function Editor(): JSX.Element {
   const tab = useStoreEditor(s => s.tab)
@@ -70,15 +72,23 @@ function EditorMain({ tab }: { tab: EditorTab }): JSX.Element {
 
   const { setManuallySelectedColumn } = useLineageFlow()
 
+  const handleModelClick = useCallback(
+    function (model: ModelSQLMeshModel) {
+      setSelectedFile(files.get(model.path))
+    },
+    [files],
+  )
+  const handleModelColumn = useCallback(function (
+    model: ModelSQLMeshModel,
+    column: Column,
+  ) {
+    setManuallySelectedColumn([model, column])
+  }, [])
   const defaultKeymapsEditorTab = useDefaultKeymapsEditorTab()
   const modelExtensions = useSQLMeshModelExtensions(
     tab.file.path,
-    model => {
-      setSelectedFile(files.get(model.path))
-    },
-    (model, column) => {
-      setManuallySelectedColumn([model, column])
-    },
+    handleModelClick,
+    handleModelColumn,
   )
 
   const [isOpenInspector, setIsOpenInspector] = useState(false)
@@ -258,6 +268,7 @@ function EditorMain({ tab }: { tab: EditorTab }): JSX.Element {
                 dialect={tab.dialect}
                 keymaps={customSQLKeymaps}
                 content={tab.file.content}
+                extensions={modelExtensions}
                 onChange={updateFileContent}
               />
             )}
diff --git a/web/client/src/library/components/editor/EditorCode.tsx b/web/client/src/library/components/editor/EditorCode.tsx
index 81cf9334..c9609d25 100644
--- a/web/client/src/library/components/editor/EditorCode.tsx
+++ b/web/client/src/library/components/editor/EditorCode.tsx
@@ -109,13 +109,40 @@ function CodeEditorDefault({
       ),
     [keymaps],
   )
+  const allModels = useMemo(() => Array.from(models.values()), [models])
+  const allModelsNames = useMemo(
+    () =>
+      allModels.map(model => ({
+        label: model.name,
+        type: 'model',
+      })),
+    [allModels],
+  )
+  const allColumnsNames = useMemo(
+    () =>
+      Array.from(
+        new Set(
+          allModels.flatMap(model => model.columns.map(column => column.name)),
+        ),
+      ).map(name => ({
+        label: name,
+        type: 'column',
+      })),
+    [allModels],
+  )
 
   const extensionsAll = useMemo(() => {
     return [
       ...extensionsDefault,
       extensions,
       type === EnumFileExtensions.SQL &&
-        SQLMeshDialect(models, dialectOptions, dialectsTitles),
+        SQLMeshDialect(
+          models,
+          allModelsNames,
+          allColumnsNames,
+          dialectOptions,
+          dialectsTitles,
+        ),
       extensionKeymap,
     ]
       .filter(Boolean)
@@ -128,6 +155,8 @@ function CodeEditorDefault({
     extensionsDefault,
     extensions,
     extensionKeymap,
+    allModelsNames,
+    allColumnsNames,
   ])
 
   useEffect(() => {
diff --git a/web/client/src/library/components/editor/EditorPreview.tsx b/web/client/src/library/components/editor/EditorPreview.tsx
index 9118e430..94edfbd1 100644
--- a/web/client/src/library/components/editor/EditorPreview.tsx
+++ b/web/client/src/library/components/editor/EditorPreview.tsx
@@ -61,6 +61,9 @@ export default function EditorPreview({
   const model = models.get(tab.file.path)
   const showLineage =
     isFalse(tab.file.isEmpty) && isNotNil(model) && isModel(tab.file.path)
+  const showErrors =
+    errors.size > 0 &&
+    ([previewTable, previewDiff].some(isNotNil) || showLineage)
 
   const tabs: string[] = useMemo(
     () =>
@@ -69,13 +72,21 @@ export default function EditorPreview({
         isNotNil(previewQuery) && EnumEditorPreviewTabs.Query,
         showLineage && EnumEditorPreviewTabs.Lineage,
         isNotNil(previewDiff) && EnumEditorPreviewTabs.Diff,
-        errors.size > 0 && EnumEditorPreviewTabs.Errors,
+        showErrors && EnumEditorPreviewTabs.Errors,
       ].filter(Boolean) as string[],
-    [tab.id, previewTable, previewQuery, previewDiff, showLineage, errors],
+    [
+      tab.id,
+      previewTable,
+      previewQuery,
+      previewDiff,
+      showLineage,
+      errors,
+      showErrors,
+    ],
   )
 
   useEffect(() => {
-    if (errors.size > 0) {
+    if (showErrors) {
       setActiveTabIndex(tabs.indexOf(EnumEditorPreviewTabs.Errors))
     } else if (isNotNil(previewDiff)) {
       setActiveTabIndex(tabs.indexOf(EnumEditorPreviewTabs.Diff))
@@ -84,7 +95,15 @@ export default function EditorPreview({
     } else if (showLineage) {
       setActiveTabIndex(tabs.indexOf(EnumEditorPreviewTabs.Lineage))
     }
-  }, [tabs, previewTable, previewQuery, previewDiff, showLineage, errors])
+  }, [
+    tabs,
+    previewTable,
+    previewQuery,
+    previewDiff,
+    showLineage,
+    errors,
+    showErrors,
+  ])
 
   return (
     <div
@@ -182,7 +201,7 @@ export default function EditorPreview({
                 />
               </Tab.Panel>
             )}
-            {errors.size > 0 && (
+            {showErrors && (
               <Tab.Panel
                 unmount={false}
                 className="w-full h-full ring-white ring-opacity-60 ring-offset-2 ring-offset-blue-400 focus:outline-none focus:ring-2 py-2"
diff --git a/web/client/src/library/components/editor/extensions/SQLMeshDialect.ts b/web/client/src/library/components/editor/extensions/SQLMeshDialect.ts
index 475e3ea9..7c37f964 100644
--- a/web/client/src/library/components/editor/extensions/SQLMeshDialect.ts
+++ b/web/client/src/library/components/editor/extensions/SQLMeshDialect.ts
@@ -1,3 +1,4 @@
+import { type Model } from '@api/client'
 import {
   completeFromList,
   type CompletionContext,
@@ -5,7 +6,6 @@ import {
 } from '@codemirror/autocomplete'
 import { keywordCompletionSource, SQLDialect } from '@codemirror/lang-sql'
 import { LanguageSupport } from '@codemirror/language'
-import { type Model } from '~/api/client'
 import { isArrayEmpty, isNil, isNotNil, isStringEmptyOrNil } from '~/utils'
 import { sqlglotWorker } from '~/workers'
 
@@ -14,12 +14,16 @@ const WHITE_SPACE = ' '
 
 export type ExtensionSQLMeshDialect = (
   models: Map<string, Model>,
+  allModelsNames: Array<{ label: string; type: string }>,
+  allColumnsNames: Array<{ label: string; type: string }>,
   options?: { types: string; keywords: string },
   dialects?: string[],
 ) => LanguageSupport
 
 export const SQLMeshDialect: ExtensionSQLMeshDialect = function SQLMeshDialect(
   models,
+  allModelsNames,
+  allColumnsNames,
   options = { types: '', keywords: '' },
   dialects,
 ): LanguageSupport {
@@ -34,20 +38,6 @@ export const SQLMeshDialect: ExtensionSQLMeshDialect = function SQLMeshDialect(
     keywords: (SQLKeywords + WHITE_SPACE + SQLMeshKeywords).toLowerCase(),
     types: (SQLTypes + WHITE_SPACE + SQLMeshTypes).toLowerCase(),
   })
-  const allModels = Array.from(new Set(models.values()))
-  const modelColumns = Array.from(
-    new Set(
-      allModels.map(model => model.columns.map(column => column.name)).flat(),
-    ),
-  )
-  const modelNames: Completion[] = allModels.map(model => ({
-    label: model.name,
-    type: 'model',
-  }))
-  const columnNames: Completion[] = modelColumns.map(name => ({
-    label: name,
-    type: 'column',
-  }))
 
   SQLMeshDialectCleanUp()
 
@@ -67,11 +57,11 @@ export const SQLMeshDialect: ExtensionSQLMeshDialect = function SQLMeshDialect(
         const dot = ctx.matchBefore(/[A-Za-z0-9_.]*\.(\w+)?\s*$/i)
 
         if (isNotNil(dot)) {
-          let options = columnNames
+          let options = allColumnsNames
           const blocks = dot.text.split('.')
           const text = blocks.pop()
           const maybeModelName = blocks.join('.')
-          const maybeModel = models.get(maybeModelName)
+          const maybeModel = models.get(maybeModelName) ?? models.get(dot.text)
 
           if (isNotNil(maybeModel)) {
             options = maybeModel.columns.map(column => ({
@@ -81,7 +71,7 @@ export const SQLMeshDialect: ExtensionSQLMeshDialect = function SQLMeshDialect(
           }
 
           return {
-            from: isStringEmptyOrNil(text) ? dot.to : dot.to - text.length,
+            from: isStringEmptyOrNil(text) ? dot.to : dot.to - dot.text.length,
             to: dot.to,
             options,
           }
@@ -94,9 +84,6 @@ export const SQLMeshDialect: ExtensionSQLMeshDialect = function SQLMeshDialect(
         const keywordKind = matchWordWithSpacesAfter(ctx, 'kind')
         const keywordDialect = matchWordWithSpacesAfter(ctx, 'dialect')
         const keywordModel = matchWordWithSpacesAfter(ctx, 'model')
-        const keywordFrom = matchWordWithSpacesAfter(ctx, 'from')
-        const keywordJoin = matchWordWithSpacesAfter(ctx, 'join')
-        const keywordSelect = matchWordWithSpacesAfter(ctx, 'select')
 
         const text = ctx.state.doc.toJSON().join('\n')
         const matchModels = text.match(/MODEL \(([\s\S]+?)\);/g) ?? []
@@ -121,27 +108,48 @@ export const SQLMeshDialect: ExtensionSQLMeshDialect = function SQLMeshDialect(
           }
 
           return completeFromList(suggestions ?? [])(ctx)
-        } else {
-          let suggestions: Completion[] = []
-
-          if (isNotNil(keywordModel) && isArrayEmpty(matchModels)) {
-            suggestions = [
-              {
-                label: 'model',
-                type: 'keyword',
-                apply: 'MODEL (\n\r);',
-              },
-            ]
-          } else if (isNotNil(keywordSelect)) {
-            suggestions = columnNames
-          } else if (isNotNil(keywordFrom) || isNotNil(keywordJoin)) {
-            suggestions = modelNames
-          }
+        }
+
+        let suggestions: Completion[] = []
+
+        if (isNotNil(keywordModel) && isArrayEmpty(matchModels)) {
+          suggestions = [
+            {
+              label: 'model',
+              type: 'keyword',
+              apply: 'MODEL (\n\r);',
+            },
+          ]
+        }
+
+        if (maybeSuggestion(allModelsNames, word.text)) {
+          suggestions.push(...allModelsNames)
+        }
 
-          return isArrayEmpty(suggestions)
-            ? keywordCompletionSource(lang)(ctx)
-            : completeFromList(suggestions)(ctx)
+        if (maybeSuggestion(allColumnsNames, word.text)) {
+          suggestions.push(...allColumnsNames)
         }
+
+        const wordLastChar = word.text.slice(-1)
+        const isUpperCase = wordLastChar === wordLastChar.toUpperCase()
+
+        if (isUpperCase) {
+          suggestions = suggestions.map(suggestion => ({
+            ...suggestion,
+            label: suggestion.label.toUpperCase(),
+          }))
+        }
+
+        const source = isArrayEmpty(suggestions)
+          ? keywordCompletionSource(lang, isUpperCase)(ctx)
+          : {
+              from: word.from,
+              to: word.to,
+              options: suggestions,
+              validFor: /^\w*$/,
+            }
+
+        return source
       },
     }),
   ])
@@ -241,3 +249,10 @@ function matchWordWithSpacesAfter(
 }> {
   return ctx.matchBefore(new RegExp(`${word}\\s*(?=\\S)([^ ]+)`, 'i'))
 }
+
+function maybeSuggestion(suggestions: Completion[], word: string): boolean {
+  return (
+    word.length > 1 &&
+    suggestions.some(suggestion => suggestion.label.includes(word))
+  )
+}
diff --git a/web/client/src/library/components/editor/extensions/SQLMeshModel.ts b/web/client/src/library/components/editor/extensions/SQLMeshModel.ts
index 7d45edbd..389801cc 100644
--- a/web/client/src/library/components/editor/extensions/SQLMeshModel.ts
+++ b/web/client/src/library/components/editor/extensions/SQLMeshModel.ts
@@ -11,9 +11,9 @@ import { getDecorations } from './help'
 
 export default function SQLMeshModel(
   models: Map<string, ModelSQLMeshModel>,
-  model: ModelSQLMeshModel,
   columns: Set<string>,
   isActionMode: boolean,
+  model?: ModelSQLMeshModel,
 ): Extension {
   return ViewPlugin.fromClass(
     class SQLMeshModelView {
@@ -22,9 +22,9 @@ export default function SQLMeshModel(
         this.decorations = getDecorations(
           models,
           view,
-          model,
           columns,
           isActionMode,
+          model,
         )
       }
 
@@ -32,9 +32,9 @@ export default function SQLMeshModel(
         this.decorations = getDecorations(
           models,
           viewUpdate.view,
-          model,
           columns,
           isActionMode,
+          model,
         )
       }
     },
diff --git a/web/client/src/library/components/editor/extensions/help.ts b/web/client/src/library/components/editor/extensions/help.ts
index 0a08ecb3..66ba3f94 100644
--- a/web/client/src/library/components/editor/extensions/help.ts
+++ b/web/client/src/library/components/editor/extensions/help.ts
@@ -52,14 +52,15 @@ export function findColumn(
 export function getDecorations(
   models: Map<string, ModelSQLMeshModel>,
   view: EditorView,
-  model: ModelSQLMeshModel,
   columns: Set<string>,
   isActionMode: boolean,
+  model?: ModelSQLMeshModel,
 ): DecorationSet {
   const ranges: any = []
   const columnNames = Array.from(columns)
-  const modelNames = Array.from(new Set(models.values())).map(m => m.name)
-  const modelColumns = model.columns.map(c => c.name.toLowerCase())
+  const allModels = Array.from(new Set(models.values()))
+  const modelNames = allModels.map(m => m.name)
+  const modelColumns = model?.columns.map(c => c.name.toLowerCase()) ?? []
   const validLeftCharColumn = new Set(['.', '(', '[', ' ', '\n'])
   const validRightCharColumn = new Set([':', ')', ',', ']', ' ', '\n'])
 
@@ -73,14 +74,14 @@ export function getDecorations(
           createMarkDecorationModel({
             model: name,
             isActionMode,
-            isActiveModel: model.name === name,
+            isActiveModel: model?.name === name,
           }).range(from, to),
         )
       },
     )
 
     getMarkDecorations(
-      columnNames,
+      isNil(model) ? columnNames : modelColumns,
       view.state.doc.sliceString(range.from, range.to),
       range,
       ({ from, to, key }) => {
diff --git a/web/client/src/library/components/editor/hooks.ts b/web/client/src/library/components/editor/hooks.ts
index 4db8cb4a..7e6508ad 100644
--- a/web/client/src/library/components/editor/hooks.ts
+++ b/web/client/src/library/components/editor/hooks.ts
@@ -4,7 +4,7 @@ import { useLineageFlow } from '@components/graph/context'
 import { useStoreEditor } from '@context/editor'
 import { type ModelSQLMeshModel } from '@models/sqlmesh-model'
 import { type Column } from '~/api/client'
-import { isFalse, isNil, isNotNil } from '@utils/index'
+import { isFalse, isNil, isNotNil, isObjectEmpty } from '@utils/index'
 import { useMemo, useState } from 'react'
 import { events, HoverTooltip, SQLMeshModel } from './extensions'
 import { findModel, findColumn } from './extensions/help'
@@ -69,14 +69,20 @@ function useSQLMeshModelExtensions(
   const [isActionMode, setIsActionMode] = useState(false)
 
   const extensions = useMemo(() => {
-    const columns = isNil(lineage)
-      ? new Set<string>()
-      : new Set(
-          Object.keys(lineage)
-            .map(modelName => models.get(modelName)?.columns.map(c => c.name))
-            .flat()
-            .filter(Boolean) as string[],
-        )
+    const columns =
+      isNil(lineage) || isObjectEmpty(lineage)
+        ? new Set<string>(
+            Array.from(new Set(models.values())).flatMap(m =>
+              m.columns.map(c => c.name),
+            ),
+          )
+        : new Set(
+            Object.keys(lineage)
+              .flatMap(
+                modelName => models.get(modelName)?.columns.map(c => c.name),
+              )
+              .filter(Boolean) as string[],
+          )
 
     function handleEventModelClick(event: MouseEvent): void {
       if (event.metaKey) {
@@ -116,7 +122,7 @@ function useSQLMeshModelExtensions(
       }),
       isNotNil(handleModelClick) && events({ click: handleEventModelClick }),
       isNotNil(handleModelColumn) && events({ click: handleEventlColumnClick }),
-      isNotNil(model) && SQLMeshModel(models, model, columns, isActionMode),
+      SQLMeshModel(models, columns, isActionMode, model),
     ].filter(Boolean) as Extension[]
   }, [handleModelClick, handleModelColumn, model, models, files, isActionMode])
 
diff --git a/web/client/src/library/components/graph/ModelLineage.tsx b/web/client/src/library/components/graph/ModelLineage.tsx
index 104e86e0..70ff077b 100644
--- a/web/client/src/library/components/graph/ModelLineage.tsx
+++ b/web/client/src/library/components/graph/ModelLineage.tsx
@@ -31,7 +31,6 @@ import {
   getUpdatedNodes,
   getUpdatedEdges,
   createGraphLayout,
-  getModelAncestors,
 } from './help'
 import ModelLineageSearch from './ModelLineageSearch'
 import { Popover } from '@headlessui/react'
@@ -121,10 +120,11 @@ export default function ModelLineage({
 
   function handleLineageWorkerMessage(e: MessageEvent): void {
     if (e.data.topic === 'lineage') {
-      setLineage(e.data.payload.lineage)
-      setNodeConnections(e.data.payload.nodesConnections)
       setIsMergingModels(false)
+      setNodeConnections(e.data.payload.nodesConnections)
+      setLineage(e.data.payload.lineage)
     }
+
     if (e.data.topic === 'error') {
       handleError?.(e.data.error)
       setIsMergingModels(false)
@@ -137,7 +137,7 @@ export default function ModelLineage({
         <div className="absolute top-0 left-0 z-10 w-full h-full bg-theme flex justify-center items-center">
           <Loading className="inline-block">
             <Spinner className="w-3 h-3 border border-neutral-10 mr-4" />
-            <h3 className="text-md">
+            <h3 className="text-md whitespace-nowrap">
               {isFetching ? "Loading Model's Lineage..." : "Merging Model's..."}
             </h3>
           </Loading>
@@ -329,7 +329,7 @@ function ModelColumnLineage(): JSX.Element {
         <div className="absolute top-0 left-0 z-10 bg-theme flex justify-center items-center w-full h-full">
           <Loading className="inline-block">
             <Spinner className="w-3 h-3 border border-neutral-10 mr-4" />
-            <h3 className="text-md">Building Lineage...</h3>
+            <h3 className="text-md whitespace-nowrap">Building Lineage...</h3>
           </Loading>
         </div>
       )}
@@ -365,9 +365,7 @@ function ModelColumnLineage(): JSX.Element {
 
 function GraphControls({ nodes = [] }: { nodes: Node[] }): JSX.Element {
   const {
-    models,
     withColumns,
-    lineage,
     mainNode,
     selectedNodes,
     withConnected,
@@ -375,7 +373,6 @@ function GraphControls({ nodes = [] }: { nodes: Node[] }): JSX.Element {
     withSecondary,
     hasBackground,
     activeNodes,
-    connectedNodes,
     highlightedNodes,
     setSelectedNodes,
     setWithColumns,
@@ -387,22 +384,6 @@ function GraphControls({ nodes = [] }: { nodes: Node[] }): JSX.Element {
 
   const lineageInfoTrigger = useRef<HTMLButtonElement>(null)
 
-  const currentModels = useMemo(() => {
-    if (isNil(mainNode) || isNil(lineage)) return []
-
-    const ancestors = getModelAncestors(lineage, mainNode)
-
-    return Object.keys(lineage)
-      .map(model => ({
-        name: model,
-        displayName: models.get(model)?.displayName ?? decodeURI(model),
-        description: `${
-          ancestors.includes(model) ? 'Upstream' : 'Downstream'
-        } | ${connectedNodes.has(model) ? 'Directly' : 'Indirectly'} Connected`,
-      }))
-      .filter(Boolean) as Array<{ name: string; description: string }>
-  }, [lineage, mainNode])
-
   const highlightedNodeModels = useMemo(
     () => Object.values(highlightedNodes ?? {}).flat(),
     [highlightedNodes],
@@ -445,10 +426,7 @@ function GraphControls({ nodes = [] }: { nodes: Node[] }): JSX.Element {
         </div>
       </div>
       <div className="flex w-full justify-end items-center">
-        <ModelLineageSearch
-          currentModels={currentModels}
-          handleSelect={handleSelect}
-        />
+        <ModelLineageSearch handleSelect={handleSelect} />
         <ListboxShow
           options={{
             Background: setHasBackground,
diff --git a/web/client/src/library/components/graph/ModelLineageSearch.tsx b/web/client/src/library/components/graph/ModelLineageSearch.tsx
index 54c72bdf..fb90a9c4 100644
--- a/web/client/src/library/components/graph/ModelLineageSearch.tsx
+++ b/web/client/src/library/components/graph/ModelLineageSearch.tsx
@@ -1,13 +1,14 @@
-import { useState } from 'react'
+import { useEffect, useState } from 'react'
 import clsx from 'clsx'
 import { MagnifyingGlassIcon, XCircleIcon } from '@heroicons/react/24/solid'
 import SearchList from '@components/search/SearchList'
 import { EnumSize, EnumVariant } from '~/types/enum'
-
+import { useLineageFlow } from './context'
 import { Button, EnumButtonShape } from '../button/Button'
+import { isArrayNotEmpty, isNil } from '@utils/index'
+import { getModelAncestors } from './help'
 
 interface ModelLineageSearchProps {
-  currentModels: Array<{ name: string; description: string }>
   handleSelect: ({
     name,
     description,
@@ -17,11 +18,24 @@ interface ModelLineageSearchProps {
   }) => void
 }
 
+interface ModelLineageSearchItem {
+  name: string
+  displayName: string
+  description: string
+}
+
 export default function ModelLineageSearch({
-  currentModels,
   handleSelect,
 }: ModelLineageSearchProps): JSX.Element {
+  const { models, lineage, mainNode, connectedNodes } = useLineageFlow()
+
   const [showSearchInput, setShowSearchInput] = useState(false)
+  const [list, setList] = useState<ModelLineageSearchItem[]>([])
+  const [isLoading, setIsLoading] = useState(false)
+
+  useEffect(() => {
+    setList([])
+  }, [mainNode, models, lineage])
 
   function showSearch(): void {
     // TODO: accessibility- also focus the search input after it is shown
@@ -33,6 +47,38 @@ export default function ModelLineageSearch({
     setShowSearchInput(false)
   }
 
+  // Load the list of models lazily, only when user starts typing
+  function handleInput(value: string): void {
+    if (
+      value.length < 1 ||
+      isArrayNotEmpty(list) ||
+      isNil(mainNode) ||
+      isNil(lineage)
+    )
+      return
+
+    setIsLoading(true)
+
+    // If takes a while to load the list of models
+    // it insures that the loading spinner is shown
+    setTimeout(() => {
+      const ancestors = Array.from(getModelAncestors(lineage, mainNode))
+
+      setList(
+        Object.keys(lineage).map(model => ({
+          name: model,
+          displayName: models.get(model)?.displayName ?? decodeURI(model),
+          description: `${
+            ancestors.includes(model) ? 'Upstream' : 'Downstream'
+          } | ${
+            connectedNodes.has(model) ? 'Directly' : 'Indirectly'
+          } Connected`,
+        })),
+      )
+      setIsLoading(false)
+    }, 300)
+  }
+
   return (
     <div
       className={clsx(
@@ -55,8 +101,8 @@ export default function ModelLineageSearch({
       >
         <MagnifyingGlassIcon className="w-3 h-3 text-primary-500" />
       </Button>
-      <SearchList<{ name: string; description: string }>
-        list={currentModels}
+      <SearchList<ModelLineageSearchItem>
+        list={list}
         placeholder="Find"
         searchBy="displayName"
         displayBy="displayName"
@@ -65,11 +111,13 @@ export default function ModelLineageSearch({
         showIndex={false}
         size={EnumSize.sm}
         onSelect={handleSelect}
+        isLoading={isLoading}
         className={clsx(
           'w-full @sm:min-w-[12rem] @[40rem]:flex',
           showSearchInput ? 'flex max-w-none' : 'hidden max-w-[20rem]',
         )}
         isFullWidth={true}
+        onInput={handleInput}
       />
       <button
         className={clsx(
diff --git a/web/client/src/library/components/graph/help.ts b/web/client/src/library/components/graph/help.ts
index 226bc8e5..bf9e0c9c 100644
--- a/web/client/src/library/components/graph/help.ts
+++ b/web/client/src/library/components/graph/help.ts
@@ -502,18 +502,18 @@ function getLineageIndex(lineage: Record<string, Lineage> = {}): string {
 function getModelAncestors(
   lineage: Record<string, Lineage> = {},
   name: string,
-): string[] {
+  output = new Set<string>(),
+): Set<string> {
   const model = lineage[name]
+  const models = model?.models ?? []
 
-  if (isNil(model) || isNil(model.models)) return []
-
-  const models = new Set(model.models)
+  for (const modelName of models) {
+    if (output.has(modelName)) continue
 
-  model.models.forEach(modelName => {
-    getModelAncestors(lineage, modelName).forEach(m => models.add(m))
-  })
+    getModelAncestors(lineage, modelName, output).add(modelName)
+  }
 
-  return Array.from(models)
+  return output
 }
 
 function getActiveNodes(
diff --git a/web/client/src/library/components/plan/PlanActions.tsx b/web/client/src/library/components/plan/PlanActions.tsx
index 6a31778d..4a34cb1b 100644
--- a/web/client/src/library/components/plan/PlanActions.tsx
+++ b/web/client/src/library/components/plan/PlanActions.tsx
@@ -8,6 +8,7 @@ import { useStorePlan } from '@context/plan'
 import { useStoreContext } from '@context/context'
 import { AddEnvironment, SelectEnvironemnt } from '~/library/pages/root/Page'
 import { Transition } from '@headlessui/react'
+import { useNavigate } from 'react-router-dom'
 
 export default function PlanActions({
   run,
@@ -20,6 +21,8 @@ export default function PlanActions({
   cancel: () => void
   reset: () => void
 }): JSX.Element {
+  const navigate = useNavigate()
+
   const environment = useStoreContext(s => s.environment)
   const environments = useStoreContext(s => s.environments)
   const addConfirmation = useStoreContext(s => s.addConfirmation)
@@ -42,6 +45,12 @@ export default function PlanActions({
     cancel()
   }
 
+  function handleGoBack(e: MouseEvent): void {
+    e.stopPropagation()
+
+    navigate(-1)
+  }
+
   function handleApply(e: MouseEvent): void {
     e.stopPropagation()
 
@@ -173,6 +182,8 @@ export default function PlanActions({
               onClick={
                 planAction.isRunning || planAction.isApplying
                   ? handleCancel
+                  : planAction.isDone
+                  ? handleGoBack
                   : handleReset
               }
               variant={
@@ -194,6 +205,8 @@ export default function PlanActions({
                   ? planCancel.isSuccessed
                     ? 'Finishing Cancellation...'
                     : 'Cancel'
+                  : planAction.isDone
+                  ? 'Go Back'
                   : 'Start Over',
               )}
             </Button>
diff --git a/web/client/src/library/components/search/SearchList.tsx b/web/client/src/library/components/search/SearchList.tsx
index 1c668598..d23d4210 100644
--- a/web/client/src/library/components/search/SearchList.tsx
+++ b/web/client/src/library/components/search/SearchList.tsx
@@ -70,8 +70,10 @@ export default function SearchList<
   autoFocus = false,
   showIndex = true,
   isFullWidth = false,
+  isLoading = false,
   direction = 'bottom',
   className,
+  onInput,
 }: {
   list: T[]
   searchBy: string
@@ -86,6 +88,8 @@ export default function SearchList<
   direction?: 'top' | 'bottom'
   className?: string
   isFullWidth?: boolean
+  isLoading?: boolean
+  onInput?: (value: string) => void
 }): JSX.Element {
   const navigate = useNavigate()
 
@@ -147,7 +151,10 @@ export default function SearchList<
           value={search}
           placeholder={placeholder}
           onInput={(e: React.ChangeEvent<HTMLInputElement>) => {
-            setSearch(e.target.value.trim())
+            const value = e.target.value.trim()
+
+            setSearch(value)
+            onInput?.(value)
           }}
           onKeyDown={(e: React.KeyboardEvent) => {
             if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
@@ -218,7 +225,7 @@ export default function SearchList<
                   size === EnumSize.lg && 'p-3',
                 )}
               >
-                No Results Found
+                {isLoading ? 'Loading...' : 'No Results Found'}
               </div>
             ) : (
               found.map(([item, index], idx) => (
diff --git a/web/client/src/library/components/sourceList/SourceList.tsx b/web/client/src/library/components/sourceList/SourceList.tsx
index dbd5a667..6b11547f 100644
--- a/web/client/src/library/components/sourceList/SourceList.tsx
+++ b/web/client/src/library/components/sourceList/SourceList.tsx
@@ -1,9 +1,11 @@
 import Input from '@components/input/Input'
-import { isArrayEmpty, isNil, isNotNil } from '@utils/index'
+import { type Virtualizer, useVirtualizer } from '@tanstack/react-virtual'
+import { isArrayEmpty, isNil, isNotNil, isStringEmptyOrNil } from '@utils/index'
 import clsx from 'clsx'
-import { useState } from 'react'
+import { useEffect, useMemo, useRef, useState } from 'react'
 import { NavLink } from 'react-router-dom'
 import { EnumSize, EnumVariant, type Variant } from '~/types/enum'
+import { Button } from '../button/Button'
 
 interface ListItem<
   TListItem extends Record<string, any> = Record<string, any>,
@@ -21,91 +23,185 @@ export default function SourceList<
   TItem extends Record<string, any> = Record<string, string>,
   TType extends Record<string, string> = Record<string, string>,
 >({
-  listItem,
   items = [],
-  types,
-  by = 'id',
-  byName,
-  byDescription,
-  to,
+  keyId = 'id',
+  keyName = '',
+  keyDescription = '',
   disabled = false,
+  to,
+  types,
   className,
+  isActive,
+  listItem,
 }: {
-  listItem: (listItem: ListItem<TItem>) => React.ReactNode
-  by: string
+  keyId: string
   to: string
   items?: TItem[]
   types?: TType
-  byName?: string
+  keyName?: string
+  keyDescription?: string
   disabled?: boolean
-  byDescription?: string
   className?: string
+  isActive?: (id: string) => boolean
+  listItem: (listItem: ListItem<TItem>) => React.ReactNode
 }): JSX.Element {
   const [filter, setFilter] = useState('')
 
-  const filtered =
-    filter === ''
-      ? items
-      : items.filter(item => {
-          const id = item[by] ?? ''
-          const description = String(
-            isNil(byDescription) ? '' : item?.[byDescription] ?? '',
-          )
-          const name = String(isNil(byName) ? '' : item?.[byName] ?? '')
-          const type = String(types?.[id] ?? '')
-
-          return (
-            name.includes(filter) ||
-            description.includes(filter) ||
-            type.includes(filter)
-          )
-        })
+  const scrollableAreaRef = useRef<HTMLDivElement>(null)
+
+  const [activeItemIndex, filtered] = useMemo(() => {
+    let activeIndex = -1
+    const filteredList: TItem[] = []
+
+    items.forEach((item, index) => {
+      const id = ensureString(item[keyId])
+      const description = ensureString(item[keyDescription])
+      const name = ensureString(item[keyName])
+      const type = ensureString(types?.[id])
+
+      if (
+        name.includes(filter) ||
+        description.includes(filter) ||
+        type.includes(filter)
+      ) {
+        filteredList.push(item)
+
+        if (isNotNil(isActive) && isActive(item[keyId])) {
+          activeIndex = index
+        }
+      }
+    })
+
+    return [activeIndex, filteredList]
+  }, [items, filter])
+
+  const rowVirtualizer = useVirtualizer({
+    count: filtered.length,
+    getScrollElement: () => scrollableAreaRef.current,
+    estimateSize: () => 32 + (keyDescription.length > 0 ? 16 : 0),
+  })
+
+  const scrollToItem = ({
+    itemIndex,
+    isSmoothScroll = true,
+  }: {
+    itemIndex: number
+    isSmoothScroll?: boolean
+  }): void => {
+    rowVirtualizer.scrollToIndex(itemIndex, {
+      align: 'center',
+      behavior: isSmoothScroll ? 'smooth' : 'auto',
+    })
+  }
+
+  const isOutsideVisibleRange = ({
+    itemIndex,
+    range,
+  }: {
+    itemIndex: number
+    range: Virtualizer<HTMLDivElement, Element>['range']
+  }): boolean =>
+    isNotNil(range) &&
+    (range.startIndex > itemIndex || range?.endIndex < itemIndex)
+
+  /**
+   * The return button should appear when the
+   * active item is available in the list (not
+   * filtered out) and it is not in the visible
+   * range of the virtualized list
+   */
+  const shouldShowReturnButton =
+    isStringEmptyOrNil(filter) &&
+    activeItemIndex > -1 &&
+    isOutsideVisibleRange({
+      range: rowVirtualizer.range,
+      itemIndex: activeItemIndex,
+    })
+
+  // scroll to the active item when the activeItemIndex changes
+  useEffect(() => {
+    if (
+      activeItemIndex > -1 &&
+      isOutsideVisibleRange({
+        range: rowVirtualizer.range,
+        itemIndex: activeItemIndex,
+      })
+    ) {
+      scrollToItem({ itemIndex: activeItemIndex, isSmoothScroll: false })
+    }
+  }, [activeItemIndex])
+
+  const rows = rowVirtualizer.getVirtualItems()
+  const totalSize = rowVirtualizer.getTotalSize()
 
   return (
-    <div className={clsx('flex flex-col w-full h-full', className)}>
-      <ul className="p-2 h-full overflow-auto hover:scrollbar scrollbar--horizontal scrollbar--vertical">
-        {isArrayEmpty(filtered) && (
-          <li
-            key="not-found"
-            className="p-2"
-            onClick={() => {
-              setFilter('')
-            }}
+    <div className={clsx('flex flex-col w-full h-full relative', className)}>
+      {shouldShowReturnButton && (
+        <Button
+          className="absolute left-[50%] translate-x-[-50%] top-0 z-10 text-ellipsis !block overflow-hidden no-wrap max-w-[90%] !border-neutral-20 shadow-md !bg-theme text-neutral-500 dark:text-neutral-400 !hover:bg-theme-10 !hover:text-neutral-600 dark:hover:text-neutral-300 !focus:ring-2 !focus:ring-theme-500 !focus:ring-offset-2 !focus:ring-offset-theme-50 !focus:ring-opacity-50 !focus:outline-none !focus:ring-offset-transparent !focus:ring-offset-0 !focus:ring"
+          onClick={() => scrollToItem({ itemIndex: activeItemIndex })}
+          size={EnumSize.sm}
+          variant={EnumVariant.Secondary}
+        >
+          Scroll to selected
+        </Button>
+      )}
+      <div
+        ref={scrollableAreaRef}
+        className="w-full h-full relative overflow-hidden overflow-y-auto hover:scrollbar scrollbar--horizontal scrollbar--vertical pt-2"
+        style={{ contain: 'strict' }}
+      >
+        <div
+          className="relative w-full"
+          style={{
+            height: totalSize > 0 ? `${totalSize}px` : '100%',
+          }}
+        >
+          <ul
+            className="w-full absolute top-0 left-0 px-2 text-neutral-400 dark:text-neutral-500"
+            style={{ transform: `translateY(${rows[0]?.start ?? 0}px)` }}
           >
-            No Results Found
-          </li>
-        )}
-        {filtered.map(item => {
-          const id = (item as Record<string, string>)[by]!
-          const name = isNil(byName)
-            ? ''
-            : (item as Record<string, string>)?.[byName] ?? ''
-          const description = isNil(byDescription)
-            ? undefined
-            : (item as Record<string, string>)?.[byDescription] ?? undefined
-
-          return (
-            <li
-              key={id}
-              className={clsx(
-                'text-sm font-normal',
-                disabled && 'cursor-not-allowed',
-              )}
-              tabIndex={id === filter ? -1 : 0}
-            >
-              {listItem?.({
-                id,
-                to: `${to}/${id}`,
-                name,
-                description,
-                text: (types as Record<string, string>)?.[id],
-                disabled,
-                item,
-              })}
-            </li>
-          )
-        })}
-      </ul>
+            {isArrayEmpty(filtered) && (
+              <li
+                key="not-found"
+                className="px-2 py-0.5 text-sm text-neutral-400 dark:text-neutral-500 text-center whitespace-nowrap overflow-ellipsis overflow-hidden"
+              >
+                {filter.length > 0 ? 'No Results Found' : 'Empty List'}
+              </li>
+            )}
+            {rows.map(virtualItem => {
+              const item = filtered[virtualItem.index]!
+              const id = ensureString(item[keyId])
+              const description = ensureString(item[keyDescription])
+              const name = ensureString(item[keyName])
+              const text = ensureString(types?.[id])
+
+              return (
+                <li
+                  key={virtualItem.key}
+                  data-index={virtualItem.index}
+                  ref={rowVirtualizer.measureElement}
+                  className={clsx(
+                    'text-sm font-normal w-full',
+                    disabled && 'cursor-not-allowed',
+                  )}
+                  tabIndex={id === filter ? -1 : 0}
+                >
+                  {listItem?.({
+                    id,
+                    to: `${to}/${id}`,
+                    name,
+                    description,
+                    text,
+                    disabled,
+                    item: filtered[virtualItem.index]!,
+                  })}
+                </li>
+              )
+            })}
+          </ul>
+        </div>
+      </div>
       <div className="p-2 w-full flex justify-between">
         <Input
           className="w-full !m-0"
@@ -116,6 +212,7 @@ export default function SourceList<
               className={clsx(className, 'w-full')}
               value={filter}
               placeholder="Filter items"
+              type="search"
               onInput={(e: React.ChangeEvent<HTMLInputElement>) => {
                 setFilter(e.target.value)
               }}
@@ -162,7 +259,7 @@ export function SourceListItem({
       to={to}
       className={({ isActive }) =>
         clsx(
-          'block overflow-hidden px-2 py-1 rounded-md w-full text-sm font-semibold',
+          'block overflow-hidden px-2 py-1.5 rounded-md w-full text-sm font-semibold',
           disabled && 'opacity-50 pointer-events-none',
           isActive
             ? variant === EnumVariant.Primary
@@ -170,14 +267,16 @@ export function SourceListItem({
               : variant === EnumVariant.Danger
               ? 'text-danger-500 bg-danger-5'
               : 'text-neutral-500 bg-neutral-10'
-            : 'hover:bg-neutral-10 text-neutral-400 dark:text-neutral-300',
+            : 'hover:bg-neutral-5 text-neutral-400 dark:text-neutral-300',
         )
       }
     >
-      <div className="flex items-center overflow-hidden whitespace-nowrap overflow-ellipsis">
-        {name}
+      <div className="flex items-center">
+        <span className="whitespace-nowrap overflow-ellipsis overflow-hidden">
+          {name}
+        </span>
         {isNotNil(text) && (
-          <span className="flex items-center ml-2 px-2 h-4 rounded-md text-[0.5rem] bg-neutral-10 dark:text-neutral-200 text-neutral-700 font-bold">
+          <span className=" ml-2 px-2 rounded-md leading-0 text-[0.5rem] bg-neutral-10 dark:text-neutral-200 text-neutral-700 font-bold">
             {text}
           </span>
         )}
@@ -190,3 +289,7 @@ export function SourceListItem({
     </NavLink>
   )
 }
+
+function ensureString(value?: string | number): string {
+  return isNil(value) ? '' : String(value)
+}
diff --git a/web/client/src/library/pages/audits/Audits.tsx b/web/client/src/library/pages/audits/Audits.tsx
index b155a29e..68d1d99c 100644
--- a/web/client/src/library/pages/audits/Audits.tsx
+++ b/web/client/src/library/pages/audits/Audits.tsx
@@ -1,4 +1,4 @@
-import { Outlet } from 'react-router-dom'
+import { Outlet, useLocation } from 'react-router-dom'
 import Page from '../root/Page'
 import { useStoreProject } from '@context/project'
 import SourceList, { SourceListItem } from '@components/sourceList/SourceList'
@@ -6,8 +6,10 @@ import { EnumSize, EnumVariant } from '~/types/enum'
 import { EnumRoutes } from '~/routes'
 import { Button } from '@components/button/Button'
 import { Divider } from '@components/divider/Divider'
+import { type ModelFile } from '@models/file'
 
 export default function PageAudits(): JSX.Element {
+  const { pathname } = useLocation()
   const files = useStoreProject(s => s.files)
 
   const items = Array.from(files.values()).filter(it =>
@@ -18,11 +20,12 @@ export default function PageAudits(): JSX.Element {
     <Page
       sidebar={
         <div className="flex flex-col w-full h-full">
-          <SourceList
-            by="basename"
-            byName="basename"
+          <SourceList<ModelFile>
+            keyId="basename"
+            keyName="basename"
             to={EnumRoutes.Audits}
             items={items}
+            isActive={id => `${EnumRoutes.Audits}/${id}` === pathname}
             className="h-full"
             listItem={({ to, name, description, text, disabled = false }) => (
               <SourceListItem
diff --git a/web/client/src/library/pages/docs/Docs.tsx b/web/client/src/library/pages/docs/Docs.tsx
index 00f8f92b..51907b8c 100644
--- a/web/client/src/library/pages/docs/Docs.tsx
+++ b/web/client/src/library/pages/docs/Docs.tsx
@@ -13,7 +13,7 @@ import { type LineageNodeModelType } from '@components/graph/Graph'
 import { getModelNodeTypeTitle } from '@components/graph/help'
 
 export default function PageDocs(): JSX.Element {
-  const location = useLocation()
+  const { pathname } = useLocation()
   const navigate = useNavigate()
   const { modelName } = useParams()
 
@@ -43,12 +43,12 @@ export default function PageDocs(): JSX.Element {
   return (
     <Page
       sidebar={
-        <SourceList
-          key={location.pathname}
-          by="displayName"
-          byName="displayName"
+        <SourceList<ModelSQLMeshModel>
+          keyId="displayName"
+          keyName="displayName"
           to={EnumRoutes.IdeDocsModels}
           items={list}
+          isActive={id => `${EnumRoutes.IdeDocsModels}/${id}` === pathname}
           types={list.reduce(
             (acc: Record<string, string>, it) =>
               Object.assign(acc, {
diff --git a/web/client/src/library/pages/errors/Errors.tsx b/web/client/src/library/pages/errors/Errors.tsx
index 6dcece0a..4c6a0236 100644
--- a/web/client/src/library/pages/errors/Errors.tsx
+++ b/web/client/src/library/pages/errors/Errors.tsx
@@ -1,14 +1,15 @@
-import { useIDE } from '../ide/context'
+import { type ErrorIDE, useIDE } from '../ide/context'
 import Page from '../root/Page'
 import SourceList, { SourceListItem } from '@components/sourceList/SourceList'
 import { EnumRoutes } from '~/routes'
-import { Outlet, useNavigate } from 'react-router-dom'
+import { Outlet, useLocation, useNavigate } from 'react-router-dom'
 import { EnumSize, EnumVariant } from '~/types/enum'
 import { useEffect, useMemo } from 'react'
 import { isArrayEmpty, isNil, isNotNil } from '@utils/index'
 import { Button } from '@components/button/Button'
 
 export default function PageErrors(): JSX.Element {
+  const { pathname } = useLocation()
   const navigate = useNavigate()
 
   const { errors, removeError, clearErrors } = useIDE()
@@ -40,7 +41,7 @@ export default function PageErrors(): JSX.Element {
   return (
     <Page
       sidebar={
-        <>
+        <div className="flex flex-col h-full w-full">
           {errors.size > 0 && (
             <div className="flex justify-end m-1">
               <Button
@@ -52,12 +53,13 @@ export default function PageErrors(): JSX.Element {
               </Button>
             </div>
           )}
-          <SourceList
-            by="id"
-            byName="key"
-            byDescription="message"
+          <SourceList<ErrorIDE>
+            keyId="id"
+            keyName="key"
+            keyDescription="message"
             to={EnumRoutes.Errors}
             items={list}
+            isActive={id => `${EnumRoutes.Errors}/${id}` === pathname}
             types={list.reduce(
               (acc: Record<string, string>, it) =>
                 Object.assign(acc, { [it.id]: it.status }),
@@ -82,7 +84,7 @@ export default function PageErrors(): JSX.Element {
               />
             )}
           />
-        </>
+        </div>
       }
       content={<Outlet />}
     />
diff --git a/web/client/src/library/pages/plan/Plan.tsx b/web/client/src/library/pages/plan/Plan.tsx
index 12a42756..9522f585 100644
--- a/web/client/src/library/pages/plan/Plan.tsx
+++ b/web/client/src/library/pages/plan/Plan.tsx
@@ -7,6 +7,7 @@ import { useEffect } from 'react'
 import { useStorePlan } from '@context/plan'
 import { isFalse, isNotNil } from '@utils/index'
 import { Modules } from '@api/client'
+import { type ModelEnvironment } from '@models/environment'
 
 export default function PagePlan(): JSX.Element {
   const navigate = useNavigate()
@@ -19,6 +20,8 @@ export default function PagePlan(): JSX.Element {
   const planAction = useStorePlan(s => s.planAction)
   const planApply = useStorePlan(s => s.planApply)
 
+  const environmentsArray = Array.from(environments)
+
   useEffect(() => {
     if (planApply.isRunning && isNotNil(planApply.environment)) {
       const pathname = `${EnumRoutes.Plan}/environments/${planApply.environment}`
@@ -45,24 +48,20 @@ export default function PagePlan(): JSX.Element {
   return (
     <Page
       sidebar={
-        <SourceList
-          by="name"
-          byName="name"
+        <SourceList<ModelEnvironment>
+          keyId="name"
+          keyName="name"
           to={`${EnumRoutes.Plan}/environments`}
-          items={Array.from(environments)}
+          items={environmentsArray}
           disabled={
             isFalse(modules.includes(Modules.plans)) ||
             planAction.isProcessing ||
             environment.isInitialProd
           }
-          listItem={({
-            to,
-            name,
-            description,
-            text,
-            disabled = false,
-            item,
-          }) => (
+          isActive={id =>
+            `${EnumRoutes.Plan}/environments/${id}` === location.pathname
+          }
+          listItem={({ to, name, description, text, disabled = false }) => (
             <SourceListItem
               to={to}
               name={name}
diff --git a/web/client/src/workers/lineage.ts b/web/client/src/workers/lineage.ts
index c72a0924..87ae73a8 100644
--- a/web/client/src/workers/lineage.ts
+++ b/web/client/src/workers/lineage.ts
@@ -17,33 +17,31 @@ const scope = self as any
 
 scope.onmessage = async (e: MessageEvent) => {
   if (e.data.topic === 'lineage') {
-    const lineage = mergeLineageWithModels(
-      e.data.payload.currentLineage,
-      e.data.payload.newLineage,
-    )
-    getNodesConnections(e.data.payload.mainNode, lineage)
-      .then(nodesConnections => {
-        scope.postMessage({
-          topic: 'lineage',
-          payload: {
-            lineage,
-            nodesConnections,
-          },
-        })
+    try {
+      const { currentLineage, newLineage, mainNode } = e.data.payload
+      const lineage = await mergeLineageWithModels(currentLineage, newLineage)
+      const nodesConnections = await getNodesConnections(mainNode, lineage)
+
+      scope.postMessage({
+        topic: 'lineage',
+        payload: {
+          lineage,
+          nodesConnections,
+        },
       })
-      .catch(error => {
-        scope.postMessage({
-          topic: 'error',
-          error,
-        })
+    } catch (error) {
+      scope.postMessage({
+        topic: 'error',
+        error,
       })
+    }
   }
 }
 
-function mergeLineageWithModels(
+async function mergeLineageWithModels(
   currentLineage: Record<string, Lineage> = {},
   data: Record<string, string[]> = {},
-): Record<string, Lineage> {
+): Promise<Record<string, Lineage>> {
   return Object.entries(data).reduce(
     (acc: Record<string, Lineage>, [key, models = []]) => {
       key = encodeURI(key)


Test patch:
diff --git a/tests/core/engine_adapter/test_integration.py b/tests/core/engine_adapter/test_integration.py
index 6a149bee..ee377034 100644
--- a/tests/core/engine_adapter/test_integration.py
+++ b/tests/core/engine_adapter/test_integration.py
@@ -7,6 +7,7 @@ import sys
 import typing as t
 from datetime import timedelta
 
+import numpy as np
 import pandas as pd
 import pytest
 from sqlglot import exp, parse_one
@@ -174,7 +175,7 @@ class TestContext:
     def get_current_data(self, table: exp.Table) -> pd.DataFrame:
         df = self.engine_adapter.fetchdf(exp.select("*").from_(table), quote_identifiers=True)
         if self.dialect == "snowflake" and "id" in df.columns:
-            df["id"] = df["id"].astype("int")
+            df["id"] = df["id"].apply(lambda x: x if pd.isna(x) else int(x))
         return df
 
     def compare_with_current(self, table: exp.Table, expected: pd.DataFrame) -> None:
@@ -820,6 +821,34 @@ def test_drop_schema(ctx: TestContext):
     assert len(results.views) == 0
 
 
+def test_nan_roundtrip(ctx: TestContext):
+    if ctx.test_type != "df":
+        pytest.skip("NaN roundtrip test only relevant for dataframes.")
+    ctx.engine_adapter.DEFAULT_BATCH_SIZE = sys.maxsize
+    ctx.init()
+    table = ctx.table("test_table")
+    # Initial Load
+    input_data = pd.DataFrame(
+        [
+            {"id": 1, "ds": "2022-01-01"},
+            {"id": 2, "ds": "2022-01-02"},
+            {"id": np.nan, "ds": np.nan},
+        ]
+    )
+    ctx.engine_adapter.create_table(table, ctx.columns_to_types)
+    ctx.engine_adapter.replace_query(
+        table,
+        ctx.input_data(input_data),
+        columns_to_types=ctx.columns_to_types,
+    )
+    results = ctx.get_metadata_results()
+    assert not results.views
+    assert not results.materialized_views
+    assert len(results.tables) == len(results.non_temp_tables) == 1
+    assert results.non_temp_tables[0] == table.name
+    ctx.compare_with_current(table, input_data)
+
+
 def test_replace_query(ctx: TestContext):
     ctx.engine_adapter.DEFAULT_BATCH_SIZE = sys.maxsize
     ctx.init()
diff --git a/tests/core/test_config.py b/tests/core/test_config.py
index f0bd1329..a4230f0b 100644
--- a/tests/core/test_config.py
+++ b/tests/core/test_config.py
@@ -12,6 +12,7 @@ from sqlmesh.core.config import (
     GatewayConfig,
     ModelDefaultsConfig,
 )
+from sqlmesh.core.config.feature_flag import DbtFeatureFlag, FeatureFlag
 from sqlmesh.core.config.loader import (
     load_config_from_env,
     load_config_from_paths,
@@ -256,10 +257,12 @@ def test_load_config_from_env():
         {
             "SQLMESH__GATEWAY__CONNECTION__TYPE": "duckdb",
             "SQLMESH__GATEWAY__CONNECTION__DATABASE": "test_db",
+            "SQLMESH__FEATURE_FLAGS__DBT__SCD_TYPE_2_SUPPORT": "false",
         },
     ):
         assert Config.parse_obj(load_config_from_env()) == Config(
-            gateways=GatewayConfig(connection=DuckDBConnectionConfig(database="test_db"))
+            gateways=GatewayConfig(connection=DuckDBConnectionConfig(database="test_db")),
+            feature_flags=FeatureFlag(dbt=DbtFeatureFlag(scd_type_2_support=False)),
         )
 
 
@@ -383,3 +386,31 @@ environment_catalog_mapping:
             load_config_from_paths(project_paths=[config_path]).environment_catalog_mapping
             == expected
         )
+
+
+def test_load_feature_flag(tmp_path_factory):
+    config_path = tmp_path_factory.mktemp("yaml_config") / "config.yaml"
+    with open(config_path, "w") as fd:
+        fd.write(
+            """
+gateways:
+    duckdb_gateway:
+        connection:
+            type: duckdb
+model_defaults:
+    dialect: bigquery
+feature_flags:
+    dbt:
+        scd_type_2_support: false
+        """
+        )
+
+    assert load_config_from_paths(
+        project_paths=[config_path],
+    ) == Config(
+        gateways={
+            "duckdb_gateway": GatewayConfig(connection=DuckDBConnectionConfig()),
+        },
+        model_defaults=ModelDefaultsConfig(dialect="bigquery"),
+        feature_flags=FeatureFlag(dbt=DbtFeatureFlag(scd_type_2_support=False)),
+    )
diff --git a/tests/dbt/test_adapter.py b/tests/dbt/test_adapter.py
index a58a4633..764bf23e 100644
--- a/tests/dbt/test_adapter.py
+++ b/tests/dbt/test_adapter.py
@@ -1,6 +1,8 @@
 from __future__ import annotations
 
+import os
 import typing as t
+from unittest import mock
 from unittest.mock import call
 
 import pytest
@@ -10,6 +12,7 @@ from dbt.contracts.relation import Policy
 from pytest_mock.plugin import MockerFixture
 from sqlglot import exp
 
+from sqlmesh import Context
 from sqlmesh.core.dialect import schema_
 from sqlmesh.core.snapshot import SnapshotId
 from sqlmesh.dbt.project import Project
@@ -208,3 +211,25 @@ def test_adapter_map_snapshot_tables(
 
     assert renderer("{{ adapter.resolve_schema(foo_bar) }}") == "foo"
     assert renderer("{{ adapter.resolve_identifier(foo_bar) }}") == "bar"
+
+
+def test_feature_flag_scd_type_2(copy_to_temp_path, caplog):
+    project_root = "tests/fixtures/dbt/sushi_test"
+    sushi_context = Context(paths=copy_to_temp_path(project_root))
+    assert '"memory"."snapshots"."items_snapshot"' in sushi_context.models
+    assert (
+        "Skipping loading Snapshot (SCD Type 2) models due to the feature flag disabling this feature"
+        not in caplog.text
+    )
+    with mock.patch.dict(
+        os.environ,
+        {
+            "SQLMESH__FEATURE_FLAGS__DBT__SCD_TYPE_2_SUPPORT": "false",
+        },
+    ):
+        sushi_context = Context(paths=copy_to_temp_path(project_root))
+        assert '"memory"."snapshots"."items_snapshot"' not in sushi_context.models
+        assert (
+            "Skipping loading Snapshot (SCD Type 2) models due to the feature flag disabling this feature"
+            in caplog.text
+        )
diff --git a/web/client/src/library/pages/tests/Tests.tsx b/web/client/src/library/pages/tests/Tests.tsx
index 290df175..55168882 100644
--- a/web/client/src/library/pages/tests/Tests.tsx
+++ b/web/client/src/library/pages/tests/Tests.tsx
@@ -1,4 +1,4 @@
-import { Outlet } from 'react-router-dom'
+import { Outlet, useLocation } from 'react-router-dom'
 import Page from '../root/Page'
 import { useStoreProject } from '@context/project'
 import SourceList, { SourceListItem } from '@components/sourceList/SourceList'
@@ -6,8 +6,10 @@ import { EnumSize, EnumVariant } from '~/types/enum'
 import { EnumRoutes } from '~/routes'
 import { Button } from '@components/button/Button'
 import { Divider } from '@components/divider/Divider'
+import { type ModelFile } from '@models/file'
 
 export default function PageTests(): JSX.Element {
+  const { pathname } = useLocation()
   const files = useStoreProject(s => s.files)
 
   const items = Array.from(files.values()).filter(it =>
@@ -18,11 +20,12 @@ export default function PageTests(): JSX.Element {
     <Page
       sidebar={
         <div className="flex flex-col w-full h-full">
-          <SourceList
-            by="basename"
-            byName="basename"
+          <SourceList<ModelFile>
+            keyId="basename"
+            keyName="basename"
             to={EnumRoutes.Tests}
             items={items}
+            isActive={id => `${EnumRoutes.Tests}/${id}` === pathname}
             listItem={({ to, name, description, text, disabled = false }) => (
               <SourceListItem
                 to={to}