diff --git a/docs/guides/working_with_data/sql.md b/docs/guides/working_with_data/sql.md index 01e575012cf..f49e181b2cf 100644 --- a/docs/guides/working_with_data/sql.md +++ b/docs/guides/working_with_data/sql.md @@ -449,6 +449,21 @@ Lint your SQL code and provide better autocompletions and error highlighting. To disable the linter, you can set the `sql_linter` configuration to `false` in your `pyproject.toml` file or disable it in the marimo editor's settings menu. +**SQL keyword case** + +marimo generates SQL in a few places — new SQL cell boilerplate, keyword +autocompletions, and table snippets in the Data Sources panel. By default, +generated keywords are uppercase (`SELECT`, `FROM`). To generate lowercase +keywords instead, set `sql_keyword_case` in your `pyproject.toml`: + +```toml +[tool.marimo.runtime] +sql_keyword_case = "lower" +``` + +or change it in the marimo editor's settings menu. This only affects SQL that +marimo generates; formatting a cell preserves the case of SQL you have written. + **SQL Formatting** Click on the paint roller icon at the bottom right of the SQL cell to format your SQL code. diff --git a/frontend/src/components/app-config/common.tsx b/frontend/src/components/app-config/common.tsx index 52ab70c9d1f..9eaa62eae91 100644 --- a/frontend/src/components/app-config/common.tsx +++ b/frontend/src/components/app-config/common.tsx @@ -1,7 +1,10 @@ /* Copyright 2026 Marimo. All rights reserved. */ import type { HTMLProps, PropsWithChildren } from "react"; -import type { SqlOutputType } from "@/core/config/config-schema"; +import type { + SqlKeywordCase, + SqlOutputType, +} from "@/core/config/config-schema"; import { cn } from "@/utils/cn"; export const formItemClasses = "flex flex-row items-center space-x-1 space-y-0"; @@ -60,3 +63,11 @@ export const SQL_OUTPUT_SELECT_OPTIONS: { { label: "Lazy Polars", value: "lazy-polars" }, { label: "Pandas", value: "pandas" }, ]; + +export const SQL_KEYWORD_CASE_SELECT_OPTIONS: { + label: string; + value: SqlKeywordCase; +}[] = [ + { label: "Uppercase (Default)", value: "upper" }, + { label: "Lowercase", value: "lower" }, +]; diff --git a/frontend/src/components/app-config/data-form.tsx b/frontend/src/components/app-config/data-form.tsx index 7a5e6fcd462..881dcadae11 100644 --- a/frontend/src/components/app-config/data-form.tsx +++ b/frontend/src/components/app-config/data-form.tsx @@ -16,6 +16,7 @@ import { Checkbox } from "../ui/checkbox"; import { formItemClasses, SettingGroup, + SQL_KEYWORD_CASE_SELECT_OPTIONS, SQL_OUTPUT_SELECT_OPTIONS, } from "./common"; import { IsOverridden, OverriddenFormField } from "./is-overridden"; @@ -266,6 +267,40 @@ export const DataForm = ({ )} /> + + ( +
+ + SQL keyword case + + field.onChange(e.target.value)} + value={override.value ?? "upper"} + disabled={field.disabled || override.isOverridden} + className="inline-flex mr-2" + > + {SQL_KEYWORD_CASE_SELECT_OPTIONS.map((option) => ( + + ))} + + + + + + + + The keyword case used in generated SQL, such as cell + boilerplate, autocomplete suggestions, and table snippets. + +
+ )} + /> ); diff --git a/frontend/src/components/datasources/__tests__/utils.test.ts b/frontend/src/components/datasources/__tests__/utils.test.ts index bad765cf5bb..9c9e5e971f1 100644 --- a/frontend/src/components/datasources/__tests__/utils.test.ts +++ b/frontend/src/components/datasources/__tests__/utils.test.ts @@ -1,8 +1,11 @@ /* Copyright 2026 Marimo. All rights reserved. */ -import { describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { userConfigAtom } from "@/core/config/config"; +import { defaultUserConfig } from "@/core/config/config-schema"; import type { SQLTableContext } from "@/core/datasets/data-source-connections"; import { DUCKDB_ENGINE } from "@/core/datasets/engines"; +import { store } from "@/core/state/jotai"; import type { Database, DatabaseSchema, @@ -467,6 +470,87 @@ describe("sqlCode", () => { ); }); }); + + describe("lowercase keyword case", () => { + beforeEach(() => { + const config = defaultUserConfig(); + store.set(userConfigAtom, { + ...config, + runtime: { ...config.runtime, sql_keyword_case: "lower" }, + }); + }); + + afterEach(() => { + store.set(userConfigAtom, defaultUserConfig()); + }); + + it("should lowercase keywords in the default formatter", () => { + const sqlTableContext: SQLTableContext = { + engine: "snowflake", + schema: "public", + defaultSchema: "public", + defaultDatabase: "mydb", + database: "mydb", + dialect: "snowflake", + }; + + const result = sqlCode({ + table: mockTable, + columnName: mockColumn.name, + sqlTableContext, + }); + expect(result).toBe( + '_df = mo.sql(f"""\nselect email from users limit 100\n""", engine=snowflake)', + ); + }); + + it("should lowercase SELECT TOP for MSSQL", () => { + const sqlTableContext: SQLTableContext = { + engine: "mssql", + schema: "dbo", + defaultSchema: "dbo", + defaultDatabase: "master", + database: "master", + dialect: "mssql", + }; + + const result = sqlCode({ + table: mockTable, + columnName: mockColumn.name, + sqlTableContext, + }); + expect(result).toBe( + '_df = mo.sql(f"""\nselect top 100 email from users\n""", engine=mssql)', + ); + }); + + it("should lowercase keywords but not quoted identifiers for postgres", () => { + const sqlTableContext: SQLTableContext = { + engine: "postgres", + schema: "public", + defaultSchema: "public", + defaultDatabase: "mydb", + database: "mydb", + dialect: "postgres", + }; + + const result = sqlCode({ + table: mockTable, + columnName: mockColumn.name, + sqlTableContext, + }); + expect(result).toBe( + '_df = mo.sql(f"""\nselect "email" from "users" limit 100\n""", engine=postgres)', + ); + }); + + it("should lowercase keywords without sqlTableContext", () => { + const result = sqlCode({ table: mockTable, columnName: mockColumn.name }); + expect(result).toBe( + "_df = mo.sql(f'select \"email\" from users limit 100')", + ); + }); + }); }); describe("tableUniqueId", () => { diff --git a/frontend/src/components/datasources/utils.ts b/frontend/src/components/datasources/utils.ts index 904d152acbd..0202dabb8aa 100644 --- a/frontend/src/components/datasources/utils.ts +++ b/frontend/src/components/datasources/utils.ts @@ -1,6 +1,7 @@ /* Copyright 2026 Marimo. All rights reserved. */ import { BigQueryDialect } from "@marimo-team/codemirror-sql/dialects"; +import { sqlKeyword } from "@/core/codemirror/language/languages/sql/keyword-case"; import { isKnownDialect } from "@/core/codemirror/language/languages/sql/utils"; import type { SQLTableContext } from "@/core/datasets/data-source-connections"; import { DUCKDB_ENGINE } from "@/core/datasets/engines"; @@ -111,7 +112,7 @@ interface SqlCodeFormatter { const defaultFormatter: SqlCodeFormatter = { formatTablePath: (tablePath: string[]) => tablePath.join("."), formatSelectClause: (columnName: string, tableName: string) => - `SELECT ${columnName} FROM ${tableName} LIMIT 100`, + `${sqlKeyword("SELECT")} ${columnName} ${sqlKeyword("FROM")} ${tableName} ${sqlKeyword("LIMIT")} 100`, }; function getFormatter(dialect: string): SqlCodeFormatter { @@ -136,7 +137,7 @@ function getFormatter(dialect: string): SqlCodeFormatter { return { formatTablePath: defaultFormatter.formatTablePath, formatSelectClause: (columnName: string, tableName: string) => - `SELECT TOP 100 ${columnName} FROM ${tableName}`, + `${sqlKeyword("SELECT TOP")} 100 ${columnName} ${sqlKeyword("FROM")} ${tableName}`, }; case "timescaledb": case "postgres": @@ -148,7 +149,7 @@ function getFormatter(dialect: string): SqlCodeFormatter { formatTablePath: (tablePath: string[]) => tablePath.map((part) => `"${part}"`).join("."), formatSelectClause: (columnName: string, tableName: string) => - `SELECT ${columnName === "*" ? "*" : `"${columnName}"`} FROM ${tableName} LIMIT 100`, + `${sqlKeyword("SELECT")} ${columnName === "*" ? "*" : `"${columnName}"`} ${sqlKeyword("FROM")} ${tableName} ${sqlKeyword("LIMIT")} 100`, }; case "db2": case "db2i": @@ -231,7 +232,7 @@ export function sqlCode({ return `_df = mo.sql(f"""\n${selectClause}\n""", engine=${engine})`; } - return `_df = mo.sql(f'SELECT "${columnName}" FROM ${table.name} LIMIT 100')`; + return `_df = mo.sql(f'${sqlKeyword("SELECT")} "${columnName}" ${sqlKeyword("FROM")} ${table.name} ${sqlKeyword("LIMIT")} 100')`; } export function convertStatsName(stat: ColumnHeaderStatsKey, type: DataType) { diff --git a/frontend/src/core/codemirror/language/__tests__/sql.test.ts b/frontend/src/core/codemirror/language/__tests__/sql.test.ts index 78daea258b9..c3082366570 100644 --- a/frontend/src/core/codemirror/language/__tests__/sql.test.ts +++ b/frontend/src/core/codemirror/language/__tests__/sql.test.ts @@ -7,12 +7,16 @@ import { forEachDiagnostic, forceLinting } from "@codemirror/lint"; import { EditorState, type Extension } from "@codemirror/state"; import { EditorView } from "@codemirror/view"; import { DuckDBDialect } from "@marimo-team/codemirror-sql/dialects"; +import { SQLParser } from "@marimo-team/smart-cells"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { CellId } from "@/core/cells/ids"; -import type { - CompletionConfig, - DiagnosticsConfig, - LSPConfig, +import { userConfigAtom } from "@/core/config/config"; +import { + type CompletionConfig, + defaultUserConfig, + type DiagnosticsConfig, + type LSPConfig, + type SqlKeywordCase, } from "@/core/config/config-schema"; import type { DataSourceConnection } from "@/core/datasets/data-source-connections"; import { @@ -634,6 +638,40 @@ _df = mo.sql( setLatestEngineSelected(DUCKDB_ENGINE); expect(adapter.defaultCode).toBe(`_df = mo.sql(f"""SELECT * FROM """)`); }); + + it("should match SQLParser.defaultCode under the default config", () => { + setLatestEngineSelected(DUCKDB_ENGINE); + expect(adapter.defaultCode).toBe(new SQLParser().defaultCode); + }); + + describe("with lowercase keyword case", () => { + const setKeywordCase = (keywordCase: SqlKeywordCase) => { + const config = defaultUserConfig(); + store.set(userConfigAtom, { + ...config, + runtime: { ...config.runtime, sql_keyword_case: keywordCase }, + }); + }; + + afterEach(() => { + store.set(userConfigAtom, defaultUserConfig()); + }); + + it("should lowercase keywords in defaultCode with an engine", () => { + setKeywordCase("lower"); + const engine = "postgres_engine" as ConnectionName; + setLatestEngineSelected(engine); + expect(adapter.defaultCode).toBe( + `_df = mo.sql(f"""select * from """, engine=${engine})`, + ); + }); + + it("should lowercase keywords in defaultCode without an engine", () => { + setKeywordCase("lower"); + setLatestEngineSelected(DUCKDB_ENGINE); + expect(adapter.defaultCode).toBe(`_df = mo.sql(f"""select * from """)`); + }); + }); }); }); @@ -2435,6 +2473,45 @@ describe("tablesCompletionSource", () => { ); }); + it("should provide lowercase keyword completions when configured", async () => { + const config = defaultUserConfig(); + store.set(userConfigAtom, { + ...config, + runtime: { ...config.runtime, sql_keyword_case: "lower" }, + }); + + const mockConnection: DataSourceConnection = { + name: TEST_ENGINE, + dialect: "postgres", + display_name: "postgres", + source: "postgres", + databases: [], + }; + + store.set(dataSourceConnectionsAtom, { + connectionsMap: new Map([[TEST_ENGINE, mockConnection]]), + latestEngineSelected: TEST_ENGINE, + }); + + const state = createEditorState("sel", { + engine: TEST_ENGINE, + }); + const ctx = createCompletionContext(state, 3); + + const adapter = new SQLLanguageAdapter(); + const extensions = adapter.getExtension(...TEST_EXTENSION_ARGS); + const completion = getCompletionSources(extensions)?.[2]; + + expect(completion).toBeDefined(); + const result = await completion!(ctx); + expect(result).toBeDefined(); + expect(result?.options.some((opt) => opt.label === "select")).toBe( + true, + ); + + store.set(userConfigAtom, defaultUserConfig()); + }); + it("should not provide keyword completions after dot", async () => { const mockConnection: DataSourceConnection = { name: TEST_ENGINE, diff --git a/frontend/src/core/codemirror/language/languages/sql/__tests__/keyword-case.test.ts b/frontend/src/core/codemirror/language/languages/sql/__tests__/keyword-case.test.ts new file mode 100644 index 00000000000..0c26fdb0290 --- /dev/null +++ b/frontend/src/core/codemirror/language/languages/sql/__tests__/keyword-case.test.ts @@ -0,0 +1,42 @@ +/* Copyright 2026 Marimo. All rights reserved. */ + +import { afterEach, describe, expect, it } from "vitest"; +import { userConfigAtom } from "@/core/config/config"; +import { + defaultUserConfig, + type SqlKeywordCase, +} from "@/core/config/config-schema"; +import { store } from "@/core/state/jotai"; +import { sqlKeyword, sqlKeywordCase } from "../keyword-case"; + +function setKeywordCase(keywordCase: SqlKeywordCase | undefined) { + const config = defaultUserConfig(); + store.set(userConfigAtom, { + ...config, + runtime: { ...config.runtime, sql_keyword_case: keywordCase }, + }); +} + +describe("sqlKeywordCase", () => { + afterEach(() => { + store.set(userConfigAtom, defaultUserConfig()); + }); + + it("defaults to upper", () => { + store.set(userConfigAtom, defaultUserConfig()); + expect(sqlKeywordCase()).toBe("upper"); + expect(sqlKeyword("select")).toBe("SELECT"); + }); + + it("returns lower when configured", () => { + setKeywordCase("lower"); + expect(sqlKeywordCase()).toBe("lower"); + expect(sqlKeyword("SELECT TOP")).toBe("select top"); + }); + + it("falls back to upper when the setting is missing", () => { + setKeywordCase(undefined); + expect(sqlKeywordCase()).toBe("upper"); + expect(sqlKeyword("limit")).toBe("LIMIT"); + }); +}); diff --git a/frontend/src/core/codemirror/language/languages/sql/completion-sources.tsx b/frontend/src/core/codemirror/language/languages/sql/completion-sources.tsx index e849df635b2..ffecca8f066 100644 --- a/frontend/src/core/codemirror/language/languages/sql/completion-sources.tsx +++ b/frontend/src/core/codemirror/language/languages/sql/completion-sources.tsx @@ -10,6 +10,7 @@ import { DefaultSqlTooltipRenders } from "@marimo-team/codemirror-sql"; import { once } from "@/utils/once"; import { languageMetadataField } from "../../metadata"; import { SCHEMA_CACHE } from "./completion-store"; +import { sqlKeywordCase } from "./keyword-case"; import type { SQLLanguageAdapterMetadata } from "./sql"; function getSQLMetadata(state: EditorState): SQLLanguageAdapterMetadata { @@ -79,7 +80,9 @@ export function customKeywordCompletionSource(): CompletionSource { }; }; - const uppercaseKeywords = true; + // Read per-request so setting changes apply without a reload; the + // completion override array is only rebuilt on language switch. + const uppercaseKeywords = sqlKeywordCase() === "upper"; const result = keywordCompletionSource( dialect, uppercaseKeywords, diff --git a/frontend/src/core/codemirror/language/languages/sql/keyword-case.ts b/frontend/src/core/codemirror/language/languages/sql/keyword-case.ts new file mode 100644 index 00000000000..5c22290d6fd --- /dev/null +++ b/frontend/src/core/codemirror/language/languages/sql/keyword-case.ts @@ -0,0 +1,25 @@ +/* Copyright 2026 Marimo. All rights reserved. */ + +import { getResolvedMarimoConfig } from "@/core/config/config"; +import type { SqlKeywordCase } from "@/core/config/config-schema"; + +/** + * The user-configured keyword case for generated SQL. + * + * Read lazily (not cached) so setting changes apply without a reload. + */ +export function sqlKeywordCase(): SqlKeywordCase { + return getResolvedMarimoConfig()?.runtime?.sql_keyword_case ?? "upper"; +} + +/** + * Case a SQL keyword fragment per the user's configured keyword case. + * + * Pass keyword fragments only (e.g. "SELECT", "SELECT TOP") — never + * identifiers, which must keep their original case. + */ +export function sqlKeyword(fragment: string): string { + return sqlKeywordCase() === "lower" + ? fragment.toLowerCase() + : fragment.toUpperCase(); +} diff --git a/frontend/src/core/codemirror/language/languages/sql/sql.ts b/frontend/src/core/codemirror/language/languages/sql/sql.ts index afbd23f6247..34fe69378bd 100644 --- a/frontend/src/core/codemirror/language/languages/sql/sql.ts +++ b/frontend/src/core/codemirror/language/languages/sql/sql.ts @@ -62,6 +62,7 @@ import { tablesCompletionSource, } from "./completion-sources"; import { SCHEMA_CACHE } from "./completion-store"; +import { sqlKeyword } from "./keyword-case"; import { getSQLMode, type SQLMode } from "./sql-mode"; import { isKnownDialect } from "./utils"; @@ -99,11 +100,12 @@ export class SQLLanguageAdapter implements LanguageAdapter SQLParser.fromQuery(query); diff --git a/frontend/src/core/config/__tests__/config-schema.test.ts b/frontend/src/core/config/__tests__/config-schema.test.ts index c9a26d3a272..ff32ba830a6 100644 --- a/frontend/src/core/config/__tests__/config-schema.test.ts +++ b/frontend/src/core/config/__tests__/config-schema.test.ts @@ -95,6 +95,7 @@ test("default UserConfig - empty", () => { "on_cell_change": "autorun", "reactive_tests": true, "show_tracebacks": false, + "sql_keyword_case": "upper", "watcher_on_save": "lazy", }, "save": { @@ -169,6 +170,7 @@ test("default UserConfig - one level", () => { "on_cell_change": "autorun", "reactive_tests": true, "show_tracebacks": false, + "sql_keyword_case": "upper", "watcher_on_save": "lazy", }, "save": { diff --git a/frontend/src/core/config/config-schema.ts b/frontend/src/core/config/config-schema.ts index 80bc3ba19c8..05f72458e1d 100644 --- a/frontend/src/core/config/config-schema.ts +++ b/frontend/src/core/config/config-schema.ts @@ -37,6 +37,12 @@ const VALID_SQL_OUTPUT_FORMATS = [ ] as const; export type SqlOutputType = (typeof VALID_SQL_OUTPUT_FORMATS)[number]; +/** + * Keyword case for generated SQL + */ +const VALID_SQL_KEYWORD_CASES = ["upper", "lower"] as const; +export type SqlKeywordCase = (typeof VALID_SQL_KEYWORD_CASES)[number]; + export const DEFAULT_AI_MODEL = "openai/gpt-4o"; /** @@ -129,6 +135,7 @@ export const UserConfigSchema = z reactive_tests: z.boolean().prefault(true), watcher_on_save: z.enum(["lazy", "autorun"]).prefault("lazy"), default_sql_output: z.enum(VALID_SQL_OUTPUT_FORMATS).prefault("auto"), + sql_keyword_case: z.enum(VALID_SQL_KEYWORD_CASES).prefault("upper"), default_auto_download: z .array(z.enum(AUTO_DOWNLOAD_FORMATS)) .prefault([]), diff --git a/marimo/_ai/_tools/tools/datasource.py b/marimo/_ai/_tools/tools/datasource.py index 231722d20a0..869a404034a 100644 --- a/marimo/_ai/_tools/tools/datasource.py +++ b/marimo/_ai/_tools/tools/datasource.py @@ -9,6 +9,7 @@ from marimo._ai._tools.base import ToolBase from marimo._ai._tools.types import SuccessResult, ToolGuidelines from marimo._ai._tools.utils.exceptions import ToolExecutionError +from marimo._config.config import SqlKeywordCase from marimo._data.models import DataTable from marimo._sql.engines.duckdb import INTERNAL_DUCKDB_ENGINE from marimo._types.ids import SessionId @@ -88,6 +89,9 @@ def _get_tables( ) tables: list[TableDetails] = [] + keyword_case = session.config_manager.get_config()["runtime"].get( + "sql_keyword_case", "upper" + ) # Pre-compile regex if query exists compiled_pattern = None @@ -113,6 +117,7 @@ def _get_tables( default_database=default_database, default_schema=default_schema, engine=connection.name, + keyword_case=keyword_case, ) tables.append( TableDetails( @@ -135,6 +140,7 @@ def _get_tables( default_database=default_database, default_schema=default_schema, engine=connection.name, + keyword_case=keyword_case, ) tables.append( TableDetails( @@ -162,12 +168,17 @@ def _form_sample_query( default_database: bool, default_schema: bool, engine: str, + keyword_case: SqlKeywordCase = "upper", ) -> str: - sample_query = f"SELECT * FROM {database}.{schema}.{table} LIMIT 100" + def kw(fragment: str) -> str: + return fragment.lower() if keyword_case == "lower" else fragment + + select_from, limit = kw("SELECT * FROM"), kw("LIMIT") + sample_query = f"{select_from} {database}.{schema}.{table} {limit} 100" if default_database: - sample_query = f"SELECT * FROM {schema}.{table} LIMIT 100" + sample_query = f"{select_from} {schema}.{table} {limit} 100" if default_schema: - sample_query = f"SELECT * FROM {table} LIMIT 100" + sample_query = f"{select_from} {table} {limit} 100" if engine != INTERNAL_DUCKDB_ENGINE: wrapped_query = ( f'df = mo.sql(f"""{sample_query}""", engine={engine})' diff --git a/marimo/_config/config.py b/marimo/_config/config.py index 1639bdf6950..dfbf6bb0188 100644 --- a/marimo/_config/config.py +++ b/marimo/_config/config.py @@ -124,6 +124,7 @@ class VenvConfig(TypedDict, total=False): Theme = Literal["light", "dark", "system"] ExportType = Literal["html", "markdown", "ipynb"] SqlOutputType = Literal["polars", "lazy-polars", "pandas", "native", "auto"] +SqlKeywordCase = Literal["upper", "lower"] StoreKey = Literal["file", "redis", "rest", "tiered"] @@ -167,6 +168,9 @@ class RuntimeConfig(TypedDict): - `default_sql_output`: the default output format for SQL queries. Can be one of: `"auto"`, `"native"`, `"polars"`, `"lazy-polars"`, or `"pandas"`. The default is `"auto"`. + - `sql_keyword_case`: the keyword case used in generated SQL, such as + cell boilerplate, autocomplete suggestions, and table snippets. + Can be `"upper"` or `"lower"`. The default is `"upper"`. - `default_auto_download`: an Optional list of export types to automatically snapshot your notebook as: `html`, `markdown`, `ipynb`. The default is None. @@ -188,6 +192,7 @@ class RuntimeConfig(TypedDict): pythonpath: NotRequired[list[str]] dotenv: NotRequired[list[str]] default_sql_output: SqlOutputType + sql_keyword_case: NotRequired[SqlKeywordCase] default_auto_download: NotRequired[list[ExportType]] default_csv_encoding: NotRequired[str] show_tracebacks: NotRequired[bool] @@ -782,6 +787,7 @@ class PartialMarimoConfig(TypedDict, total=False): os.getenv("MARIMO_STD_STREAM_MAX_BYTES", "1000000") ), "default_sql_output": "auto", + "sql_keyword_case": "upper", "default_csv_encoding": "utf-8", "show_tracebacks": False, }, diff --git a/packages/openapi/api.yaml b/packages/openapi/api.yaml index 93149a18a44..3f1fd99aac2 100644 --- a/packages/openapi/api.yaml +++ b/packages/openapi/api.yaml @@ -1590,6 +1590,18 @@ components: - label title: DetectedDataSourceOrigin type: object + DiagnosticsConfig: + description: "Configuration options for diagnostics.\n\n **Keys.**\n\n \ + \ - `enabled`: if `True`, diagnostics will be shown in the editor\n -\ + \ `sql_linter`: if `True`, SQL cells will have linting enabled" + properties: + enabled: + type: boolean + sql_linter: + type: boolean + required: [] + title: DiagnosticsConfig + type: object DialectHidesWhen: description: Hide this suggestion when a live SQL engine dialect contains a substring. @@ -1606,18 +1618,6 @@ components: - substrings title: DialectHidesWhen type: object - DiagnosticsConfig: - description: "Configuration options for diagnostics.\n\n **Keys.**\n\n \ - \ - `enabled`: if `True`, diagnostics will be shown in the editor\n -\ - \ `sql_linter`: if `True`, SQL cells will have linting enabled" - properties: - enabled: - type: boolean - sql_linter: - type: boolean - required: [] - title: DiagnosticsConfig - type: object DiscoverDataSourcesCommand: description: "Discover datasource connections from the live kernel environment\ \ and configuration.\n\n Attributes:\n request_id: Unique identifier\ @@ -4643,8 +4643,11 @@ components: \ is found, otherwise `[]`.\n - `default_sql_output`: the default output\ \ format for SQL queries. Can be one of:\n `\"auto\"`, `\"native\"\ `, `\"polars\"`, `\"lazy-polars\"`, or `\"pandas\"`.\n The default\ - \ is `\"auto\"`.\n - `default_auto_download`: an Optional list of export\ - \ types to automatically snapshot your notebook as:\n `html`, `markdown`,\ + \ is `\"auto\"`.\n - `sql_keyword_case`: the keyword case used in generated\ + \ SQL, such as\n cell boilerplate, autocomplete suggestions, and table\ + \ snippets.\n Can be `\"upper\"` or `\"lower\"`. The default is `\"\ + upper\"`.\n - `default_auto_download`: an Optional list of export types\ + \ to automatically snapshot your notebook as:\n `html`, `markdown`,\ \ `ipynb`.\n The default is None.\n - `default_csv_encoding`: the\ \ default encoding for CSV exports.\n The default is `\"utf-8\"`.\n\ \ - `show_tracebacks`: if `True`, show detailed error tracebacks in run\ @@ -4694,6 +4697,10 @@ components: type: boolean show_tracebacks: type: boolean + sql_keyword_case: + enum: + - lower + - upper std_stream_max_bytes: type: integer watcher_on_save: diff --git a/packages/openapi/src/api.ts b/packages/openapi/src/api.ts index 69006608a6d..e509340570f 100644 --- a/packages/openapi/src/api.ts +++ b/packages/openapi/src/api.ts @@ -4631,15 +4631,6 @@ export interface components { /** @enum {unknown} */ type: "configuration" | "environment"; }; - /** - * DialectHidesWhen - * @description Hide this suggestion when a live SQL engine dialect contains a substring. - */ - DialectHidesWhen: { - /** @enum {unknown} */ - kind: "dialect"; - substrings: string[]; - }; /** * DiagnosticsConfig * @description Configuration options for diagnostics. @@ -4653,6 +4644,15 @@ export interface components { enabled?: boolean; sql_linter?: boolean; }; + /** + * DialectHidesWhen + * @description Hide this suggestion when a live SQL engine dialect contains a substring. + */ + DialectHidesWhen: { + /** @enum {unknown} */ + kind: "dialect"; + substrings: string[]; + }; /** * DiscoverDataSourcesCommand * @description Discover datasource connections from the live kernel environment and configuration. @@ -6500,6 +6500,9 @@ export interface components { * - `default_sql_output`: the default output format for SQL queries. Can be one of: * `"auto"`, `"native"`, `"polars"`, `"lazy-polars"`, or `"pandas"`. * The default is `"auto"`. + * - `sql_keyword_case`: the keyword case used in generated SQL, such as + * cell boilerplate, autocomplete suggestions, and table snippets. + * Can be `"upper"` or `"lower"`. The default is `"upper"`. * - `default_auto_download`: an Optional list of export types to automatically snapshot your notebook as: * `html`, `markdown`, `ipynb`. * The default is None. @@ -6530,6 +6533,8 @@ export interface components { reactive_tests: boolean; serve_cached_sessions_in_apps?: boolean; show_tracebacks?: boolean; + /** @enum {unknown} */ + sql_keyword_case?: "lower" | "upper"; std_stream_max_bytes: number; /** @enum {unknown} */ watcher_on_save: "autorun" | "lazy"; diff --git a/tests/_ai/tools/test_utils.py b/tests/_ai/tools/test_utils.py index ae5c056f609..d3553f39b03 100644 --- a/tests/_ai/tools/test_utils.py +++ b/tests/_ai/tools/test_utils.py @@ -3,9 +3,11 @@ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Any +from marimo._config.config import DEFAULT_CONFIG + @dataclass class MockSessionView: @@ -25,11 +27,24 @@ def __post_init__(self) -> None: self.variable_values = {} +@dataclass +class MockConfigManager: + """Mock config manager for testing.""" + + config: Any = None + + def get_config(self) -> Any: + return self.config if self.config is not None else DEFAULT_CONFIG + + @dataclass class MockSession: """Mock session for testing.""" _session_view: MockSessionView + config_manager: MockConfigManager = field( + default_factory=MockConfigManager + ) @property def session_view(self) -> MockSessionView: diff --git a/tests/_ai/tools/tools/test_datasource_tool.py b/tests/_ai/tools/tools/test_datasource_tool.py index 15109e1a361..71d40799c27 100644 --- a/tests/_ai/tools/tools/test_datasource_tool.py +++ b/tests/_ai/tools/tools/test_datasource_tool.py @@ -2,6 +2,7 @@ from __future__ import annotations +from copy import deepcopy from dataclasses import dataclass import pytest @@ -13,10 +14,15 @@ TableDetails, ) from marimo._ai._tools.utils.exceptions import ToolExecutionError +from marimo._config.config import DEFAULT_CONFIG from marimo._data.models import Database, DataTable, DataTableColumn, Schema from marimo._messaging.notification import DataSourceConnectionsNotification from marimo._sql.engines.duckdb import INTERNAL_DUCKDB_ENGINE -from tests._ai.tools.test_utils import MockSession, MockSessionView +from tests._ai.tools.test_utils import ( + MockConfigManager, + MockSession, + MockSessionView, +) @dataclass @@ -658,3 +664,49 @@ def test_form_sample_query_internal_duckdb_with_defaults( ) assert query == 'df = mo.sql(f"""SELECT * FROM mytable LIMIT 100""")' + + +def test_form_sample_query_lower_keyword_case(tool: GetDatabaseTables): + """Test forming a sample query with lowercase keywords.""" + + query = tool._form_sample_query( + database="mydb", + schema="myschema", + table="mytable", + default_database=False, + default_schema=False, + engine=INTERNAL_DUCKDB_ENGINE, + keyword_case="lower", + ) + + assert ( + query + == 'df = mo.sql(f"""select * from mydb.myschema.mytable limit 100""")' + ) + + +def test_get_tables_lower_keyword_case( + tool: GetDatabaseTables, sample_session: MockSession +): + """Test that handle() respects a lowercase sql_keyword_case config.""" + config = deepcopy(DEFAULT_CONFIG) + config["runtime"]["sql_keyword_case"] = "lower" + sample_session.config_manager = MockConfigManager(config=config) + + def mock_get_session(_session_id): + return sample_session + + tool.context.get_session = mock_get_session + + args = GetDatabaseTablesArgs( + session_id="test_session", + query=None, + ) + + result = tool.handle(args) + + assert len(result.tables) == 1 + assert result.tables[0].sample_query == ( + 'df = mo.sql(f"""select * from test_db.public.users limit 100""", ' + "engine=postgres_conn)" + ) diff --git a/tests/_server/templates/snapshots/export1.txt b/tests/_server/templates/snapshots/export1.txt index 58f4db78fae..99a23be0d48 100644 --- a/tests/_server/templates/snapshots/export1.txt +++ b/tests/_server/templates/snapshots/export1.txt @@ -100,7 +100,7 @@ "mode": "read", "version": "0.0.0", "serverToken": "token", - "config": {"ai": {"custom_providers": {}, "enabled": true, "models": {"custom_models": [], "displayed_models": []}}, "completion": {"activate_on_typing": true, "auto_close_pairs": true, "copilot": false, "signature_hint_on_typing": false}, "diagnostics": {"sql_linter": true}, "display": {"cell_output": "below", "code_editor_font_size": 14, "code_lens": true, "dataframes": "rich", "default_table_max_columns": 50, "default_table_page_size": 10, "default_width": "medium", "reference_highlighting": true, "theme": "light"}, "formatting": {"line_length": 79}, "keymap": {"overrides": {}, "preset": "default"}, "language_servers": {"pylsp": {"enable_flake8": false, "enable_mypy": true, "enable_pydocstyle": false, "enable_pyflakes": false, "enable_pylint": false, "enable_ruff": true, "enabled": false}}, "mcp": {"mcpServers": {}, "presets": []}, "package_management": {"manager": "uv"}, "runtime": {"auto_instantiate": false, "auto_reload": "off", "default_csv_encoding": "utf-8", "default_sql_output": "auto", "on_cell_change": "autorun", "output_max_bytes": 8000000, "reactive_tests": true, "show_tracebacks": false, "std_stream_max_bytes": 1000000, "watcher_on_save": "lazy"}, "save": {"autosave": "after_delay", "autosave_delay": 1000, "format_on_save": false}, "server": {"browser": "default", "follow_symlink": false}, "snippets": {"custom_paths": [], "include_default_snippets": true}}, + "config": {"ai": {"custom_providers": {}, "enabled": true, "models": {"custom_models": [], "displayed_models": []}}, "completion": {"activate_on_typing": true, "auto_close_pairs": true, "copilot": false, "signature_hint_on_typing": false}, "diagnostics": {"sql_linter": true}, "display": {"cell_output": "below", "code_editor_font_size": 14, "code_lens": true, "dataframes": "rich", "default_table_max_columns": 50, "default_table_page_size": 10, "default_width": "medium", "reference_highlighting": true, "theme": "light"}, "formatting": {"line_length": 79}, "keymap": {"overrides": {}, "preset": "default"}, "language_servers": {"pylsp": {"enable_flake8": false, "enable_mypy": true, "enable_pydocstyle": false, "enable_pyflakes": false, "enable_pylint": false, "enable_ruff": true, "enabled": false}}, "mcp": {"mcpServers": {}, "presets": []}, "package_management": {"manager": "uv"}, "runtime": {"auto_instantiate": false, "auto_reload": "off", "default_csv_encoding": "utf-8", "default_sql_output": "auto", "on_cell_change": "autorun", "output_max_bytes": 8000000, "reactive_tests": true, "show_tracebacks": false, "sql_keyword_case": "upper", "std_stream_max_bytes": 1000000, "watcher_on_save": "lazy"}, "save": {"autosave": "after_delay", "autosave_delay": 1000, "format_on_save": false}, "server": {"browser": "default", "follow_symlink": false}, "snippets": {"custom_paths": [], "include_default_snippets": true}}, "configOverrides": {"formatting": {"line_length": 100}}, "appConfig": {"sql_output": "auto", "width": "compact"}, "view": {"showAppCode": true}, diff --git a/tests/_server/templates/snapshots/export2.txt b/tests/_server/templates/snapshots/export2.txt index 93a8770ae25..16a5135d86b 100644 --- a/tests/_server/templates/snapshots/export2.txt +++ b/tests/_server/templates/snapshots/export2.txt @@ -100,7 +100,7 @@ "mode": "read", "version": "0.0.0", "serverToken": "token", - "config": {"ai": {"custom_providers": {}, "enabled": true, "models": {"custom_models": [], "displayed_models": []}}, "completion": {"activate_on_typing": true, "auto_close_pairs": true, "copilot": false, "signature_hint_on_typing": false}, "diagnostics": {"sql_linter": true}, "display": {"cell_output": "below", "code_editor_font_size": 14, "code_lens": true, "dataframes": "rich", "default_table_max_columns": 50, "default_table_page_size": 10, "default_width": "medium", "reference_highlighting": true, "theme": "light"}, "formatting": {"line_length": 79}, "keymap": {"overrides": {}, "preset": "default"}, "language_servers": {"pylsp": {"enable_flake8": false, "enable_mypy": true, "enable_pydocstyle": false, "enable_pyflakes": false, "enable_pylint": false, "enable_ruff": true, "enabled": false}}, "mcp": {"mcpServers": {}, "presets": []}, "package_management": {"manager": "uv"}, "runtime": {"auto_instantiate": false, "auto_reload": "off", "default_csv_encoding": "utf-8", "default_sql_output": "auto", "on_cell_change": "autorun", "output_max_bytes": 8000000, "reactive_tests": true, "show_tracebacks": false, "std_stream_max_bytes": 1000000, "watcher_on_save": "lazy"}, "save": {"autosave": "after_delay", "autosave_delay": 1000, "format_on_save": false}, "server": {"browser": "default", "follow_symlink": false}, "snippets": {"custom_paths": [], "include_default_snippets": true}}, + "config": {"ai": {"custom_providers": {}, "enabled": true, "models": {"custom_models": [], "displayed_models": []}}, "completion": {"activate_on_typing": true, "auto_close_pairs": true, "copilot": false, "signature_hint_on_typing": false}, "diagnostics": {"sql_linter": true}, "display": {"cell_output": "below", "code_editor_font_size": 14, "code_lens": true, "dataframes": "rich", "default_table_max_columns": 50, "default_table_page_size": 10, "default_width": "medium", "reference_highlighting": true, "theme": "light"}, "formatting": {"line_length": 79}, "keymap": {"overrides": {}, "preset": "default"}, "language_servers": {"pylsp": {"enable_flake8": false, "enable_mypy": true, "enable_pydocstyle": false, "enable_pyflakes": false, "enable_pylint": false, "enable_ruff": true, "enabled": false}}, "mcp": {"mcpServers": {}, "presets": []}, "package_management": {"manager": "uv"}, "runtime": {"auto_instantiate": false, "auto_reload": "off", "default_csv_encoding": "utf-8", "default_sql_output": "auto", "on_cell_change": "autorun", "output_max_bytes": 8000000, "reactive_tests": true, "show_tracebacks": false, "sql_keyword_case": "upper", "std_stream_max_bytes": 1000000, "watcher_on_save": "lazy"}, "save": {"autosave": "after_delay", "autosave_delay": 1000, "format_on_save": false}, "server": {"browser": "default", "follow_symlink": false}, "snippets": {"custom_paths": [], "include_default_snippets": true}}, "configOverrides": {"formatting": {"line_length": 100}}, "appConfig": {"sql_output": "auto", "width": "compact"}, "view": {"showAppCode": true}, diff --git a/tests/_server/templates/snapshots/export3.txt b/tests/_server/templates/snapshots/export3.txt index 2ee0d354132..041e0779866 100644 --- a/tests/_server/templates/snapshots/export3.txt +++ b/tests/_server/templates/snapshots/export3.txt @@ -100,7 +100,7 @@ "mode": "read", "version": "0.0.0", "serverToken": "token", - "config": {"ai": {"custom_providers": {}, "enabled": true, "models": {"custom_models": [], "displayed_models": []}}, "completion": {"activate_on_typing": true, "auto_close_pairs": true, "copilot": false, "signature_hint_on_typing": false}, "diagnostics": {"sql_linter": true}, "display": {"cell_output": "below", "code_editor_font_size": 14, "code_lens": true, "dataframes": "rich", "default_table_max_columns": 50, "default_table_page_size": 10, "default_width": "medium", "reference_highlighting": true, "theme": "light"}, "formatting": {"line_length": 79}, "keymap": {"overrides": {}, "preset": "default"}, "language_servers": {"pylsp": {"enable_flake8": false, "enable_mypy": true, "enable_pydocstyle": false, "enable_pyflakes": false, "enable_pylint": false, "enable_ruff": true, "enabled": false}}, "mcp": {"mcpServers": {}, "presets": []}, "package_management": {"manager": "uv"}, "runtime": {"auto_instantiate": false, "auto_reload": "off", "default_csv_encoding": "utf-8", "default_sql_output": "auto", "on_cell_change": "autorun", "output_max_bytes": 8000000, "reactive_tests": true, "show_tracebacks": false, "std_stream_max_bytes": 1000000, "watcher_on_save": "lazy"}, "save": {"autosave": "after_delay", "autosave_delay": 1000, "format_on_save": false}, "server": {"browser": "default", "follow_symlink": false}, "snippets": {"custom_paths": [], "include_default_snippets": true}}, + "config": {"ai": {"custom_providers": {}, "enabled": true, "models": {"custom_models": [], "displayed_models": []}}, "completion": {"activate_on_typing": true, "auto_close_pairs": true, "copilot": false, "signature_hint_on_typing": false}, "diagnostics": {"sql_linter": true}, "display": {"cell_output": "below", "code_editor_font_size": 14, "code_lens": true, "dataframes": "rich", "default_table_max_columns": 50, "default_table_page_size": 10, "default_width": "medium", "reference_highlighting": true, "theme": "light"}, "formatting": {"line_length": 79}, "keymap": {"overrides": {}, "preset": "default"}, "language_servers": {"pylsp": {"enable_flake8": false, "enable_mypy": true, "enable_pydocstyle": false, "enable_pyflakes": false, "enable_pylint": false, "enable_ruff": true, "enabled": false}}, "mcp": {"mcpServers": {}, "presets": []}, "package_management": {"manager": "uv"}, "runtime": {"auto_instantiate": false, "auto_reload": "off", "default_csv_encoding": "utf-8", "default_sql_output": "auto", "on_cell_change": "autorun", "output_max_bytes": 8000000, "reactive_tests": true, "show_tracebacks": false, "sql_keyword_case": "upper", "std_stream_max_bytes": 1000000, "watcher_on_save": "lazy"}, "save": {"autosave": "after_delay", "autosave_delay": 1000, "format_on_save": false}, "server": {"browser": "default", "follow_symlink": false}, "snippets": {"custom_paths": [], "include_default_snippets": true}}, "configOverrides": {"formatting": {"line_length": 100}}, "appConfig": {"sql_output": "auto", "width": "compact"}, "view": {"showAppCode": true}, diff --git a/tests/_server/templates/snapshots/export4.txt b/tests/_server/templates/snapshots/export4.txt index fcfb41581b1..52847cc0558 100644 --- a/tests/_server/templates/snapshots/export4.txt +++ b/tests/_server/templates/snapshots/export4.txt @@ -100,7 +100,7 @@ "mode": "read", "version": "0.0.0", "serverToken": "token", - "config": {"ai": {"custom_providers": {}, "enabled": true, "models": {"custom_models": [], "displayed_models": []}}, "completion": {"activate_on_typing": true, "auto_close_pairs": true, "copilot": false, "signature_hint_on_typing": false}, "diagnostics": {"sql_linter": true}, "display": {"cell_output": "below", "code_editor_font_size": 14, "code_lens": true, "dataframes": "rich", "default_table_max_columns": 50, "default_table_page_size": 10, "default_width": "medium", "reference_highlighting": true, "theme": "light"}, "formatting": {"line_length": 79}, "keymap": {"overrides": {}, "preset": "default"}, "language_servers": {"pylsp": {"enable_flake8": false, "enable_mypy": true, "enable_pydocstyle": false, "enable_pyflakes": false, "enable_pylint": false, "enable_ruff": true, "enabled": false}}, "mcp": {"mcpServers": {}, "presets": []}, "package_management": {"manager": "uv"}, "runtime": {"auto_instantiate": false, "auto_reload": "off", "default_csv_encoding": "utf-8", "default_sql_output": "auto", "on_cell_change": "autorun", "output_max_bytes": 8000000, "reactive_tests": true, "show_tracebacks": false, "std_stream_max_bytes": 1000000, "watcher_on_save": "lazy"}, "save": {"autosave": "after_delay", "autosave_delay": 1000, "format_on_save": false}, "server": {"browser": "default", "follow_symlink": false}, "snippets": {"custom_paths": [], "include_default_snippets": true}}, + "config": {"ai": {"custom_providers": {}, "enabled": true, "models": {"custom_models": [], "displayed_models": []}}, "completion": {"activate_on_typing": true, "auto_close_pairs": true, "copilot": false, "signature_hint_on_typing": false}, "diagnostics": {"sql_linter": true}, "display": {"cell_output": "below", "code_editor_font_size": 14, "code_lens": true, "dataframes": "rich", "default_table_max_columns": 50, "default_table_page_size": 10, "default_width": "medium", "reference_highlighting": true, "theme": "light"}, "formatting": {"line_length": 79}, "keymap": {"overrides": {}, "preset": "default"}, "language_servers": {"pylsp": {"enable_flake8": false, "enable_mypy": true, "enable_pydocstyle": false, "enable_pyflakes": false, "enable_pylint": false, "enable_ruff": true, "enabled": false}}, "mcp": {"mcpServers": {}, "presets": []}, "package_management": {"manager": "uv"}, "runtime": {"auto_instantiate": false, "auto_reload": "off", "default_csv_encoding": "utf-8", "default_sql_output": "auto", "on_cell_change": "autorun", "output_max_bytes": 8000000, "reactive_tests": true, "show_tracebacks": false, "sql_keyword_case": "upper", "std_stream_max_bytes": 1000000, "watcher_on_save": "lazy"}, "save": {"autosave": "after_delay", "autosave_delay": 1000, "format_on_save": false}, "server": {"browser": "default", "follow_symlink": false}, "snippets": {"custom_paths": [], "include_default_snippets": true}}, "configOverrides": {"formatting": {"line_length": 100}}, "appConfig": {"css_file": "custom.css", "sql_output": "auto", "width": "compact"}, "view": {"showAppCode": true}, diff --git a/tests/_server/templates/snapshots/export5.txt b/tests/_server/templates/snapshots/export5.txt index a7462b01901..715fed3ea58 100644 --- a/tests/_server/templates/snapshots/export5.txt +++ b/tests/_server/templates/snapshots/export5.txt @@ -112,7 +112,7 @@ "mode": "read", "version": "0.0.0", "serverToken": "token", - "config": {"ai": {"custom_providers": {}, "enabled": true, "models": {"custom_models": [], "displayed_models": []}}, "completion": {"activate_on_typing": true, "auto_close_pairs": true, "copilot": false, "signature_hint_on_typing": false}, "diagnostics": {"sql_linter": true}, "display": {"cell_output": "below", "code_editor_font_size": 14, "code_lens": true, "dataframes": "rich", "default_table_max_columns": 50, "default_table_page_size": 10, "default_width": "medium", "reference_highlighting": true, "theme": "light"}, "formatting": {"line_length": 79}, "keymap": {"overrides": {}, "preset": "default"}, "language_servers": {"pylsp": {"enable_flake8": false, "enable_mypy": true, "enable_pydocstyle": false, "enable_pyflakes": false, "enable_pylint": false, "enable_ruff": true, "enabled": false}}, "mcp": {"mcpServers": {}, "presets": []}, "package_management": {"manager": "uv"}, "runtime": {"auto_instantiate": false, "auto_reload": "off", "default_csv_encoding": "utf-8", "default_sql_output": "auto", "on_cell_change": "autorun", "output_max_bytes": 8000000, "reactive_tests": true, "show_tracebacks": false, "std_stream_max_bytes": 1000000, "watcher_on_save": "lazy"}, "save": {"autosave": "after_delay", "autosave_delay": 1000, "format_on_save": false}, "server": {"browser": "default", "follow_symlink": false}, "snippets": {"custom_paths": [], "include_default_snippets": true}}, + "config": {"ai": {"custom_providers": {}, "enabled": true, "models": {"custom_models": [], "displayed_models": []}}, "completion": {"activate_on_typing": true, "auto_close_pairs": true, "copilot": false, "signature_hint_on_typing": false}, "diagnostics": {"sql_linter": true}, "display": {"cell_output": "below", "code_editor_font_size": 14, "code_lens": true, "dataframes": "rich", "default_table_max_columns": 50, "default_table_page_size": 10, "default_width": "medium", "reference_highlighting": true, "theme": "light"}, "formatting": {"line_length": 79}, "keymap": {"overrides": {}, "preset": "default"}, "language_servers": {"pylsp": {"enable_flake8": false, "enable_mypy": true, "enable_pydocstyle": false, "enable_pyflakes": false, "enable_pylint": false, "enable_ruff": true, "enabled": false}}, "mcp": {"mcpServers": {}, "presets": []}, "package_management": {"manager": "uv"}, "runtime": {"auto_instantiate": false, "auto_reload": "off", "default_csv_encoding": "utf-8", "default_sql_output": "auto", "on_cell_change": "autorun", "output_max_bytes": 8000000, "reactive_tests": true, "show_tracebacks": false, "sql_keyword_case": "upper", "std_stream_max_bytes": 1000000, "watcher_on_save": "lazy"}, "save": {"autosave": "after_delay", "autosave_delay": 1000, "format_on_save": false}, "server": {"browser": "default", "follow_symlink": false}, "snippets": {"custom_paths": [], "include_default_snippets": true}}, "configOverrides": {"formatting": {"line_length": 100}}, "appConfig": {"app_title": "My App", "html_head_file": "head.html", "sql_output": "auto", "width": "compact"}, "view": {"showAppCode": true}, diff --git a/tests/_server/templates/snapshots/export6.txt b/tests/_server/templates/snapshots/export6.txt index 793397b4b59..5054531ba73 100644 --- a/tests/_server/templates/snapshots/export6.txt +++ b/tests/_server/templates/snapshots/export6.txt @@ -101,7 +101,7 @@ "mode": "read", "version": "0.0.0", "serverToken": "token", - "config": {"ai": {"custom_providers": {}, "enabled": true, "models": {"custom_models": [], "displayed_models": []}}, "completion": {"activate_on_typing": true, "auto_close_pairs": true, "copilot": false, "signature_hint_on_typing": false}, "diagnostics": {"sql_linter": true}, "display": {"cell_output": "below", "code_editor_font_size": 14, "code_lens": true, "custom_css": ["custom1.css", "custom2.css"], "dataframes": "rich", "default_table_max_columns": 50, "default_table_page_size": 10, "default_width": "medium", "reference_highlighting": true, "theme": "light"}, "formatting": {"line_length": 79}, "keymap": {"overrides": {}, "preset": "default"}, "language_servers": {"pylsp": {"enable_flake8": false, "enable_mypy": true, "enable_pydocstyle": false, "enable_pyflakes": false, "enable_pylint": false, "enable_ruff": true, "enabled": false}}, "mcp": {"mcpServers": {}, "presets": []}, "package_management": {"manager": "uv"}, "runtime": {"auto_instantiate": false, "auto_reload": "off", "default_csv_encoding": "utf-8", "default_sql_output": "auto", "on_cell_change": "autorun", "output_max_bytes": 8000000, "reactive_tests": true, "show_tracebacks": false, "std_stream_max_bytes": 1000000, "watcher_on_save": "lazy"}, "save": {"autosave": "after_delay", "autosave_delay": 1000, "format_on_save": false}, "server": {"browser": "default", "follow_symlink": false}, "snippets": {"custom_paths": [], "include_default_snippets": true}}, + "config": {"ai": {"custom_providers": {}, "enabled": true, "models": {"custom_models": [], "displayed_models": []}}, "completion": {"activate_on_typing": true, "auto_close_pairs": true, "copilot": false, "signature_hint_on_typing": false}, "diagnostics": {"sql_linter": true}, "display": {"cell_output": "below", "code_editor_font_size": 14, "code_lens": true, "custom_css": ["custom1.css", "custom2.css"], "dataframes": "rich", "default_table_max_columns": 50, "default_table_page_size": 10, "default_width": "medium", "reference_highlighting": true, "theme": "light"}, "formatting": {"line_length": 79}, "keymap": {"overrides": {}, "preset": "default"}, "language_servers": {"pylsp": {"enable_flake8": false, "enable_mypy": true, "enable_pydocstyle": false, "enable_pyflakes": false, "enable_pylint": false, "enable_ruff": true, "enabled": false}}, "mcp": {"mcpServers": {}, "presets": []}, "package_management": {"manager": "uv"}, "runtime": {"auto_instantiate": false, "auto_reload": "off", "default_csv_encoding": "utf-8", "default_sql_output": "auto", "on_cell_change": "autorun", "output_max_bytes": 8000000, "reactive_tests": true, "show_tracebacks": false, "sql_keyword_case": "upper", "std_stream_max_bytes": 1000000, "watcher_on_save": "lazy"}, "save": {"autosave": "after_delay", "autosave_delay": 1000, "format_on_save": false}, "server": {"browser": "default", "follow_symlink": false}, "snippets": {"custom_paths": [], "include_default_snippets": true}}, "configOverrides": {"formatting": {"line_length": 100}}, "appConfig": {"sql_output": "auto", "width": "compact"}, "view": {"showAppCode": true},