Compare commits
204 Commits
proj-pytho
...
python-run
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c72149945 | ||
|
|
350e69e4e9 | ||
|
|
ad9e3af2cd | ||
|
|
9bc2f603e2 | ||
|
|
b9e1f5774d | ||
|
|
0e93fb8dbc | ||
|
|
92e9305d20 | ||
|
|
df4a0869b5 | ||
|
|
581c1e7a38 | ||
|
|
17a61a81c8 | ||
|
|
1d699a5fae | ||
|
|
277d1fdaff | ||
|
|
82b3ba1fcc | ||
|
|
dd7845cc83 | ||
|
|
7e7ed1ac9f | ||
|
|
19387d8d29 | ||
|
|
7ac8feb49b | ||
|
|
4447e2c6bc | ||
|
|
8434b23764 | ||
|
|
be72407fc8 | ||
|
|
e530911305 | ||
|
|
ca9ba3390d | ||
|
|
a947f3cb94 | ||
|
|
33de954fcf | ||
|
|
db79c64d3d | ||
|
|
3e646a69f3 | ||
|
|
d6c15aab01 | ||
|
|
34c224b5f2 | ||
|
|
568443a99c | ||
|
|
5fe1efe15b | ||
|
|
894176cf26 | ||
|
|
c28f66c961 | ||
|
|
85e6e7f779 | ||
|
|
d44a2cab66 | ||
|
|
e349faa741 | ||
|
|
817af7b71d | ||
|
|
e8cd58b376 | ||
|
|
62d7beec2b | ||
|
|
c2ec56beed | ||
|
|
5274867997 | ||
|
|
b5a9d7457f | ||
|
|
12f4387864 | ||
|
|
6b5e3bd646 | ||
|
|
b7c4063dd9 | ||
|
|
49fcc4a40f | ||
|
|
7fb62b782b | ||
|
|
f393237eb9 | ||
|
|
48376d3f63 | ||
|
|
a67c48a036 | ||
|
|
8c3d77473a | ||
|
|
9f029dbb2f | ||
|
|
1adaaae66e | ||
|
|
21956d6ab7 | ||
|
|
fd94a0b0cb | ||
|
|
7b6fbd6374 | ||
|
|
eeecd9a10a | ||
|
|
685424df74 | ||
|
|
c4af9c415b | ||
|
|
ca66414190 | ||
|
|
855e846d0a | ||
|
|
270086315f | ||
|
|
16ea0dc141 | ||
|
|
94b9737f0b | ||
|
|
4448933543 | ||
|
|
a9e0afd420 | ||
|
|
d018b4a1bc | ||
|
|
92011484de | ||
|
|
c10fd5cd5b | ||
|
|
564556d706 | ||
|
|
5e55ecd75c | ||
|
|
0a33f82baf | ||
|
|
7ecdc2f757 | ||
|
|
0bf1ac867f | ||
|
|
cbdc23a7bf | ||
|
|
c59e9de090 | ||
|
|
9668ebe98c | ||
|
|
3c2d46151b | ||
|
|
1cacd1f510 | ||
|
|
a8074ca04b | ||
|
|
28014961c3 | ||
|
|
ec5b20af0c | ||
|
|
ab68e21cf7 | ||
|
|
821c0f624b | ||
|
|
7423aaf572 | ||
|
|
03f68f0a2c | ||
|
|
e8f125c19b | ||
|
|
21c5271879 | ||
|
|
45d701fd6b | ||
|
|
7c1152e9d0 | ||
|
|
ab20852102 | ||
|
|
137bc61a42 | ||
|
|
f567aeece1 | ||
|
|
e60e38682b | ||
|
|
84efc79be4 | ||
|
|
8de8d13eb6 | ||
|
|
a52d58a3e2 | ||
|
|
f34cb3d5c3 | ||
|
|
d8571ccabe | ||
|
|
19f279610b | ||
|
|
5647233576 | ||
|
|
14481bcf2f | ||
|
|
bbccbb4842 | ||
|
|
4b99060f46 | ||
|
|
3903af487f | ||
|
|
0fb061b839 | ||
|
|
ab9e7bbcb8 | ||
|
|
a137631b7f | ||
|
|
e4a04ebc05 | ||
|
|
432287f763 | ||
|
|
d8be52f651 | ||
|
|
6516199e3f | ||
|
|
1bba35237e | ||
|
|
4337a1baf3 | ||
|
|
23cb8b11c0 | ||
|
|
c863f41c2a | ||
|
|
34e7a0e9f1 | ||
|
|
454a05e3c6 | ||
|
|
1ed050b03d | ||
|
|
9041dbb329 | ||
|
|
63cc5c3137 | ||
|
|
7551f766dd | ||
|
|
f4b675b3f2 | ||
|
|
123e0cef51 | ||
|
|
eed3bb1e19 | ||
|
|
dad9b5c709 | ||
|
|
166e20c944 | ||
|
|
e4d0318213 | ||
|
|
3913c6e850 | ||
|
|
f6384ac6f3 | ||
|
|
bd6d5e527b | ||
|
|
4dac7d9dcf | ||
|
|
5bda80e53f | ||
|
|
07a34dc556 | ||
|
|
3a4e2a311c | ||
|
|
f0427f0a34 | ||
|
|
ebcdcf1ce9 | ||
|
|
20afc08468 | ||
|
|
c60b139cf4 | ||
|
|
cb7e5a0437 | ||
|
|
1e4e4bcae5 | ||
|
|
f65c0fb194 | ||
|
|
fa187a027d | ||
|
|
d25a14aec8 | ||
|
|
4a59711053 | ||
|
|
c33489af35 | ||
|
|
d6c7ef8e63 | ||
|
|
6e7aeb6d71 | ||
|
|
6db1e7e2ad | ||
|
|
f901d08b1c | ||
|
|
5bc7d211ba | ||
|
|
d04ffc2925 | ||
|
|
c471c32f3f | ||
|
|
d867a09072 | ||
|
|
090fbc2d5a | ||
|
|
33fe7b3afe | ||
|
|
ea7eeeb15e | ||
|
|
b693b137f6 | ||
|
|
1a6044bbb3 | ||
|
|
e5706a4def | ||
|
|
0014bbc775 | ||
|
|
b96b8fbead | ||
|
|
7e09b87447 | ||
|
|
102025c69c | ||
|
|
80b31c4b1e | ||
|
|
d4adbb7ef0 | ||
|
|
04192e385d | ||
|
|
18b9ae17c7 | ||
|
|
b69218fe00 | ||
|
|
61a8141a44 | ||
|
|
2ed9e3e1d4 | ||
|
|
a1a9e71867 | ||
|
|
0ed177ddb1 | ||
|
|
814dd5c800 | ||
|
|
8285e38f9c | ||
|
|
8ad7ee944d | ||
|
|
975002a6f7 | ||
|
|
3fddbec54e | ||
|
|
c3461ab646 | ||
|
|
b5878d9bf5 | ||
|
|
6206b6f204 | ||
|
|
4ef96bd73a | ||
|
|
b336da1517 | ||
|
|
c98dca560d | ||
|
|
e370820778 | ||
|
|
69405a7dea | ||
|
|
c0ca9e0493 | ||
|
|
ced8d84def | ||
|
|
8c8279e570 | ||
|
|
6afe00b9f1 | ||
|
|
6e8cca3da7 | ||
|
|
a1582d07de | ||
|
|
40387cc586 | ||
|
|
911da7f851 | ||
|
|
4deefb87b3 | ||
|
|
c56432c2f7 | ||
|
|
f9e9a20d94 | ||
|
|
5d68e7363f | ||
|
|
0efa8eac5b | ||
|
|
6e3dfdb62b | ||
|
|
e915bf2d40 | ||
|
|
118613ab89 | ||
|
|
4f764b82ef | ||
|
|
ce5a554d4d | ||
|
|
eb5100abc6 |
@@ -1,7 +1,7 @@
|
||||
;; -*- comment-column: 100; -*-
|
||||
{:config-paths ["macros" "src" "config"]
|
||||
:linters
|
||||
{;;
|
||||
{ ;;
|
||||
;; enabled optional linters (these are disabled by default)
|
||||
;;
|
||||
|
||||
@@ -187,7 +187,7 @@
|
||||
:metabase/disallow-hardcoded-driver-names-in-tests
|
||||
;; don't include `:h2` since that will probably lead to a mountain of tests where we are specifically testing
|
||||
;; middleware or compilation behavior with just H2 since it's the default.
|
||||
{:drivers #{;; core drivers
|
||||
{:drivers #{ ;; core drivers
|
||||
#_:h2
|
||||
:postgres
|
||||
:mysql
|
||||
@@ -627,7 +627,7 @@
|
||||
metabase.test/with-temp hooks.toucan2.tools.with-temp/with-temp
|
||||
metabase.test/with-temp-file hooks.metabase.test.util/with-temp-file
|
||||
metabase.test/with-temporary-setting-values hooks.metabase.test.util/with-temporary-setting-values
|
||||
metabase.users.api-test/with-temp-user-email! hooks.common/with-one-binding
|
||||
metabase.users.api-test/with-temp-user-email! hooks.common/with-one-binding
|
||||
metabase.util.i18n/deferred-trs hooks.metabase.util.i18n/deferred-trs
|
||||
metabase.util.i18n/deferred-tru hooks.metabase.util.i18n/deferred-tru
|
||||
metabase.util.i18n/trs hooks.metabase.util.i18n/trs
|
||||
@@ -742,9 +742,7 @@
|
||||
;; `metabase-enterprise.sandbox.query-processor*-test`
|
||||
;;
|
||||
{:pattern "^metabase(?:((?:-enterprise\\.sandbox)?\\.query-processor)|(?:\\.driver\\.sql(?!server)(?!ite))).*-test$"
|
||||
:name general-qp-and-driver-test-namespaces}
|
||||
{:pattern "^metabase\\.python-runner\\..*"
|
||||
:name python-runner-namespaces}]
|
||||
:name general-qp-and-driver-test-namespaces}]
|
||||
|
||||
:config-in-ns
|
||||
{test-namespaces
|
||||
@@ -769,8 +767,8 @@
|
||||
source-namespaces
|
||||
{:linters
|
||||
{:metabase/validate-escaped-single-quotes-in-i18n {:level :warning}
|
||||
:metabase/validate-string-or-str-args-to-i18n {:level :warning}
|
||||
:metabase/discourage-millis-duration {:level :warning}
|
||||
:metabase/validate-string-or-str-args-to-i18n {:level :warning}
|
||||
:metabase/discourage-millis-duration {:level :warning}
|
||||
|
||||
:discouraged-var
|
||||
{clojure.core/with-redefs {:message "Don't use with-redefs outside of tests"}
|
||||
@@ -837,7 +835,16 @@
|
||||
performance-sensitive
|
||||
{:linters
|
||||
{:discouraged-var
|
||||
{clojure.core/update-keys {:message "Use metabase.util.performance/update-keys"}}}}
|
||||
{clojure.core/every? {:message "Use metabase.util.performance/every?"}
|
||||
clojure.core/mapv {:message "Use metabase.util.performance/mapv"}
|
||||
clojure.core/run! {:message "Use metabase.util.performance/run!"}
|
||||
clojure.core/select-keys {:message "Use metabase.util.performance/select-keys"}
|
||||
clojure.core/some {:message "Use metabase.util.performance/some"}
|
||||
clojure.core/update-keys {:message "Use metabase.util.performance/update-keys"}
|
||||
clojure.walk/keywordize-keys {:message "Use metabase.util.performance/keywordize-keys"}
|
||||
clojure.walk/postwalk {:message "Use metabase.util.performance/postwalk"}
|
||||
clojure.walk/prewalk {:message "Use metabase.util.performance/prewalk"}
|
||||
clojure.walk/walk {:message "Use metabase.util.performance/walk"}}}}
|
||||
|
||||
qp-and-driver-source-namespaces
|
||||
{:linters
|
||||
|
||||
@@ -502,6 +502,7 @@
|
||||
metabase.embedding.init
|
||||
metabase.embedding.jwt
|
||||
metabase.embedding.settings
|
||||
metabase.embedding.util
|
||||
metabase.embedding.validation}
|
||||
:uses #{analytics
|
||||
api
|
||||
@@ -516,6 +517,7 @@
|
||||
public-sharing
|
||||
queries
|
||||
query-processor
|
||||
request
|
||||
server
|
||||
settings
|
||||
tiles
|
||||
@@ -547,6 +549,13 @@
|
||||
settings
|
||||
util}}
|
||||
|
||||
glossary
|
||||
{:team "Workflows"
|
||||
:api #{metabase.glossary.api}
|
||||
:uses #{api
|
||||
models
|
||||
util}}
|
||||
|
||||
indexed-entities
|
||||
{:team "UX West"
|
||||
:api #{metabase.indexed-entities.api
|
||||
@@ -943,6 +952,7 @@
|
||||
:uses #{api
|
||||
legacy-mbql
|
||||
lib
|
||||
lib-be
|
||||
models
|
||||
permissions
|
||||
query-processor
|
||||
@@ -1025,6 +1035,7 @@
|
||||
metabase.request.schema}
|
||||
:uses #{api
|
||||
config
|
||||
embedding
|
||||
permissions ; FIXME
|
||||
session
|
||||
settings
|
||||
@@ -1230,6 +1241,7 @@
|
||||
events
|
||||
lib
|
||||
models
|
||||
premium-features
|
||||
query-processor
|
||||
settings
|
||||
task
|
||||
@@ -1475,6 +1487,7 @@
|
||||
driver
|
||||
legacy-mbql
|
||||
lib
|
||||
lib-be
|
||||
models
|
||||
queries
|
||||
query-permissions
|
||||
@@ -1630,6 +1643,7 @@
|
||||
:api #{metabase-enterprise.comments.api
|
||||
metabase-enterprise.comments.init}
|
||||
:uses #{api
|
||||
channel
|
||||
events
|
||||
models
|
||||
request
|
||||
@@ -1817,10 +1831,12 @@
|
||||
pulse
|
||||
query-processor
|
||||
request
|
||||
search
|
||||
server
|
||||
settings
|
||||
sync
|
||||
system
|
||||
task
|
||||
users
|
||||
util
|
||||
warehouse-schema
|
||||
|
||||
@@ -13,7 +13,10 @@
|
||||
toucan2.core/insert-returning-pk!
|
||||
toucan2.core/insert-returning-pks!
|
||||
clojure.core.async/<!!
|
||||
clojure.core.async/<!
|
||||
clojure.core.async/>!!
|
||||
clojure.core.async/>!
|
||||
clojure.core.async/alts!
|
||||
clojure.core.async/alts!!
|
||||
clojure.core.async/close!
|
||||
clojure.core.async/poll!
|
||||
|
||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -3,7 +3,7 @@
|
||||
|
||||
> **Warning**
|
||||
>
|
||||
> If that is your first contribution to Metabase, please sign the [Contributor License Agreement](https://docs.google.com/a/metabase.com/forms/d/1oV38o7b9ONFSwuzwmERRMi9SYrhYeOrkbmNaq9pOJ_E/viewform) (unless it's a tiny documentation change). Also, if you're attempting to fix a translation issue, please submit your changes to our [Crowdin project](https://crowdin.com/project/metabase-i18n) instead of opening a PR.
|
||||
> If that is your first contribution to Metabase, please sign the [Contributor License Agreement](https://docs.google.com/a/metabase.com/forms/d/1oV38o7b9ONFSwuzwmERRMi9SYrhYeOrkbmNaq9pOJ_E/viewform). Also, if you're attempting to fix a translation issue, please submit your changes to our [Crowdin project](https://crowdin.com/project/metabase-i18n) instead of opening a PR.
|
||||
|
||||
Closes https://github.com/metabase/metabase/issues/[issue_number]
|
||||
|
||||
|
||||
14
.github/actions/await-port/action.yml
vendored
@@ -11,18 +11,6 @@ runs:
|
||||
- name: Wait for port ${{ inputs.port }} to be ready
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Waiting for port ${{ inputs.port }} to be ready..."
|
||||
timeout=300 # 5 minutes timeout
|
||||
elapsed=0
|
||||
while ! nc -z localhost ${{ inputs.port }}; do
|
||||
if [ $elapsed -ge $timeout ]; then
|
||||
echo "❌ Timeout waiting for port ${{ inputs.port }} after ${timeout} seconds"
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
elapsed=$((elapsed + 1))
|
||||
if [ $((elapsed % 10)) -eq 0 ]; then
|
||||
echo " Still waiting for port ${{ inputs.port }}... ($elapsed seconds)"
|
||||
fi
|
||||
sleep 0.1;
|
||||
done
|
||||
echo "✅ Port ${{ inputs.port }} is ready!"
|
||||
|
||||
2
.github/actions/create-backport/action.yml
vendored
@@ -74,7 +74,7 @@ runs:
|
||||
|
||||
git push -u origin ${BACKPORT_BRANCH}
|
||||
|
||||
BACKPORT_PR_URL=$(gh pr create --base "${TARGET_BRANCH}" --head "${BACKPORT_BRANCH}" --label "was-backported" --assignee "${GITHUB_ACTOR}" --title "🤖 backported \"${ORIGINAL_TITLE}\"" --body "${PR_BODY}")
|
||||
BACKPORT_PR_URL=$(gh pr create --base "${TARGET_BRANCH}" --head "${BACKPORT_BRANCH}" --label "was-backported" --assignee "${{ github.event.pull_request.user.login }}" --title "🤖 backported \"${ORIGINAL_TITLE}\"" --body "${PR_BODY}")
|
||||
BACKPORT_PR_NUMBER=${BACKPORT_PR_URL##*/}
|
||||
|
||||
echo "backport_pr_number=$BACKPORT_PR_NUMBER" >> $GITHUB_OUTPUT
|
||||
|
||||
172
.github/scripts/average-e2e-timings.js
vendored
Normal file
@@ -0,0 +1,172 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { execSync } = require("child_process");
|
||||
|
||||
const collectAndAverageTimings = async ({
|
||||
github,
|
||||
context,
|
||||
daysBack = 7,
|
||||
existingTimingsPath = "e2e/support/timings.json",
|
||||
}) => {
|
||||
try {
|
||||
const artifacts = await findMergedTimingArtifacts({
|
||||
github,
|
||||
context,
|
||||
daysBack,
|
||||
});
|
||||
|
||||
if (artifacts.length === 0) {
|
||||
throw new Error("No timing artifacts found in the specified time range");
|
||||
}
|
||||
|
||||
const timingData = await downloadAndExtractArtifacts(
|
||||
artifacts,
|
||||
token,
|
||||
github,
|
||||
context,
|
||||
);
|
||||
|
||||
if (timingData.length === 0) {
|
||||
throw new Error("No valid timing data extracted from artifacts");
|
||||
}
|
||||
|
||||
const averagedTimings = averageTimings(timingData);
|
||||
averagedTimings.durations.sort((a, b) => a.spec.localeCompare(b.spec));
|
||||
|
||||
fs.writeFileSync(
|
||||
existingTimingsPath,
|
||||
JSON.stringify(averagedTimings, null, 2),
|
||||
);
|
||||
console.log(`✅ Updated ${existingTimingsPath} with new timing data`);
|
||||
|
||||
return { artifactCount: timingData.length };
|
||||
} catch (error) {
|
||||
console.error("❌ Error in collectAndAverageTimings:", error.message);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const findMergedTimingArtifacts = async ({ github, context, daysBack }) => {
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - daysBack);
|
||||
|
||||
console.log(
|
||||
`Searching for merged timing artifacts created after ${cutoffDate.toISOString()}`,
|
||||
);
|
||||
|
||||
const artifacts = [];
|
||||
let page = 1;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
const { data } = await github.rest.actions.listArtifactsForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: "merged-e2e-timings",
|
||||
per_page: 100,
|
||||
page,
|
||||
});
|
||||
|
||||
const validArtifacts = data.artifacts
|
||||
.filter((a) => new Date(a.created_at) > cutoffDate && !a.expired)
|
||||
.map(({ name, created_at, id }) => ({ name, created_at, id }));
|
||||
artifacts.push(...validArtifacts);
|
||||
|
||||
if (
|
||||
data.artifacts.some((a) => new Date(a.created_at) <= cutoffDate) ||
|
||||
data.artifacts.length < 100
|
||||
) {
|
||||
break;
|
||||
}
|
||||
page++;
|
||||
} catch (error) {
|
||||
console.log(
|
||||
`Error fetching artifacts page ${page - 1}: ${error.message}`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Found ${artifacts.length} merged timing artifacts total`);
|
||||
return artifacts;
|
||||
};
|
||||
|
||||
const downloadAndExtractArtifacts = async (artifacts, github, context) => {
|
||||
const tempDir = "timing-artifacts-temp";
|
||||
fs.mkdirSync(tempDir, { recursive: true });
|
||||
|
||||
const timingData = [];
|
||||
|
||||
for (const artifact of artifacts) {
|
||||
try {
|
||||
console.log(`Downloading artifact: ${artifact.name}`);
|
||||
|
||||
const zipPath = path.join(tempDir, `${artifact.name}.zip`);
|
||||
const extractPath = path.join(tempDir, artifact.name);
|
||||
|
||||
const { data } = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: artifact.id,
|
||||
archive_format: "zip",
|
||||
});
|
||||
|
||||
fs.writeFileSync(zipPath, Buffer.from(data));
|
||||
fs.mkdirSync(extractPath, { recursive: true });
|
||||
execSync(`unzip -q "${zipPath}" -d "${extractPath}"`);
|
||||
|
||||
const timingFilePath = path.join(extractPath, "runTimings.json");
|
||||
|
||||
if (fs.existsSync(timingFilePath)) {
|
||||
const content = JSON.parse(fs.readFileSync(timingFilePath, "utf8"));
|
||||
|
||||
if (content.durations?.length) {
|
||||
timingData.push(content);
|
||||
console.log(
|
||||
`✅ Extracted timing data from ${artifact.name} (${content.durations.length} specs)`,
|
||||
);
|
||||
} else {
|
||||
console.log(`⚠ Invalid timing data structure in ${artifact.name}`);
|
||||
}
|
||||
} else {
|
||||
console.log(`⚠ No timing data found in ${artifact.name}`);
|
||||
}
|
||||
|
||||
fs.rmSync(extractPath, { recursive: true });
|
||||
} catch (error) {
|
||||
console.log(`❌ Error processing ${artifact.name}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
fs.rmSync(tempDir, { recursive: true });
|
||||
|
||||
console.log(
|
||||
`Successfully extracted timing data from ${timingData.length}/${artifacts.length} artifacts`,
|
||||
);
|
||||
return timingData;
|
||||
};
|
||||
|
||||
const averageTimings = (timingDataArray) => {
|
||||
if (!timingDataArray.length) {
|
||||
throw new Error("No timing data to process");
|
||||
}
|
||||
|
||||
const specTimings = timingDataArray
|
||||
.flatMap((data) => data.durations)
|
||||
.filter((item) => item.spec && typeof item.duration === "number")
|
||||
.reduce((acc, { spec, duration }) => {
|
||||
(acc[spec] = acc[spec] || []).push(duration);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const durations = Object.entries(specTimings).map(([spec, durations]) => ({
|
||||
spec,
|
||||
duration: Math.round(
|
||||
durations.reduce((sum, d) => sum + d, 0) / durations.length,
|
||||
),
|
||||
}));
|
||||
|
||||
return { durations };
|
||||
};
|
||||
|
||||
module.exports = { collectAndAverageTimings, averageTimings };
|
||||
132
.github/scripts/average-e2e-timings.unit.spec.js
vendored
Normal file
@@ -0,0 +1,132 @@
|
||||
const { averageTimings } = require("./average-e2e-timings");
|
||||
|
||||
describe("averageTimings", () => {
|
||||
it("should throw error when given empty array", () => {
|
||||
expect(() => averageTimings([])).toThrow("No timing data to process");
|
||||
});
|
||||
|
||||
it("should work for n=1", () => {
|
||||
const input = [
|
||||
{
|
||||
durations: [
|
||||
{ spec: "test1.cy.js", duration: 1000 },
|
||||
{ spec: "test2.cy.js", duration: 2000 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = averageTimings(input);
|
||||
|
||||
expect(result.durations).toHaveLength(2);
|
||||
expect(result.durations).toContainEqual({
|
||||
spec: "test1.cy.js",
|
||||
duration: 1000,
|
||||
});
|
||||
expect(result.durations).toContainEqual({
|
||||
spec: "test2.cy.js",
|
||||
duration: 2000,
|
||||
});
|
||||
});
|
||||
|
||||
it("should calculate average for multiple specs", () => {
|
||||
const input = [
|
||||
{
|
||||
durations: [
|
||||
{ spec: "test1.cy.js", duration: 1000 },
|
||||
{ spec: "test2.cy.js", duration: 2000 },
|
||||
],
|
||||
},
|
||||
{
|
||||
durations: [
|
||||
{ spec: "test1.cy.js", duration: 2000 },
|
||||
{ spec: "test2.cy.js", duration: 4000 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = averageTimings(input);
|
||||
|
||||
expect(result.durations).toHaveLength(2);
|
||||
expect(result.durations).toContainEqual({
|
||||
spec: "test1.cy.js",
|
||||
duration: 1500,
|
||||
});
|
||||
expect(result.durations).toContainEqual({
|
||||
spec: "test2.cy.js",
|
||||
duration: 3000,
|
||||
});
|
||||
});
|
||||
|
||||
it("should round averages to nearest integer", () => {
|
||||
const input = [
|
||||
{
|
||||
durations: [{ spec: "test1.cy.js", duration: 1001 }],
|
||||
},
|
||||
{
|
||||
durations: [{ spec: "test1.cy.js", duration: 1002 }],
|
||||
},
|
||||
];
|
||||
|
||||
const result = averageTimings(input);
|
||||
|
||||
expect(result.durations[0].duration).toBe(1002);
|
||||
});
|
||||
|
||||
it("should ignore invalid entries", () => {
|
||||
const input = [
|
||||
{
|
||||
durations: [
|
||||
{ spec: "test1.cy.js", duration: 1000 },
|
||||
{ spec: "test2.cy.js", duration: "invalid" },
|
||||
{ duration: 2000 }, // missing spec
|
||||
{ spec: "test3.cy.js", duration: 3000 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = averageTimings(input);
|
||||
|
||||
expect(result.durations).toHaveLength(2);
|
||||
expect(result.durations).toContainEqual({
|
||||
spec: "test1.cy.js",
|
||||
duration: 1000,
|
||||
});
|
||||
expect(result.durations).toContainEqual({
|
||||
spec: "test3.cy.js",
|
||||
duration: 3000,
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle merging different specs", () => {
|
||||
const input = [
|
||||
{
|
||||
durations: [
|
||||
{ spec: "test1.cy.js", duration: 1000 },
|
||||
{ spec: "test2.cy.js", duration: 2000 },
|
||||
],
|
||||
},
|
||||
{
|
||||
durations: [
|
||||
{ spec: "test1.cy.js", duration: 3000 },
|
||||
{ spec: "test3.cy.js", duration: 1000 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = averageTimings(input);
|
||||
|
||||
expect(result.durations).toHaveLength(3);
|
||||
expect(result.durations).toContainEqual({
|
||||
spec: "test1.cy.js",
|
||||
duration: 2000,
|
||||
});
|
||||
expect(result.durations).toContainEqual({
|
||||
spec: "test2.cy.js",
|
||||
duration: 2000,
|
||||
});
|
||||
expect(result.durations).toContainEqual({
|
||||
spec: "test3.cy.js",
|
||||
duration: 1000,
|
||||
});
|
||||
});
|
||||
});
|
||||
39
.github/scripts/extract-changed-timings.js
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
const extractChangedTimings = ({ oldTimings, newTimings }) => {
|
||||
const oldTimingMap =
|
||||
oldTimings.durations?.reduce((map, item) => {
|
||||
if (item.spec) {
|
||||
map[item.spec] = item.duration;
|
||||
}
|
||||
return map;
|
||||
}, {}) || {};
|
||||
|
||||
const changedDurations = newTimings.durations
|
||||
.filter((item) => {
|
||||
if (!item || !item.spec || typeof item.duration !== "number") {
|
||||
return false;
|
||||
}
|
||||
const newSpec = convertPathFormat(item.spec);
|
||||
const oldDuration = oldTimingMap[newSpec] || oldTimingMap[item.spec];
|
||||
// Include if it's a new spec or the duration has changed
|
||||
return oldDuration === undefined || oldDuration !== item.duration;
|
||||
})
|
||||
.map((item) => ({
|
||||
spec: convertPathFormat(item.spec),
|
||||
duration: item.duration,
|
||||
}));
|
||||
|
||||
return {
|
||||
changedTimings: { durations: changedDurations },
|
||||
hasChanges: changedDurations.length > 0,
|
||||
};
|
||||
};
|
||||
|
||||
// Convert cypress-split path format to timings.json path format
|
||||
// from "e2e/test/scenarios/..." to "../test/scenarios/..."
|
||||
function convertPathFormat(specPath) {
|
||||
return specPath.startsWith("e2e/test/")
|
||||
? specPath.replace("e2e/test/", "../test/")
|
||||
: specPath;
|
||||
}
|
||||
|
||||
module.exports = { extractChangedTimings };
|
||||
195
.github/scripts/extract-changed-timings.unit.spec.js
vendored
Normal file
@@ -0,0 +1,195 @@
|
||||
const { extractChangedTimings } = require("./extract-changed-timings");
|
||||
|
||||
describe("extractChangedTimings", () => {
|
||||
it("should return empty durations when no changes", () => {
|
||||
const oldTimings = {
|
||||
durations: [
|
||||
{ spec: "test1.cy.js", duration: 1000 },
|
||||
{ spec: "test2.cy.js", duration: 2000 },
|
||||
],
|
||||
};
|
||||
|
||||
const newTimings = {
|
||||
durations: [
|
||||
{ spec: "test1.cy.js", duration: 1000 },
|
||||
{ spec: "test2.cy.js", duration: 2000 },
|
||||
],
|
||||
};
|
||||
|
||||
const result = extractChangedTimings({ oldTimings, newTimings });
|
||||
|
||||
expect(result.changedTimings.durations).toHaveLength(0);
|
||||
expect(result.hasChanges).toBe(false);
|
||||
});
|
||||
|
||||
it("should detect new specs", () => {
|
||||
const oldTimings = {
|
||||
durations: [{ spec: "test1.cy.js", duration: 1000 }],
|
||||
};
|
||||
|
||||
const newTimings = {
|
||||
durations: [
|
||||
{ spec: "test1.cy.js", duration: 1000 },
|
||||
{ spec: "test2.cy.js", duration: 2000 },
|
||||
],
|
||||
};
|
||||
|
||||
const result = extractChangedTimings({ oldTimings, newTimings });
|
||||
|
||||
expect(result.changedTimings.durations).toHaveLength(1);
|
||||
expect(result.changedTimings.durations[0]).toEqual({
|
||||
spec: "test2.cy.js",
|
||||
duration: 2000,
|
||||
});
|
||||
expect(result.hasChanges).toBe(true);
|
||||
});
|
||||
|
||||
it("should detect duration changes", () => {
|
||||
const oldTimings = {
|
||||
durations: [
|
||||
{ spec: "test1.cy.js", duration: 1000 },
|
||||
{ spec: "test2.cy.js", duration: 2000 },
|
||||
],
|
||||
};
|
||||
|
||||
const newTimings = {
|
||||
durations: [
|
||||
{ spec: "test1.cy.js", duration: 1000 },
|
||||
{ spec: "test2.cy.js", duration: 2200 },
|
||||
],
|
||||
};
|
||||
|
||||
const result = extractChangedTimings({ oldTimings, newTimings });
|
||||
|
||||
expect(result.changedTimings.durations).toHaveLength(1);
|
||||
expect(result.changedTimings.durations[0]).toEqual({
|
||||
spec: "test2.cy.js",
|
||||
duration: 2200,
|
||||
});
|
||||
expect(result.hasChanges).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle missing old timings", () => {
|
||||
const oldTimings = { durations: [] };
|
||||
|
||||
const newTimings = {
|
||||
durations: [
|
||||
{ spec: "test1.cy.js", duration: 1000 },
|
||||
{ spec: "test2.cy.js", duration: 2000 },
|
||||
],
|
||||
};
|
||||
|
||||
const result = extractChangedTimings({ oldTimings, newTimings });
|
||||
|
||||
// All timings should be considered "changed" since old timings is empty
|
||||
expect(result.changedTimings.durations).toHaveLength(2);
|
||||
expect(result.changedTimings.durations).toContainEqual({
|
||||
spec: "test1.cy.js",
|
||||
duration: 1000,
|
||||
});
|
||||
expect(result.changedTimings.durations).toContainEqual({
|
||||
spec: "test2.cy.js",
|
||||
duration: 2000,
|
||||
});
|
||||
expect(result.hasChanges).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle invalid timing entries", () => {
|
||||
const oldTimings = {
|
||||
durations: [{ spec: "test1.cy.js", duration: 1000 }],
|
||||
};
|
||||
|
||||
const newTimings = {
|
||||
durations: [
|
||||
{ spec: "test1.cy.js", duration: 1000 },
|
||||
{ spec: "test2.cy.js", duration: "invalid" },
|
||||
{ duration: 2000 }, // missing spec
|
||||
{ spec: "test3.cy.js", duration: 3000 },
|
||||
],
|
||||
};
|
||||
|
||||
const result = extractChangedTimings({ oldTimings, newTimings });
|
||||
|
||||
// Only test3.cy.js should be included (new spec with valid data)
|
||||
expect(result.changedTimings.durations).toHaveLength(1);
|
||||
expect(result.changedTimings.durations[0]).toEqual({
|
||||
spec: "test3.cy.js",
|
||||
duration: 3000,
|
||||
});
|
||||
expect(result.hasChanges).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle timing data with different specs", () => {
|
||||
const oldTimings = {
|
||||
durations: [
|
||||
{ spec: "test1.cy.js", duration: 1000 },
|
||||
{ spec: "test2.cy.js", duration: 2000 },
|
||||
],
|
||||
};
|
||||
|
||||
const newTimings = {
|
||||
durations: [
|
||||
{ spec: "test1.cy.js", duration: 3000 },
|
||||
{ spec: "test3.cy.js", duration: 1000 },
|
||||
],
|
||||
};
|
||||
|
||||
const result = extractChangedTimings({ oldTimings, newTimings });
|
||||
|
||||
expect(result.changedTimings.durations).toHaveLength(2);
|
||||
expect(result.changedTimings.durations).toContainEqual({
|
||||
spec: "test1.cy.js",
|
||||
duration: 3000,
|
||||
});
|
||||
expect(result.changedTimings.durations).toContainEqual({
|
||||
spec: "test3.cy.js",
|
||||
duration: 1000,
|
||||
});
|
||||
expect(result.hasChanges).toBe(true);
|
||||
});
|
||||
|
||||
it("should convert cypress-split paths to relative paths", () => {
|
||||
const oldTimings = {
|
||||
durations: [
|
||||
{
|
||||
spec: "../test/scenarios/dashboard/dashboard.cy.spec.js",
|
||||
duration: 1000,
|
||||
},
|
||||
{
|
||||
spec: "../test/scenarios/admin/databases.cy.spec.js",
|
||||
duration: 2000,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const newTimings = {
|
||||
durations: [
|
||||
{
|
||||
spec: "e2e/test/scenarios/dashboard/dashboard.cy.spec.js",
|
||||
duration: 1200,
|
||||
},
|
||||
{
|
||||
spec: "e2e/test/scenarios/admin/databases.cy.spec.js",
|
||||
duration: 2000,
|
||||
},
|
||||
{
|
||||
spec: "e2e/test/scenarios/collections/collections.cy.spec.js",
|
||||
duration: 1500,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = extractChangedTimings({ oldTimings, newTimings });
|
||||
|
||||
expect(result.changedTimings.durations).toHaveLength(2);
|
||||
expect(result.changedTimings.durations).toContainEqual({
|
||||
spec: "../test/scenarios/dashboard/dashboard.cy.spec.js",
|
||||
duration: 1200,
|
||||
});
|
||||
expect(result.changedTimings.durations).toContainEqual({
|
||||
spec: "../test/scenarios/collections/collections.cy.spec.js",
|
||||
duration: 1500,
|
||||
});
|
||||
expect(result.hasChanges).toBe(true);
|
||||
});
|
||||
});
|
||||
1
.github/scripts/setup-python-runner.sh
vendored
@@ -19,6 +19,7 @@ while [[ $# -gt 0 ]]; do
|
||||
done
|
||||
|
||||
echo "Cloning python-runner repository..."
|
||||
rm -r python-runner
|
||||
git clone --depth 1 https://${METABASE_AUTOMATION_USER_TOKEN}@github.com/metabase/python-runner-container.git python-runner
|
||||
|
||||
if [ "$SKIP_START" = false ]; then
|
||||
|
||||
4
.github/workflows/app-db.yml
vendored
@@ -330,10 +330,6 @@ jobs:
|
||||
test-args: >-
|
||||
${{ matrix.job.test-args }}
|
||||
:exclude-tags '[ ${{ matrix.job.exclude-tag }} ${{ env.__ADDITIONAL_EXCLUDED_TAG__ }}]'
|
||||
- name: Setup python-runner
|
||||
env:
|
||||
METABASE_AUTOMATION_USER_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
run: ./.github/scripts/setup-python-runner.sh
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always()
|
||||
|
||||
16
.github/workflows/auto-backport.yml
vendored
@@ -6,11 +6,19 @@ on:
|
||||
|
||||
jobs:
|
||||
create-backport:
|
||||
if: | # the PR must be merged and have a backport label
|
||||
if:
|
||||
| # the PR must have a backport label when it's merged, or be labeled with a backport label after merge
|
||||
github.event.pull_request.merged == true && (
|
||||
contains(github.event.pull_request.labels.*.name, 'backport') ||
|
||||
contains(github.event.pull_request.labels.*.name, 'double-backport') ||
|
||||
contains(github.event.pull_request.labels.*.name, 'triple-backport')
|
||||
github.event.action == 'closed' && (
|
||||
contains(github.event.pull_request.labels.*.name, 'backport') ||
|
||||
contains(github.event.pull_request.labels.*.name, 'double-backport') ||
|
||||
contains(github.event.pull_request.labels.*.name, 'triple-backport')
|
||||
) ||
|
||||
github.event.action == 'labeled' && (
|
||||
github.event.label.name == 'backport' ||
|
||||
github.event.label.name == 'double-backport' ||
|
||||
github.event.label.name == 'triple-backport'
|
||||
)
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
1
.github/workflows/backend.yml
vendored
@@ -195,7 +195,6 @@ jobs:
|
||||
- name: Run cljfmt
|
||||
run: ./bin/mage cljfmt-all --force-check
|
||||
|
||||
|
||||
be-tests-result:
|
||||
needs:
|
||||
- be-linter-eastwood
|
||||
|
||||
76
.github/workflows/drivers-stress-test.yml
vendored
@@ -689,25 +689,57 @@ jobs:
|
||||
junit-name: 'be-tests-starburst-ee'
|
||||
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
|
||||
|
||||
# TODO(rileythomp, 2025-08-20): Re-enable vertica tests once the docker image is updated
|
||||
# be-tests-vertica-ee:
|
||||
# if: ${{ false && inputs.driver == 'Vertica' }}
|
||||
# runs-on: ubuntu-22.04
|
||||
# timeout-minutes: 90
|
||||
# env:
|
||||
# CI: 'true'
|
||||
# DRIVERS: vertica
|
||||
# services:
|
||||
# vertica:
|
||||
# image: vertica/vertica-ce:12.0.2-0
|
||||
# ports:
|
||||
# - "5433:5433"
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - name: Make plugins directory
|
||||
# run: mkdir plugins
|
||||
# - name: Test Vertica driver
|
||||
# uses: ./.github/actions/test-driver
|
||||
# with:
|
||||
# junit-name: 'be-tests-vertica-ee'
|
||||
# test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
|
||||
login-to-ecr:
|
||||
if: inputs.driver == 'Vertica'
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: ${{ secrets.CI_IAM_ROLE }}
|
||||
role-session-name: GitHub_to_AWS_via_FederatedOIDC
|
||||
aws-region: us-east-1
|
||||
- name: Get ECR password
|
||||
id: login-ecr
|
||||
run: |
|
||||
PASSWORD=$(aws ecr get-login-password --region us-east-1)
|
||||
|
||||
REGISTRY="$(aws sts get-caller-identity --query Account --output text).dkr.ecr.us-east-1.amazonaws.com"
|
||||
|
||||
echo "docker_username=AWS" >> $GITHUB_OUTPUT
|
||||
echo "docker_password=$PASSWORD" >> $GITHUB_OUTPUT
|
||||
echo "registry=$REGISTRY" >> $GITHUB_OUTPUT
|
||||
outputs:
|
||||
registry: ${{ steps.login-ecr.outputs.registry }}
|
||||
docker_username: ${{ steps.login-ecr.outputs.docker_username }}
|
||||
docker_password: ${{ steps.login-ecr.outputs.docker_password }}
|
||||
|
||||
be-tests-vertica-ee:
|
||||
if: inputs.driver == 'Vertica'
|
||||
runs-on: ubuntu-22.04
|
||||
needs: login-to-ecr
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
CI: 'true'
|
||||
DRIVERS: vertica
|
||||
services:
|
||||
vertica:
|
||||
image: ${{ needs.login-to-ecr.outputs.registry }}/vertica-ce:25.3.0-0
|
||||
ports:
|
||||
- "5433:5433"
|
||||
credentials:
|
||||
username: ${{ needs.login-to-ecr.outputs.docker_username }}
|
||||
password: ${{ needs.login-to-ecr.outputs.docker_password }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Make plugins directory
|
||||
run: mkdir plugins
|
||||
- name: Test Vertica driver
|
||||
uses: ./.github/actions/test-driver
|
||||
with:
|
||||
junit-name: 'be-tests-vertica-ee'
|
||||
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
|
||||
|
||||
123
.github/workflows/drivers.yml
vendored
@@ -115,10 +115,11 @@ jobs:
|
||||
test-args: >-
|
||||
:only-tags [:mb/driver-tests]
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
- name: Transforms Python Tests
|
||||
test-args: >-
|
||||
:only-tags [:mb/transforms-python-test]
|
||||
needs-python-runner: true
|
||||
# DS 2025-09-26 temporarily disabled while we diagnose flakes
|
||||
# - name: Transforms Python Tests
|
||||
# test-args: >-
|
||||
# :only-tags [:mb/transforms-python-test]
|
||||
# needs-python-runner: true
|
||||
env:
|
||||
CI: 'true'
|
||||
DRIVERS: bigquery-cloud-sdk
|
||||
@@ -982,10 +983,11 @@ jobs:
|
||||
test-args: >-
|
||||
:only-tags [:mb/driver-tests]
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
- name: Transforms Python Tests
|
||||
test-args: >-
|
||||
:only-tags [:mb/transforms-python-test]
|
||||
needs-python-runner: true
|
||||
# DS 2025-09-26 temporarily disabled while we diagnose flakes
|
||||
# - name: Transforms Python Tests
|
||||
# test-args: >-
|
||||
# :only-tags [:mb/transforms-python-test]
|
||||
# needs-python-runner: true
|
||||
env:
|
||||
CI: 'true'
|
||||
DRIVERS: snowflake
|
||||
@@ -1197,42 +1199,75 @@ jobs:
|
||||
# aws-region: ${{ vars.AWS_REGION }}
|
||||
# trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
|
||||
|
||||
# TODO(rileythomp, 2025-08-20): Re-enable vertica tests once the docker image is updated
|
||||
# be-tests-vertica-ee:
|
||||
# needs: determine-driver-skips
|
||||
# if: ${{ false && !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
# runs-on: ubuntu-22.04
|
||||
# timeout-minutes: 60
|
||||
# env:
|
||||
# CI: 'true'
|
||||
# DRIVERS: vertica
|
||||
# services:
|
||||
# vertica:
|
||||
# image: vertica/vertica-ce:12.0.2-0
|
||||
# ports:
|
||||
# - "5433:5433"
|
||||
# name: Vertica
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - name: Make plugins directory
|
||||
# run: mkdir plugins
|
||||
# - name: Test Vertica driver
|
||||
# uses: ./.github/actions/test-driver
|
||||
# with:
|
||||
# junit-name: 'be-tests-vertica-ee'
|
||||
# test-args: >-
|
||||
# :only-tags [:mb/driver-tests]
|
||||
# - name: Upload Test Results
|
||||
# uses: ./.github/actions/upload-test-results
|
||||
# if: always()
|
||||
# with:
|
||||
# input-path: ./target/junit/
|
||||
# output-name: ${{ github.job }}
|
||||
# bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
|
||||
# aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
|
||||
# aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
|
||||
# aws-region: ${{ vars.AWS_REGION }}
|
||||
# trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
|
||||
login-to-ecr:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: ${{ secrets.CI_IAM_ROLE }}
|
||||
role-session-name: GitHub_to_AWS_via_FederatedOIDC
|
||||
aws-region: us-east-1
|
||||
- name: Get ECR password
|
||||
id: login-ecr
|
||||
run: |
|
||||
PASSWORD=$(aws ecr get-login-password --region us-east-1)
|
||||
|
||||
REGISTRY="$(aws sts get-caller-identity --query Account --output text).dkr.ecr.us-east-1.amazonaws.com"
|
||||
|
||||
echo "docker_username=AWS" >> $GITHUB_OUTPUT
|
||||
echo "docker_password=$PASSWORD" >> $GITHUB_OUTPUT
|
||||
echo "registry=$REGISTRY" >> $GITHUB_OUTPUT
|
||||
outputs:
|
||||
registry: ${{ steps.login-ecr.outputs.registry }}
|
||||
docker_username: ${{ steps.login-ecr.outputs.docker_username }}
|
||||
docker_password: ${{ steps.login-ecr.outputs.docker_password }}
|
||||
|
||||
be-tests-vertica-ee:
|
||||
needs: [determine-driver-skips, login-to-ecr]
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
CI: 'true'
|
||||
DRIVERS: vertica
|
||||
services:
|
||||
vertica:
|
||||
image: ${{ needs.login-to-ecr.outputs.registry }}/vertica-ce:25.3.0-0
|
||||
ports:
|
||||
- "5433:5433"
|
||||
credentials:
|
||||
username: ${{ needs.login-to-ecr.outputs.docker_username }}
|
||||
password: ${{ needs.login-to-ecr.outputs.docker_password }}
|
||||
name: Vertica
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Make plugins directory
|
||||
run: mkdir plugins
|
||||
- name: Test Vertica driver
|
||||
uses: ./.github/actions/test-driver
|
||||
with:
|
||||
junit-name: 'be-tests-vertica-ee'
|
||||
test-args: >-
|
||||
:only-tags [:mb/driver-tests]
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always()
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
|
||||
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ vars.AWS_REGION }}
|
||||
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
|
||||
|
||||
drivers-tests-result:
|
||||
needs:
|
||||
|
||||
47
.github/workflows/e2e-test.yml
vendored
@@ -131,7 +131,7 @@ jobs:
|
||||
SPLIT_INDEX: ${{ inputs.split_index }}
|
||||
SPLIT_FILE: "./e2e/support/timings.json"
|
||||
SPLIT_OUTPUT_FILE: "newTimes.json"
|
||||
SPLIT_TIME_THRESHOLD: 5.0
|
||||
SPLIT_TIME_THRESHOLD: 0.01 # 1% threshold for changes
|
||||
# do not write cypress-split summary to github
|
||||
SPLIT_SUMMARY: false
|
||||
SPEC_PATH: ${{ env.specs || inputs.specs }}
|
||||
@@ -185,6 +185,51 @@ jobs:
|
||||
name: failed-tests-${{ inputs.name }}-${{ inputs.edition }}
|
||||
path: ./cypress/test-results
|
||||
|
||||
- name: Extract chunk-specific changed timings
|
||||
id: extract-timings
|
||||
if: inputs.split_index >= 0 && github.ref == 'refs/heads/master'
|
||||
continue-on-error: true
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const oldTimingsPath = "./e2e/support/timings.json";
|
||||
const newTimingsPath = "./e2e/support/newTimes.json";
|
||||
|
||||
if (!fs.existsSync(newTimingsPath)) {
|
||||
throw new Error(`newTimes.json not found - cypress-split may not have generated it`);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(oldTimingsPath)) {
|
||||
throw new Error(`Original timings file ${oldTimingsPath} not found`);
|
||||
}
|
||||
|
||||
const oldTimings = fs.existsSync(oldTimingsPath)
|
||||
? JSON.parse(fs.readFileSync(oldTimingsPath, "utf8"))
|
||||
: { durations: [] };
|
||||
|
||||
const newTimings = fs.existsSync(newTimingsPath)
|
||||
? JSON.parse(fs.readFileSync(newTimingsPath, "utf8"))
|
||||
: { durations: [] };
|
||||
|
||||
const { extractChangedTimings } = require('${{ github.workspace }}/.github/scripts/extract-changed-timings.js');
|
||||
|
||||
const result = extractChangedTimings({ oldTimings, newTimings });
|
||||
|
||||
if (result.hasChanges) {
|
||||
core.setOutput('has-changes', 'true');
|
||||
fs.writeFileSync('chunkTimings.json', JSON.stringify(result.changedTimings, null, 2));
|
||||
console.log(`Extracted ${Object.keys(result.changedTimings).length} changed timings for chunk index ${{ inputs.split_index }}`);
|
||||
}
|
||||
|
||||
- name: Upload Timing Data
|
||||
uses: actions/upload-artifact@v4
|
||||
if: steps.extract-timings.outcome == 'success' && steps.extract-timings.outputs.has-changes == 'true'
|
||||
with:
|
||||
name: timing-data-${{ inputs.name }}-${{ inputs.split_index }}
|
||||
path: chunkTimings.json
|
||||
retention-days: 1 # gets merged immediately by parent workflow
|
||||
|
||||
- name: Publish Summary
|
||||
if: failure()
|
||||
uses: actions/github-script@v7
|
||||
|
||||
61
.github/workflows/e2e-tests.yml
vendored
@@ -87,6 +87,67 @@ jobs:
|
||||
name: Flaky
|
||||
tags: "@flaky"
|
||||
|
||||
collect-timings:
|
||||
name: Collect E2E Timings
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
needs: e2e-tests
|
||||
if: always() && !cancelled() && github.ref == 'refs/heads/master'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github
|
||||
e2e/support
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Download all timing artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: timing-data-*
|
||||
path: timing-files
|
||||
merge-multiple: false
|
||||
continue-on-error: true
|
||||
|
||||
- name: Merge split timings
|
||||
id: merge
|
||||
continue-on-error: true
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const chunkFiles = glob.sync('timing-files/*/chunkTimings.json');
|
||||
|
||||
const specs = new Map();
|
||||
chunkFiles.forEach(file => {
|
||||
const { durations = [] } = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
durations.forEach(({ spec, duration }) => specs.set(spec, duration));
|
||||
});
|
||||
|
||||
if (specs.size === 0) {
|
||||
throw new Error('No timing data found - no specs had timing changes');
|
||||
}
|
||||
|
||||
const outputPath = 'e2e/support/runTimings.json';
|
||||
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
||||
|
||||
const runTimings = {
|
||||
durations: [...specs].map(([spec, duration]) => ({ spec, duration }))
|
||||
};
|
||||
|
||||
fs.writeFileSync(outputPath, JSON.stringify(runTimings, null, 2));
|
||||
console.log(`Merged timing data for ${specs.size} specs`);
|
||||
|
||||
- name: Upload merged timing files
|
||||
if: steps.merge.outcome == 'success'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: merged-e2e-timings
|
||||
path: e2e/support/runTimings.json
|
||||
retention-days: 7
|
||||
|
||||
e2e-tests-result:
|
||||
needs:
|
||||
- e2e-tests
|
||||
|
||||
20
.github/workflows/loki-update.yml
vendored
@@ -26,26 +26,8 @@ jobs:
|
||||
- name: Compile CLJS
|
||||
run: yarn build-pure:cljs
|
||||
|
||||
- name: Build Storybook
|
||||
# We won't be able to use Storybook play's `userEvent` with production build
|
||||
# see https://github.com/storybookjs/storybook/issues/19758
|
||||
run: NODE_ENV=development yarn build-storybook
|
||||
|
||||
- name: Serve Storybook
|
||||
run: yarn http-server storybook-static -p 6006 &
|
||||
shell: bash
|
||||
|
||||
- name: Wait for Storybook to become available
|
||||
run: |
|
||||
echo "Waiting for Storybook to be served..."
|
||||
while ! curl --silent --fail http://localhost:6006; do
|
||||
sleep 5
|
||||
echo "Retrying..."
|
||||
done
|
||||
echo "Storybook is being served!"
|
||||
|
||||
- name: Update Loki Snapshots
|
||||
run: yarn test-visual:loki || yarn test-visual:loki-approve-diff && yarn test-visual:loki-prune
|
||||
run: yarn test-visual:loki:ci || yarn test-visual:loki-approve-diff && yarn test-visual:loki-prune
|
||||
|
||||
- name: Check for changes
|
||||
id: check-changes
|
||||
|
||||
8
.github/workflows/loki.yml
vendored
@@ -53,14 +53,8 @@ jobs:
|
||||
- name: Compile CLJS
|
||||
run: NODE_ENV=development yarn build-pure:cljs
|
||||
|
||||
- name: Build Storybook
|
||||
# We won't be able to use Storybook play's `userEvent` with production build
|
||||
# see https://github.com/storybookjs/storybook/issues/19758
|
||||
run: NODE_ENV=development yarn build-storybook
|
||||
|
||||
- name: Run Loki Visual Tests
|
||||
run: |
|
||||
yarn loki --requireReference --reactUri file:./storybook-static --chromeFlags='--headless --disable-gpu' --verboseRenderer
|
||||
run: yarn test-visual:loki:ci
|
||||
|
||||
- name: Generate Visual Report on Failure
|
||||
if: failure()
|
||||
|
||||
5
.github/workflows/run-tests.yml
vendored
@@ -8,6 +8,7 @@ on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
merge_group:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
@@ -74,7 +75,7 @@ jobs:
|
||||
secrets: inherit
|
||||
|
||||
backend-tests:
|
||||
if: ${{ !cancelled() }}
|
||||
if: ${{ !cancelled() && needs.files-changed.result == 'success' }}
|
||||
needs: [files-changed, static-viz-files-changed]
|
||||
uses: ./.github/workflows/backend.yml
|
||||
secrets: inherit
|
||||
@@ -112,7 +113,7 @@ jobs:
|
||||
|
||||
e2e-tests:
|
||||
needs: [files-changed, uberjar]
|
||||
if: always() && !cancelled()
|
||||
if: always() && !cancelled() && needs.files-changed.result == 'success'
|
||||
uses: ./.github/workflows/e2e-tests.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
|
||||
92
.github/workflows/update-e2e-timings.yml
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
name: Update E2E Timings
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
days_back:
|
||||
description: "Number of days to look back for timing artifacts"
|
||||
type: number
|
||||
default: 7
|
||||
required: false
|
||||
schedule:
|
||||
# Run weekly on Sundays at 6 AM UTC
|
||||
- cron: "0 6 * * 0"
|
||||
|
||||
jobs:
|
||||
collect-and-average-timings:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
DAYS_BACK: ${{ inputs.days_back || 7 }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Collect and average timing data
|
||||
id: collect-timings
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { collectAndAverageTimings } = require('${{ github.workspace }}/.github/scripts/average-e2e-timings.js');
|
||||
const result = await collectAndAverageTimings({
|
||||
github,
|
||||
context,
|
||||
token: '${{ secrets.GITHUB_TOKEN }}',
|
||||
daysBack: ${{ env.DAYS_BACK }},
|
||||
existingTimingsPath: 'e2e/support/timings.json'
|
||||
});
|
||||
|
||||
core.setOutput('artifact-count', result.artifactCount);
|
||||
|
||||
- name: Set up git configuration
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
BRANCH_NAME="update-e2e-timings-$(date +%Y%m%d-%H%M%S)"
|
||||
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
|
||||
echo "Branch name will be: $BRANCH_NAME"
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: ${{ env.BRANCH_NAME }}
|
||||
base: master
|
||||
commit-message: |
|
||||
Update E2E test timings with averaged data
|
||||
|
||||
Collected timing data from ${{ env.DAYS_BACK }} days of test runs.
|
||||
All specs with timing data were included (simple averages).
|
||||
|
||||
🤖 Generated automatically by update-e2e-timings workflow
|
||||
|
||||
Co-Authored-By: GitHub Actions <actions@github.com>
|
||||
committer: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
|
||||
author: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
|
||||
title: "[chore] Update E2E test timings with recent data"
|
||||
body: |
|
||||
## Summary
|
||||
This PR updates the E2E test timing data with averaged values from recent test runs.
|
||||
|
||||
## Details
|
||||
- Collected data from the last **${{ env.DAYS_BACK }} days** of test runs
|
||||
- Averaged timing data from **${{ steps.collect-timings.outputs.artifact-count }}** test runs
|
||||
|
||||
## Generated automatically
|
||||
🤖 This PR was created automatically by the `update-e2e-timings` workflow.
|
||||
The timing data helps cypress-split optimize test distribution across parallel jobs.
|
||||
|
||||
Co-Authored-By: GitHub Actions <actions@github.com>
|
||||
labels: |
|
||||
.CI & Tests
|
||||
add-paths: e2e/support/timings.json
|
||||
1
.gitignore
vendored
@@ -163,4 +163,3 @@ CLAUDE.local.md
|
||||
|
||||
/logs
|
||||
/.clojure-mcp
|
||||
__pycache__/
|
||||
|
||||
4
.gitmodules
vendored
@@ -1,4 +0,0 @@
|
||||
[submodule "python-runner"]
|
||||
path = python-runner
|
||||
url = https://github.com/metabase/python-runner-container.git
|
||||
branch = prototype
|
||||
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 114 KiB |
12
bb.edn
@@ -160,6 +160,18 @@
|
||||
:requires [[mage.stats-repl :as stats-repl]]
|
||||
:task (stats-repl/connect (cli/parse! (current-task)))}
|
||||
|
||||
notify
|
||||
{:doc "Send notification for a comment (that already exists"
|
||||
:requires [[mage.be-dev :refer [nrepl-eval]]]
|
||||
:options [["-c" "--comment-id ID" "Notify about this comment id"]
|
||||
["-p" "--port PORT" "Port to use for the task, defaults to value in .nrepl-port"]]
|
||||
:task (let [opts (:options (cli/parse! (current-task)))
|
||||
;; note: no quoting -> expect it to be int
|
||||
code (format "(notify-comment-id! %d)" (parse-long (:comment-id opts)))]
|
||||
(if (:port opts)
|
||||
(nrepl-eval "metabase-enterprise.comments.core" code (:port opts))
|
||||
(nrepl-eval "metabase-enterprise.comments.core" code)))}
|
||||
|
||||
ls {:doc "List all mage public and private tasks"
|
||||
:examples [["./bin/mage ls" "List all tasks"]]
|
||||
:requires [[mage.util :as u]]
|
||||
|
||||
30
deps.edn
@@ -36,7 +36,7 @@
|
||||
com.clearspring.analytics/stream {:mvn/version "2.9.8" ; Various sketching algorithms
|
||||
:exclusions [it.unimi.dsi/fastutil
|
||||
org.slf4j/slf4j-api]}
|
||||
com.clojure-goes-fast/jvm-alloc-rate-meter {:mvn/version "0.1.4"} ; Monitor heap allocation rate.
|
||||
com.clojure-goes-fast/jvm-alloc-rate-meter{:mvn/version "0.1.4"} ; Monitor heap allocation rate.
|
||||
com.clojure-goes-fast/jvm-hiccup-meter {:mvn/version "0.1.1"} ; Monitor GC pauses and other system-induced pauses.
|
||||
com.draines/postal {:mvn/version "2.0.5" ; SMTP library
|
||||
:exclusions [com.sun.mail/jakarta.mail]}
|
||||
@@ -45,7 +45,7 @@
|
||||
com.github.albfernandez/juniversalchardet {:mvn/version "2.5.0"}
|
||||
com.github.blagerweij/liquibase-sessionlock {:mvn/version "1.6.9"} ; session lock for liquibase migrations
|
||||
com.github.jknack/handlebars ^:antq/exclude ; 4.4.0 requires java 17+
|
||||
{:mvn/version "4.3.1"} ; templating engine
|
||||
{:mvn/version "4.3.1"} ; templating engine
|
||||
com.github.oliyh/martian-clj-http {:mvn/version "0.1.31"} ; openapi client
|
||||
com.github.pangloss/transducers {:git/sha "c91d39046c8c64dc31b84ebb27154a8084fcda4c" ; like medley for transducers
|
||||
:git/url "https://github.com/pangloss/transducers"}
|
||||
@@ -55,14 +55,14 @@
|
||||
com.github.vertical-blank/sql-formatter {:mvn/version "2.0.5"} ; Java SQL formatting library https://github.com/vertical-blank/sql-formatter
|
||||
com.google.guava/guava {:mvn/version "33.4.8-jre"} ; dep for BigQuery, Spark, and GA. Require here rather than letting different dep versions stomp on each other — see comments on #9697
|
||||
com.h2database/h2 ^:antq/exclude ; https://metaboat.slack.com/archives/CKZEMT1MJ/p1727191010259979
|
||||
{:mvn/version "2.1.214"} ; embedded SQL database
|
||||
{:mvn/version "2.1.214"} ; embedded SQL database
|
||||
com.ibm.icu/icu4j {:mvn/version "77.1"} ; Used to transliterate Unicode characters into Latin characters
|
||||
com.knuddels/jtokkit {:mvn/version "1.1.0"} ; Java tokenizer library for OpenAI models, used for token counting in semantic search
|
||||
com.mchange/c3p0 {:mvn/version "0.10.1"} ; DB connection pool
|
||||
com.mchange/c3p0 {:mvn/version "0.11.2"} ; DB connection pool
|
||||
com.snowplowanalytics/snowplow-java-tracker ^:antq/exclude ; 2+ depend on apache httpclient 5.x, but saml and clj-http depend on 4.x
|
||||
{:mvn/version "1.0.1"} ; Snowplow analytics
|
||||
{:mvn/version "1.0.1"} ; Snowplow analytics
|
||||
com.sun.mail/jakarta.mail ^:antq/exclude ; 2+ changes imports from javax.mail to jakarta.mail
|
||||
{:mvn/version "1.6.8"} ; SMTP lib, required by postal
|
||||
{:mvn/version "1.6.8"} ; SMTP lib, required by postal
|
||||
com.taoensso/nippy {:mvn/version "3.6.0"} ; Fast serialization (i.e., GZIP) library for Clojure
|
||||
com.vladsch.flexmark/flexmark {:mvn/version "0.64.8"} ; Markdown parsing
|
||||
com.vladsch.flexmark/flexmark-ext-autolink
|
||||
@@ -85,14 +85,14 @@
|
||||
hiccup/hiccup {:mvn/version "2.0.0"} ; HTML templating
|
||||
inflections/inflections {:mvn/version "0.15.0"} ; Clojure/Script library used for prularizing words
|
||||
instaparse/instaparse {:mvn/version "1.5.0"} ; Make your own parser
|
||||
io.github.camsaul/toucan2 {:mvn/version "1.0.566"}
|
||||
io.github.camsaul/toucan2 {:mvn/version "1.0.567"}
|
||||
io.github.eerohele/pp {#_#_:git/tag "2024-11-13.77" ; super fast pretty-printing library
|
||||
:git/sha "4998a1fdd9d3713d7034d04509e9212204773bf2"
|
||||
:git/url "https://github.com/eerohele/pp"}
|
||||
;; TODO: replace back to normal artifact
|
||||
io.github.metabase/macaw {:mvn/version "0.2.27"} ; Parse native SQL queries
|
||||
io.github.resilience4j/resilience4j-retry ^:antq/exclude ; 2.x do not support Java 8
|
||||
{:mvn/version "1.7.1"} ; Support for retrying operations
|
||||
{:mvn/version "1.7.1"} ; Support for retrying operations
|
||||
io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"} ; prometheus jvm collector
|
||||
junegunn/grouper {:mvn/version "0.1.1"} ; Batch processing helper
|
||||
kixi/stats {:mvn/version "0.5.7" ; Various statistic measures implemented as transducers
|
||||
@@ -198,8 +198,8 @@
|
||||
org.eclipse.jetty.ee9/jetty-ee9-servlet
|
||||
org.eclipse.jetty.ee9.websocket/jetty-ee9-websocket-jetty-server]}
|
||||
slingshot/slingshot {:mvn/version "0.12.2"} ; enhanced throw/catch, used by other deps
|
||||
software.amazon.awssdk/s3 {:mvn/version "2.33.0"}
|
||||
software.amazon.awssdk/sts {:mvn/version "2.33.0"} ; AWS SDK v2 for S3
|
||||
software.amazon.awssdk/s3 {:mvn/version "2.33.0"} ; AWS SDK v2 for S3
|
||||
software.amazon.awssdk/sts {:mvn/version "2.33.0"} ; Required for S3 client to use service account
|
||||
stencil/stencil {:mvn/version "0.5.0"} ; Mustache templates for Clojure
|
||||
user-agent/user-agent {:mvn/version "0.1.1"} ; User-Agent string parser, for Login History page & elsewhere
|
||||
weavejester/dependency {:mvn/version "1.0.0"} ; Dependency graphs and topological sorting
|
||||
@@ -286,6 +286,8 @@
|
||||
"-Duser.country=US"
|
||||
;; Allow clojure goes fast tooling to work
|
||||
"-Djdk.attach.allowAttachSelf"
|
||||
;; have core.async report usages of blocking operations in go blocks
|
||||
"-Dclojure.core.async.go-checking=true"
|
||||
;; This will suppress the warning about dynamically loaded agents (like clj-memory-meter)
|
||||
"-XX:+EnableDynamicAgentLoading"
|
||||
;; set the logging properties set in `metabase.core.bootstrap`. calling (dev) will load it but
|
||||
@@ -838,14 +840,8 @@
|
||||
:test/lib
|
||||
{:exec-args {:only ["test/metabase/lib"
|
||||
"test/metabase/lib_be"]}}
|
||||
;; test the python transforms
|
||||
;; clj -X:dev:ee:ee-dev:driver:test:test/python-transform
|
||||
:test/python-transform
|
||||
{:exec-args {:only [metabase-enterprise.transforms.api-test
|
||||
"enterprise/backend/test/metabase_enterprise/transforms"
|
||||
"enterprise/backend/test/metabase_enterprise/transforms_python"]}}
|
||||
|
||||
;; test the Query Processor
|
||||
;; test the Query Processor
|
||||
;;
|
||||
;; clj -X:dev:ee:ee-dev:test:test/qp
|
||||
:test/qp
|
||||
|
||||
@@ -61,7 +61,7 @@ Once one or more people have started to work on a feature, it should be marked [
|
||||
|
||||
### E) Verification and merging
|
||||
|
||||
All PRs that involve more than an insignificant change should be reviewed. See our [Code Review Process](./developers-guide/code-reviews.md).
|
||||
All PRs that involve more than an insignificant change should be reviewed. See our [Code Review Process](https://www.metabase.com/docs/master/developers-guide/code-reviews).
|
||||
|
||||
If all goes well, the feature gets coded up, verified and then the pull request gets merged! High-fives all around.
|
||||
|
||||
@@ -95,7 +95,7 @@ By our definition, "Bugs" are situations where the program doesn't do what it wa
|
||||
|
||||
There are a lot of docs, which means keeping them up to date is hard. If you notice inconsistencies, errors, or outdated information, please help us keep them current!
|
||||
|
||||
Note that **we cannot accept translations for documentation at this time**. We support [in-app translations](./configuring-metabase/localization.md), and only support languages that have 100% coverage. But 1) the in-app text is orders of magnitude shorter than our docs, 2) it changes at a slower pace, and 3) we have a lot of people help out. We may consider supporting docs in multiple languages in the future, but for now we need to focus our resources on improving our existing documentation (and expanding it to include all of the new features we're adding).
|
||||
Note that **we cannot accept translations for documentation at this time**. We support [in-app translations](https://www.metabase.com/docs/master/configuring-metabase/localization), and only support languages that have 100% coverage. But 1) the in-app text is orders of magnitude shorter than our docs, 2) it changes at a slower pace, and 3) we have a lot of people help out. We may consider supporting docs in multiple languages in the future, but for now we need to focus our resources on improving our existing documentation (and expanding it to include all of the new features we're adding).
|
||||
|
||||
### Working on features
|
||||
|
||||
|
||||
@@ -22,11 +22,13 @@ Here are a few ideas for what you can do with actions:
|
||||
|
||||
Actions must be added to a [model](../data-modeling/models.md), but actions only run on the raw tables that back those models (so actions will never edit your [model definition](../data-modeling/models.md#edit-a-models-query)).
|
||||
|
||||
If you're an admin who just wants to edit data in your tables, check out [Editable table data](../data-modeling/editable-tables.md).
|
||||
|
||||
## Enabling actions for a database
|
||||
|
||||
For actions to work, you'll first need to do the following two things:
|
||||
|
||||
1. **Enable model actions for the database connection**. To enable actions for a database connection, admins should click on the **gear** icon in the upper right and navigate to **Admin settings** > **Databases**, then click on the database you want to create actions for. On the right side of the connection settings form, toggle the **Model actions** option. For actions to work, the database user account (the account you're using to connect to the database) must have [write permissions](../databases/users-roles-privileges.md#privileges-to-enable-actions). And for now, actions are only supported on PostgreSQL and MySQL databases.
|
||||
1. **Enable model actions for the database connection**. To enable actions for a database connection, admins should click on the **gear** icon in the upper right and navigate to **Admin settings** > **Databases**, then click on the database you want to create actions for. On the right side of the connection settings form, toggle the **Model actions** option. For actions to work, the database user account (the account you're using to connect to the database) must have [write permissions](../databases/users-roles-privileges.md#privileges-to-enable-actions-and-editable-table-data). And for now, actions are only supported on PostgreSQL and MySQL databases.
|
||||
2. **Create at least one model from that database.** Actions are associated with models, so you'll need to have created (or have access to) at least one model before you can start creating actions.
|
||||
|
||||
## Who can use actions
|
||||
|
||||
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 129 KiB |
@@ -83,7 +83,7 @@ To have Metabot generate SQL for you:
|
||||
3. Type cmd+b on Mac, ctrl+b on Windows, to open up the [chat sidebar](#the-metabot-chat-sidebar).
|
||||
4. Ask it to "Write a SQL query that..." and type your prompt.
|
||||
|
||||
Metabot will generate the SQL for you, but it won't run the query. This gives you a chance to inspect the code before running it. The native editor is designed to be read-only (so don't worry about Metabot dropping any tables), but you should still check the query to make sure it targets the data you want.
|
||||
Metabot will generate the SQL for you, but it won't run the query. This gives you a chance to inspect the code before running it.
|
||||
|
||||
If you don't specify a specific table in a natural language question, Metabot will only check the first 100 tables in the currently selected database. If your question pertains to tables other than those first 100 tables, Metabot may hallucinate the tables it needs, and the query will fail.
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
title: Metabot settings
|
||||
title: Metabot AI settings
|
||||
summary: Configure Metabot settings, including which collections Metabot can access, and learn tips for improving Metabot's performance through data modeling and metadata.
|
||||
---
|
||||
|
||||
# Metabot settings
|
||||
# Metabot AI settings
|
||||
|
||||
{% include beta-blockquote.html
|
||||
message="For now, <a href='https://www.metabase.com/features/metabot-ai'>Metabot</a> is only available as an add-on for Pro and Enterprise plans on Metabase Cloud."
|
||||
@@ -13,21 +13,26 @@ _Settings > Admin settings > AI_
|
||||
|
||||
This page covers admin settings for Metabase's AI assistant, [Metabot](./metabot.md).
|
||||
|
||||
## Configure Metabot
|
||||
|
||||
To help Metabot focus on the data you care about most, select the collection containing the models and metrics you'd like it to work with.
|
||||
|
||||
## Pick a collection for Metabot to have access to
|
||||
|
||||

|
||||
|
||||
To select a collection for Metabot:
|
||||
## Verified content
|
||||
|
||||
Admins on Pro and Enterprise plans can tell Metabot to only work with [models](../data-modeling/models.md) and [metrics](../data-modeling/metrics.md) that have been [verified](../exploration-and-organization/content-verification.md).
|
||||
|
||||
Restricting Metabot to verified models and metrics (and only models and metrics) can help Metabot produce more reliable answers, since you know someone has at least vetted the data Metabot can use.
|
||||
|
||||
## When embedding Metabot, you can pick a collection for Metabot to have access to
|
||||
|
||||
When embedding Metabot in your app, you can select a collection for Metabot:
|
||||
|
||||
1. Go to **Settings** > **Admin settings** > **AI**.
|
||||
2. Click the **Collection** dropdown.
|
||||
2. Click **Embedded Metabot**.
|
||||
3. In the **Collection Embedded Metabot can use** section, click **Pick a collection**.
|
||||
3. Select the collection that contains the models and metrics you want Metabot to use.
|
||||
|
||||
Metabot will use the models and metrics in that collection to help answer questions and generate queries. You can change this collection at any time.
|
||||
Metabot will use the models and metrics in that collection to help answer questions and generate queries. You can change this collection at any time. To give Metabot access to all collections, you can set the collection to the root collection, called "Our Analytics" (the default).
|
||||
|
||||
Alternatively (or additionally), you can restrict Metabot to [verified content](#verified-content).
|
||||
|
||||
## Tips for making the most of Metabot
|
||||
|
||||
@@ -66,7 +71,7 @@ Metabase will try to set semantic types automatically, but you should confirm th
|
||||
|
||||
### Curate prompt suggestions
|
||||
|
||||
When you select a collection for Metabot to "learn", Metabot will suggest a series of prompts based on the content it finds in that collection. These give people a feel for the kinds of things people can ask Metabot to do.
|
||||
When you select a collection for Metabot to "learn", Metabot will suggest a series of prompts based on the content it finds in that collection. These prompts just give people a feel for the kinds of things people can ask Metabot to do.
|
||||
|
||||
Admins can run these generated prompts to test the answers, or trash the individual prompts if they're not useful or misleading. You can also regenerate all the prompts with a click.
|
||||
|
||||
|
||||
1645
docs/api.html
@@ -80,6 +80,7 @@ Here's what you need to know when syncing Google Sheets:
|
||||
- **Special character replacement**. Some characters just don't play nice with databases (like "?"). When we find these in column names, we'll replace them with "x" to keep things running smoothly.
|
||||
- **Renamed files will create new tables**. If you rename files in your folder or tabs in your sheets, we'll treat them as brand new tables and import them fresh.
|
||||
- **New columns sync automatically**. Adding new columns to your sheets? No problem — they'll show up in Metabase as expected.
|
||||
- **Renaming sheet columns will create new table columns**. If you rename a column in a spreadsheet, Metabase will create a new column with the new name in the synced table. The original column will remain in the table but it'll become empty. You'll be able to hide the old column in Table Metadata settings.
|
||||
- **Empty sheets won't import**. We won't import completely empty sheets or sheets that only have column headers. There needs to be some actual data in there.
|
||||
- **Google Sheets must have unique names**. If files in the synced Google Drive folder (and its subfolders) have the same name (e.g., one sheet is in the root folder, another sheet in a subfolder), the sheets might not sync properly.
|
||||
|
||||
|
||||
@@ -137,8 +137,6 @@ config:
|
||||
embedding-app-origins-interactive: null
|
||||
embedding-app-origins-sdk: ''
|
||||
embedding-homepage: hidden
|
||||
embedding-hub-production-embed-snippet-created: false
|
||||
embedding-hub-test-embed-snippet-created: false
|
||||
embedding-secret-key: null
|
||||
enable-embedding-interactive: false
|
||||
enable-embedding-sdk: false
|
||||
|
||||
@@ -585,24 +585,6 @@ Allow Metabase SDK access to these space delimited origins.
|
||||
|
||||
Embedding homepage status, indicating if it's visible, hidden or has been dismissed.
|
||||
|
||||
### `MB_EMBEDDING_HUB_PRODUCTION_EMBED_SNIPPET_CREATED`
|
||||
|
||||
- Type: boolean
|
||||
- Default: `false`
|
||||
- [Exported as](../installation-and-operation/serialization.md): `embedding-hub-production-embed-snippet-created`.
|
||||
- [Configuration file name](./config-file.md): `embedding-hub-production-embed-snippet-created`
|
||||
|
||||
Indicates if a production embed snippet has been created for tracking in the embedding hub.
|
||||
|
||||
### `MB_EMBEDDING_HUB_TEST_EMBED_SNIPPET_CREATED`
|
||||
|
||||
- Type: boolean
|
||||
- Default: `false`
|
||||
- [Exported as](../installation-and-operation/serialization.md): `embedding-hub-test-embed-snippet-created`.
|
||||
- [Configuration file name](./config-file.md): `embedding-hub-test-embed-snippet-created`
|
||||
|
||||
Indicates if a test embed snippet has been created for tracking in the embedding hub.
|
||||
|
||||
### `MB_EMBEDDING_SECRET_KEY`
|
||||
|
||||
- Type: string
|
||||
|
||||
@@ -175,6 +175,8 @@ Which cards you can connect a filter or parameter widget to depends on:
|
||||
- Whether the card has a relevant field (i.e., the field the widget is filtering).
|
||||
- Where the widget is located: on the dashboard, heading card, or question card. Only dashboard widgets can be connected to cards across all [dashboard tabs](./introduction.md#dashboard-tabs). Widgets on heading and question cards are restricted to the cards on their respective tabs.
|
||||
|
||||
For cards built in the query builder, _filters_ can be connected to fields at any stage of the query. _Time grouping parameters_ can only be connected to fields in the last stage of the query, and so to make use of time grouping parameters, your query should have a summary by a datetime field as the last stage. For example, if you have a query that's just computing `Count` of products by `Category` and `Created At`, you'll be able to connect the time grouping parameter to the `Created At` grouping, but if you modify the query to filter for results with `Count > 100`, you won't be able to connect time grouping parameter on a dashboard to that query because the last stage is now a filter.
|
||||
|
||||
## Filtering dashboards with native/SQL questions
|
||||
|
||||
To connect a dashboard filter widget to a card with a question created with the native editor, you'll need to [add a field filter or basic variable to your query](../questions/native-editor/sql-parameters.md).
|
||||
|
||||
@@ -58,29 +58,32 @@ If there are no results, you can tell Metabase to skip sending the email.
|
||||
|
||||
Here you can set filters to apply when Metabase sends the subscription.
|
||||
|
||||
### Attach results
|
||||
## Attach results
|
||||
|
||||
Tell Metabase if it should attach results to the email as a file, in addition to displaying the table in the email body.
|
||||
When configuring a subscription, you can tell Metabase if it should attach results to the email as a file, in addition to displaying the table in the email body.
|
||||
|
||||
If you've added filters to your dashboard and set default values for those filters, Metabase will apply those default values to your subscriptions, filtering the results of all questions that are connected to those filters when the subscriptions are sent. To learn more, check out [dashboard filters](./filters.md).
|
||||
|
||||
#### File format
|
||||
### File format
|
||||
|
||||
You can choose between the following file formats:
|
||||
|
||||
- .csv
|
||||
- .xlsx
|
||||
- .json
|
||||
|
||||
#### Use unformatted values in attachments
|
||||
### Use unformatted values in attachments
|
||||
|
||||
If you check this box, Metabase will drop any visualization settings applied to the questions' results (e.g., date formatting) and send only the raw, unformatted results.
|
||||
|
||||
#### Questions to attach
|
||||
### Questions to attach
|
||||
|
||||
Here you can specify which questions Metabase should attach results for.
|
||||
Here you can specify which questions Metabase should attach results for. If you don't select any questions, Metabase won't send any attachments.
|
||||
|
||||
The attached files will include up to 1 048 575 rows by default. If you're self-hosting Metabase, you can adjust this row limit by setting the environment variable [MB_ATTACHMENT_ROW_LIMIT](../configuring-metabase/environment-variables.md#mb_attachment_row_limit). To change this row limit on your Metabase Cloud instance, you can [contact us](https://www.metabase.com/help-premium) and request a different row limit.
|
||||
The attached files will include up to 1 048 575 rows by default (Excel's row limit for spreadsheets). If you're self-hosting Metabase, you can adjust this row limit by setting the environment variable [`MB_ATTACHMENT_ROW_LIMIT`](../configuring-metabase/environment-variables.md#mb_attachment_row_limit). To change this row limit on your Metabase Cloud instance, you can [contact us](https://www.metabase.com/help-premium) and request a different row limit.
|
||||
|
||||
### Send only attachments (no charts)
|
||||
|
||||
You can toggle on "Send only attachments (no charts)" if you want to receive just the .csv or .xlsx files without the charts. Remember to select the results you want to attach in "Questions to attach" above the toggle.
|
||||
|
||||
## Slack subscription options
|
||||
|
||||
|
||||
85
docs/data-modeling/editable-tables.md
Normal file
@@ -0,0 +1,85 @@
|
||||
---
|
||||
title: Editable tables
|
||||
summary: Admins can create, update, and delete data in tables right in Metabase.
|
||||
---
|
||||
|
||||
# Editable tables
|
||||
|
||||
{% include plans-blockquote.html feature="Editable table data" %}
|
||||
|
||||
Admins can enable editable table data in Metabase.
|
||||
|
||||

|
||||
|
||||
## Setting up editable tables
|
||||
|
||||
_Currently, editable table data is only available for PostgreSQL and MySQL._
|
||||
|
||||
To set up table editing, admins can:
|
||||
|
||||
1. Click the **gear** icon in the top right.
|
||||
2. Select **Admin settings**.
|
||||
3. Click **Databases**.
|
||||
4. Select your database.
|
||||
5. Toggle on **Editable table data**.
|
||||
|
||||
For this to work, Metabase's connection to your database must support writing back to its tables. See [privileges to enable actions and editable table data](../databases/users-roles-privileges.md#privileges-to-enable-actions-and-editable-table-data).
|
||||
|
||||
## Update or delete records in a table
|
||||
|
||||
To update or delete records in a table:
|
||||
|
||||
1. Open the left sidebar.
|
||||
2. In the Data section, click **Databases**.
|
||||
3. Hover over the table you want to edit and click the **Pencil** icon.
|
||||
4. Edit rows in the table just like you would in a spreadsheet.
|
||||
|
||||
Depending on the data in the column, Metabase may show a list of values to choose from. If you need to add an option to that list, simply type out the option and add it (you may need to refresh the browser for the new option to show up in the list).
|
||||
|
||||

|
||||
|
||||
To remove an option from the list of values, you'll need to change any records with that value to a different value.
|
||||
|
||||
## Create a new record
|
||||
|
||||

|
||||
|
||||
You can add a row to a table by clicking **+ New record** in the top right of the table. Fill out the fields. Metabase will respect field constraints, so if a field is required, Metabase will prevent you from saving the record until you fill in the required field.
|
||||
|
||||
## Delete a record
|
||||
|
||||
To delete a record, check the box next to the record you want to delete and click **Delete**. Deletion is permanent. Metabase will prompt you to confirm the deletion.
|
||||
|
||||
You can also select multiple rows and delete them all at once (nervous sweating).
|
||||
|
||||
If you try to delete a row that has foreign key references from other tables, your database may prevent the deletion to maintain referential integrity. You'll see an error message like "Other rows refer to this row so it cannot be deleted." To delete these rows, you'll either need to remove the referencing rows first, or change the foreign key constraints in your database.
|
||||
|
||||
## Metabase enforces type constraints
|
||||
|
||||
- You can't edit sequential primary keys.
|
||||
- If you try to add a value of a different type (like changing the number 13579 to the string "Wizard"), Metabase won't allow it.
|
||||
|
||||
## Editable table data vs. actions
|
||||
|
||||
Metabase offers two ways to modify data in your database: **editable table data** and [actions](../actions/introduction.md).
|
||||
|
||||
### When to use editable table data
|
||||
|
||||
Use editable table data to:
|
||||
|
||||
- Do data maintenance or cleanup.
|
||||
- Modify many records at once.
|
||||
- Get direct access to all table data.
|
||||
- Do one-off data corrections.
|
||||
|
||||
You need to be an admin to edit tables directly.
|
||||
|
||||
### When to use actions
|
||||
|
||||
Use actions to:
|
||||
|
||||
- Create custom forms.
|
||||
- Give non-admins the ability to modify data.
|
||||
- Add a button to a dashboard to trigger a form or query.
|
||||
|
||||
See [actions](../actions/introduction.md).
|
||||
BIN
docs/data-modeling/images/add-option.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
docs/data-modeling/images/create-new-record.png
Normal file
|
After Width: | Height: | Size: 287 KiB |
BIN
docs/data-modeling/images/customize-list.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
docs/data-modeling/images/edit-table.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
docs/data-modeling/images/model-list-detail.png
Normal file
|
After Width: | Height: | Size: 237 KiB |
BIN
docs/data-modeling/images/model-list-view.png
Normal file
|
After Width: | Height: | Size: 129 KiB |
@@ -40,10 +40,14 @@ Models you create are automatically [pinned to the current collection](../explor
|
||||
|
||||
### Create a model from scratch
|
||||
|
||||
1. In the upper right, click **New +** > **Model**.
|
||||
2. Choose either the query builder or a native query (if you want to use SQL). The advantage of using the query builder is that Metabase will be able to fill out some of the metadata for you; if you use SQL, you'll have to fill out that metadata manually.
|
||||
3. Select your data.
|
||||
4. Create and save your query.
|
||||
- Navigate to the Models tab in the sidebar. You might have to open it using the button in the top left, then scroll down to the section labeled **Data**, and pick **Models**. Then click on the **+** button in the top right.
|
||||
- Or open the [command palette](https://www.metabase.com/docs/latest/exploration-and-organization/exploration#command-palette) and type "model." Then click on the **New model** action.
|
||||
|
||||
Now choose either the query builder or a native query (if you want to use SQL). The advantage of using the query builder is that Metabase will be able to fill out some of the metadata for you; if you use SQL, you'll have to fill out that metadata manually.
|
||||
|
||||
Next, select your data, create your query, and save it.
|
||||
|
||||
Models you create are automatically [pinned to the current collection](../exploration-and-organization/collections.md#pinned-items).
|
||||
|
||||
### Create a model from a saved question
|
||||
|
||||
@@ -115,6 +119,46 @@ There are some limitations to this indexing:
|
||||
|
||||
You can edit a model's query by clicking on the down arrow next to the model's name and clicking on **Edit query definition**. When you're doing editing, be sure to save your changes. Unlike questions, which prompt you to save as a new question, any changes here will overwrite the existing model. If you want to create a new model from an existing model, select **Duplicate this model** from the model sidebar (the icon of two overlapping squares).
|
||||
|
||||
## Model list view
|
||||
|
||||

|
||||
|
||||
To view a model as a list:
|
||||
|
||||
1. Visit the model.
|
||||
2. Click the three-dot menu.
|
||||
3. Select **Edit metadata**.
|
||||
4. Navigate to the **Settings** tab.
|
||||
5. Under "What should the default view of this data be?", toggle to **List**.
|
||||
|
||||

|
||||
|
||||
### Customize model list view
|
||||
|
||||

|
||||
|
||||
You can customize how the data appears by clicking **Customize the List layout**.
|
||||
|
||||
Each item in the list has:
|
||||
|
||||
- An entity icon
|
||||
- A left column with:
|
||||
- The title or primary identifier on top.
|
||||
- A subtitle/description or secondary identifier.
|
||||
- An image (only if the record contains an image link)
|
||||
- A right column for up to 5 additional columns.
|
||||
|
||||
You can:
|
||||
|
||||
- **Search for columns** using the "Find a column..." search box
|
||||
- **Drag columns** from the available list into either the left (Title + Subtitle) or right columns areas
|
||||
- **Reorder columns** by dragging them within their respective areas
|
||||
- **Remove columns** by clicking on the X on the column.
|
||||
|
||||
You can see a preview on the bottom with sample data from your model.
|
||||
|
||||
Click **Done** to save your changes.
|
||||
|
||||
## Start a question from a model
|
||||
|
||||
See [asking questions][question].
|
||||
|
||||
@@ -101,7 +101,7 @@ Admins, and people with [permission to manage table metadata](../permissions/dat
|
||||
|
||||
Data types can't be edited in Metabase directly, but you can cast certain [data types to different types](./metadata-editing.md#cast-to-a-specific-data-type) so that, for example, Metabase will interpret a text data type as a date type.
|
||||
|
||||
Changes made in Table Metadata apply across your entire Metabase. Metabase currently only supports casting to a datetime type in Metadata settings. However, if you build a query in the query builder, you can use type casting custom expressions like [`date()`](../questions/query-builder/expressions-list.md#date) or [`integer()`](../questions/query-builder/expressions-list.md#integer) to cast a string to a different type in your query.
|
||||
Changes made in Table Metadata apply across your entire Metabase. However, if you build a query in the query builder, you can use type casting custom expressions like [`date()`](../questions/query-builder/expressions-list.md#date) or [`integer()`](../questions/query-builder/expressions-list.md#integer) to cast a string to a different type just in your query.
|
||||
|
||||
### Semantic types don't change the data types
|
||||
|
||||
|
||||