Skip to content
Snippets Groups Projects
Unverified Commit 2b9e8167 authored by Nemanja Glumac's avatar Nemanja Glumac Committed by GitHub
Browse files

Consolidate `engine` utilities (#42211)

* Remove unused functions

* Remove `export` from a local function

* Remove googleanalytics related engine code

* Use `getEngines` selector instead of the deprecated Metabase `Settings`

* Use `getDefaultEngineKey`

* Use `formatSQL` function locally

* Remove unused `getEngineLogo` function

* Convert to TS

* Remove unused export

* Fix types

* Expand test coverage

* Update type

* Remove `googleanalitics` from an assertion

* Address review comment

* Expand types

* Add tests for `isDeprecatedEngine`

* Expand tests
parent 3a33bbac
No related branches found
No related tags found
No related merge requests found
import { getEngines } from "metabase/databases/selectors";
import { isDeprecatedEngine } from "metabase/lib/engine";
import { getSetting } from "metabase/selectors/settings";
import type Database from "metabase-lib/v1/metadata/Database";
......@@ -16,9 +17,11 @@ export const isNoticeEnabled = (state: State): boolean => {
};
export const hasDeprecatedDatabase = (state: State, props: Props): boolean => {
const engines = getEngines(state);
return (
props.databases?.some(d => !d.is_sample && isDeprecatedEngine(d.engine)) ??
false
props.databases?.some(
d => !d.is_sample && d.engine && isDeprecatedEngine(engines, d.engine),
) ?? false
);
};
......
......@@ -3,9 +3,10 @@ import { createAction } from "redux-actions";
import _ from "underscore";
import { updateSetting } from "metabase/admin/settings/settings";
import { getEngines } from "metabase/databases/selectors";
import { getDefaultEngineKey } from "metabase/databases/utils/engine";
import Databases from "metabase/entities/databases";
import * as MetabaseAnalytics from "metabase/lib/analytics";
import { getDefaultEngine } from "metabase/lib/engine";
import {
combineReducers,
createThunkAction,
......@@ -117,10 +118,11 @@ export const initializeDatabase = function (databaseId) {
dispatch({ type: INITIALIZE_DATABASE_ERROR, payload: error });
}
} else {
const engines = getEngines(getState());
const newDatabase = {
name: "",
auto_run_queries: true,
engine: getDefaultEngine(),
engine: getDefaultEngineKey(engines),
details: {},
created: false,
};
......
import { formatSQL } from "metabase/lib/formatting";
import Settings from "metabase/lib/settings";
export function getDefaultEngine() {
const engines = Object.keys(Settings.get("engines"));
return engines.includes("postgres") ? "postgres" : engines[0];
}
export function getEngineNativeType(engine) {
switch (engine) {
case "mongo":
case "druid":
case "googleanalytics":
return "json";
default:
return "sql";
}
}
export function getNativeQueryLanguage(engine) {
return getEngineNativeType(engine).toUpperCase();
}
export function getEngineNativeAceMode(engine) {
switch (engine) {
case "mongo":
case "druid":
case "googleanalytics":
return "ace/mode/json";
default:
return "ace/mode/sql";
}
}
export function getEngineLogo(engine) {
const path = `app/assets/img/drivers`;
switch (engine) {
case "bigquery":
case "druid":
case "googleanalytics":
case "h2":
case "mongo":
case "mysql":
case "oracle":
case "postgres":
case "redshift":
case "snowflake":
case "sparksql":
case "sqlite":
case "sqlserver":
case "vertica":
return `${path}/${engine}.svg`;
case "bigquery-cloud-sdk":
return `${path}/bigquery.svg`;
case "presto-jdbc":
return `${path}/presto.svg`;
case "starburst":
return `${path}/starburst.svg`;
case "materialize":
return `${path}/materialize.svg`;
}
}
export function getElevatedEngines() {
return [
"mysql",
"postgres",
"sqlserver",
"redshift",
"bigquery-cloud-sdk",
"snowflake",
];
}
export function getEngineSupportsFirewall(engine) {
return engine !== "googleanalytics";
}
export function formatJsonQuery(query, engine) {
if (engine === "googleanalytics") {
return formatGAQuery(query);
}
return JSON.stringify(query, null, 2);
}
export function formatNativeQuery(query, engine) {
return getEngineNativeType(engine) === "json"
? formatJsonQuery(query, engine)
: formatSQL(query);
}
export function isDeprecatedEngine(engine) {
const engines = Settings.get("engines", {});
return engines[engine] != null && engines[engine]["superseded-by"] != null;
}
const GA_ORDERED_PARAMS = [
"ids",
"start-date",
"end-date",
"metrics",
"dimensions",
"sort",
"filters",
"segment",
"samplingLevel",
"include-empty-rows",
"start-index",
"max-results",
];
// does 3 things: removes null values, sorts the keys by the order in the documentation, and formats with 2 space indents
function formatGAQuery(query) {
if (!query) {
return "";
}
const object = {};
for (const param of GA_ORDERED_PARAMS) {
if (query[param] != null) {
object[param] = query[param];
}
}
return JSON.stringify(object, null, 2);
}
import type { Engine } from "metabase-types/api";
export function getEngineNativeType(engine?: string): "sql" | "json" {
switch (engine) {
case "mongo":
case "druid":
return "json";
default:
return "sql";
}
}
export function getNativeQueryLanguage(engine?: string) {
return getEngineNativeType(engine).toUpperCase() as "SQL" | "JSON";
}
export function getEngineNativeAceMode(engine?: string | null) {
switch (engine) {
case "mongo":
case "druid":
return "ace/mode/json";
default:
return "ace/mode/sql";
}
}
type JSONQuery = Record<string, any> | Record<string, any>[];
function formatJsonQuery(query: JSONQuery) {
return JSON.stringify(query, null, 2);
}
export function formatNativeQuery(query?: string | JSONQuery, engine?: string) {
if (!query || !engine) {
return undefined;
}
const engineType = getEngineNativeType(engine);
if (typeof query === "string" && engineType === "sql") {
return formatSQL(query);
}
if (typeof query === "object" && engineType === "json") {
return formatJsonQuery(query);
}
return undefined;
}
export function isDeprecatedEngine(
engines: Record<string, Engine> = {},
engine: string,
) {
return engines[engine] != null && engines[engine]["superseded-by"] != null;
}
function formatSQL(sql: string) {
if (typeof sql === "string") {
sql = sql.replace(/\sFROM/, "\nFROM");
sql = sql.replace(/\sLEFT JOIN/, "\nLEFT JOIN");
sql = sql.replace(/\sWHERE/, "\nWHERE");
sql = sql.replace(/\sGROUP BY/, "\nGROUP BY");
sql = sql.replace(/\sORDER BY/, "\nORDER BY");
sql = sql.replace(/\sLIMIT/, "\nLIMIT");
sql = sql.replace(/\sAND\s/, "\n AND ");
sql = sql.replace(/\sOR\s/, "\n OR ");
return sql;
}
}
import { getEngineNativeAceMode } from "metabase/lib/engine";
import {
getEngineNativeAceMode,
getEngineNativeType,
getNativeQueryLanguage,
formatNativeQuery,
isDeprecatedEngine,
} from "metabase/lib/engine";
import type { Engine } from "metabase-types/api";
describe("getEngineNativeAceMode", () => {
it("should be SQL when engine is undefined", () => {
expect(getEngineNativeAceMode()).toBe("ace/mode/sql");
});
it("should be SQL mode for H2", () => {
expect(getEngineNativeAceMode("h2")).toBe("ace/mode/sql");
});
......@@ -9,3 +20,135 @@ describe("getEngineNativeAceMode", () => {
expect(getEngineNativeAceMode("mongo")).toBe("ace/mode/json");
});
});
describe("getEngineNativeType", () => {
it("should be sql when engine is undefined", () => {
expect(getEngineNativeType()).toBe("sql");
});
it("should be sql for Postgres", () => {
expect(getEngineNativeType("postgres")).toBe("sql");
});
it("should be json for Druid or MongoDB", () => {
expect(getEngineNativeType("druid")).toBe("json");
expect(getEngineNativeType("mongo")).toBe("json");
});
});
describe("getNativeQueryLanguage", () => {
it("should be SQL when engine is undefined", () => {
expect(getNativeQueryLanguage()).toBe("SQL");
});
it("should be SQL for Postgres", () => {
expect(getNativeQueryLanguage("postgres")).toBe("SQL");
});
it("should be JSON for Druid or MongoDB", () => {
expect(getNativeQueryLanguage("druid")).toBe("JSON");
expect(getNativeQueryLanguage("mongo")).toBe("JSON");
});
});
describe("formatNativeQuery", () => {
it("should return `undefined` when neither query nor engine are provided", () => {
expect(formatNativeQuery()).toBeUndefined();
});
it("should return `undefined` when the query exists, but engine is `undefined`", () => {
expect(formatNativeQuery("")).toBeUndefined();
expect(formatNativeQuery("select 1")).toBeUndefined();
expect(formatNativeQuery({})).toBeUndefined();
expect(formatNativeQuery([])).toBeUndefined();
expect(formatNativeQuery([{}, { a: 1 }])).toBeUndefined();
});
it("should return `undefined` when the query exists, but engine is an empty string", () => {
expect(formatNativeQuery("", "")).toBeUndefined();
expect(formatNativeQuery("select 1", "")).toBeUndefined();
expect(formatNativeQuery({}, "")).toBeUndefined();
expect(formatNativeQuery([], "")).toBeUndefined();
expect(formatNativeQuery([{}, { a: 1 }], "")).toBeUndefined();
});
it("should return `undefined` when engine is passed as the only argument", () => {
// Because parameters are positional, passing an engine as the only argument
// will be treated as a query, and the engine will be `undefined`.
expect(formatNativeQuery("mongo")).toBeUndefined();
expect(formatNativeQuery("postgres")).toBeUndefined();
expect(formatNativeQuery("mongo", "")).toBeUndefined();
expect(formatNativeQuery("postgres", "")).toBeUndefined();
});
it("should return `undefined` when the query and the engine don't match", () => {
expect(formatNativeQuery("select 1", "mongo")).toBeUndefined();
expect(formatNativeQuery("foo bar baz", "mongo")).toBeUndefined();
expect(formatNativeQuery("", "mongo")).toBeUndefined();
expect(formatNativeQuery({}, "postgres")).toBeUndefined();
expect(formatNativeQuery([], "postgres")).toBeUndefined();
expect(formatNativeQuery([{}], "postgres")).toBeUndefined();
expect(formatNativeQuery({ a: 1 }, "postgres")).toBeUndefined();
expect(formatNativeQuery([{ a: 1 }], "postgres")).toBeUndefined();
});
it("should return formatted SQL", () => {
expect(formatNativeQuery("select 1", "postgres")).toEqual("select 1");
expect(
formatNativeQuery("SELECT * FROM PUBLIC.ORDERS", "postgres"),
).toEqual("SELECT *\nFROM PUBLIC.ORDERS");
});
it("should return any valid string if the engine type is sql", () => {
expect(formatNativeQuery("foo", "postgres")).toEqual("foo");
expect(formatNativeQuery("FOO BAR baz", "postgres")).toEqual("FOO BAR baz");
expect(formatNativeQuery("FOO: BAR, baz.", "postgres")).toEqual(
"FOO: BAR, baz.",
);
expect(formatNativeQuery("-- foo", "postgres")).toEqual("-- foo");
});
it("should return formatted JSON", () => {
expect(formatNativeQuery({}, "mongo")).toEqual("{}");
expect(formatNativeQuery([], "mongo")).toEqual("[]");
expect(formatNativeQuery(["foo"], "mongo")).toEqual('[\n "foo"\n]');
expect(formatNativeQuery({ a: 1 }, "mongo")).toEqual('{\n "a": 1\n}');
});
});
describe("isDeprecatedEngine", () => {
const engines: Record<string, Engine> = {
foo: {
"driver-name": "Foo",
source: { type: "official", contact: null },
"superseded-by": "deprecated",
},
bar: {
"driver-name": "Bar",
source: { type: "official", contact: null },
"superseded-by": "baz",
},
baz: {
"driver-name": "Baz",
source: { type: "official", contact: null },
"superseded-by": null,
},
};
it("should be true for a deprecated engine", () => {
expect(isDeprecatedEngine(engines, "foo")).toBe(true);
expect(isDeprecatedEngine(engines, "bar")).toBe(true);
});
it("should be false for an engine that's not deprecated", () => {
expect(isDeprecatedEngine(engines, "baz")).toBe(false);
});
it("should be false if an engine doesn't exist", () => {
expect(isDeprecatedEngine(engines, "buzzzzz")).toBe(false);
});
});
......@@ -7,7 +7,6 @@ export * from "./formatting/field";
export * from "./formatting/geography";
export * from "./formatting/image";
export * from "./formatting/numbers";
export * from "./formatting/sql";
export * from "./formatting/strings";
export * from "./formatting/time";
export * from "./formatting/url";
......
export function formatSQL(sql: string) {
if (typeof sql === "string") {
sql = sql.replace(/\sFROM/, "\nFROM");
sql = sql.replace(/\sLEFT JOIN/, "\nLEFT JOIN");
sql = sql.replace(/\sWHERE/, "\nWHERE");
sql = sql.replace(/\sGROUP BY/, "\nGROUP BY");
sql = sql.replace(/\sORDER BY/, "\nORDER BY");
sql = sql.replace(/\sLIMIT/, "\nLIMIT");
sql = sql.replace(/\sAND\s/, "\n AND ");
sql = sql.replace(/\sOR\s/, "\n OR ");
return sql;
}
}
......@@ -194,7 +194,6 @@ describe("utils", () => {
it("should return false for non-SQL engines and unsupported SQL engines", () => {
expect(canFormatForEngine("mongo")).toBe(false);
expect(canFormatForEngine("googleanalytics")).toBe(false);
expect(canFormatForEngine("druid")).toBe(false);
expect(canFormatForEngine("sqlite")).toBe(false);
expect(canFormatForEngine("sqlserver")).toBe(false);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment