- Now let's export some tables and pre-made queries from our staging
- area in Splitgraph to our cache-optimized{" "}
+ {["uninitialized", "unstarted", "awaiting_import"].includes(
+ stepperState
+ )
+ ? "Next we'll "
+ : "Now let's "}
+ export some tables and pre-made queries from our staging area in
+ Splitgraph to our cache-optimized{" "}
Seafowl
{" "}
- instance running at https://demo.seafowl.cloud.{" "}
+ instance running at https://demo.seafowl.cloud. This
+ demo exports them programatically with{" "}
+
+ madatdata
+ {" "}
+ calling the Splitgraph API from a Next.js API route, but you can
+ write your own queries and manually export them from the{" "}
+
+ Splitgraph Console
+ {" "}
+ (once you've created an account and logged into Splitgraph).
{stepStatus === "active" && (
- <> Click the button to start the export.>
+ <>
+
+ Click the button to start the export. While it's
+ running, you can use the embedded query editors to play with the
+ imported Splitgraph data, and when it's complete, you can run
+ the same queries in Seafowl.
+ >
)}
+ These are the tables that we'll export from Splitgraph to Seafowl.
+ You can query them in Splitgraph now, and then when the export is
+ complete, you'll be able to query them in Seafowl too.
+
+ >
+ ) : (
+ <>
+
Exported Tables
+
+ We successfully exported the tables to Seafowl, so now you can query
+ them in Seafowl too.
+
+ We've prepared a few queries to export from Splitgraph to Seafowl,
+ so that we can use them to render the charts that we want.
+ Splitgraph will execute the query and insert its result into
+ Seafowl. You can query them in Splitgraph now, and then when the
+ export is complete, you'll be able to query them in Seafowl too.
+
+ >
+ ) : (
+ <>
+
Exported Queries
+
+ We successfully exported these queries from Splitgraph to Seafowl,
+ so now you can query them in Seafowl too.{" "}
+
+ Note: If some queries failed to export, it's probably because they
+ had empty result sets (e.g. the table of issue reactions)
+
+
+ >
+ )}
+
{queriesToExport
.filter((_) => true)
.map((exportQuery) => (
@@ -264,10 +345,24 @@ const ExportPreview = ({
);
};
+/**
+ * Given a function to match a candidate `ExportTable` to (presumably) an `ExportTableInput`,
+ * determine if the table (which could also be a query - it's keyed by `destinationSchema`
+ * and `destinationTable`) is currently exporting (`loading`) or has exported (`completed`).
+ *
+ * Return `{ loading, completed, unstarted }`, where:
+ *
+ * * `loading` is `true` if there is a match in the `exportedTablesLoading` set,
+ * * `completed` is `true` if there is a match in the `exportedTablesCompleted` set
+ * (or if `stepperState` is `export_complete`),
+ * * `unstarted` is `true` if there is no match in either set.
+ *
+ */
const useFindMatchingExportTable = (
isMatch: (candidateTable: ExportTable) => boolean
) => {
- const [{ exportedTablesLoading, exportedTablesCompleted }] = useStepper();
+ const [{ stepperState, exportedTablesLoading, exportedTablesCompleted }] =
+ useStepper();
const matchingCompletedTable = useMemo(
() => Array.from(exportedTablesCompleted).find(isMatch),
@@ -278,7 +373,11 @@ const useFindMatchingExportTable = (
[exportedTablesLoading, isMatch]
);
- const completed = matchingCompletedTable ?? false;
+ // If the state is export_complete, we might have loaded the page directly
+ // and thus we don't have the sets of exportedTablesCompleted, but we know they exist
+ const exportFullyCompleted = stepperState === "export_complete";
+
+ const completed = matchingCompletedTable ?? (exportFullyCompleted || false);
const loading = matchingLoadingTable ?? false;
const unstarted = !completed && !loading;
@@ -289,6 +388,10 @@ const useFindMatchingExportTable = (
};
};
+const useStepperDebug = () => useStepper()[0].debug;
+
+import EmbeddedQueryStyles from "./EmbeddedQuery.module.css";
+
const ExportEmbedPreviewTableOrQuery = <
ExportInputShape extends ExportQueryInput | ExportTableInput
>({
@@ -312,6 +415,8 @@ const ExportEmbedPreviewTableOrQuery = <
splitgraphRepository: string;
};
}) => {
+ const debug = useStepperDebug();
+
const embedProps = {
importedRepository,
tableName:
@@ -321,7 +426,7 @@ const ExportEmbedPreviewTableOrQuery = <
makeQuery: () => makeQuery({ ...exportInput, ...importedRepository }),
};
- const { unstarted, loading, completed } = useFindMatchingExportTable(
+ const { loading, completed } = useFindMatchingExportTable(
makeMatchInputToExported(exportInput)
);
@@ -334,27 +439,82 @@ const ExportEmbedPreviewTableOrQuery = <
"splitgraph"
);
+ const linkToConsole = useMemo(() => {
+ switch (selectedTab) {
+ case "splitgraph":
+ return {
+ anchor: "Open in Console",
+ href: makeSplitgraphQueryHref(
+ makeQuery({ ...exportInput, ...importedRepository })
+ ),
+ };
+
+ case "seafowl":
+ return {
+ anchor: "Open in Console",
+ href: makeSeafowlQueryHref(
+ makeQuery({ ...exportInput, ...importedRepository })
+ ),
+ };
+ }
+ }, [selectedTab]);
+
return (
- <>
-
+
+
{heading}
-
-
-
+
+
+ setSelectedTab("splitgraph")}
+ active={selectedTab === "splitgraph"}
+ style={{ marginRight: "1rem" }}
+ title={
+ selectedTab === "splitgraph"
+ ? ""
+ : "Query the imported data in Splitgraph"
+ }
+ >
+ data.splitgraph.com
+
+ setSelectedTab("seafowl")}
+ active={selectedTab === "seafowl"}
+ disabled={!completed}
+ style={{ marginRight: "1rem" }}
+ title={
+ selectedTab === "seafowl"
+ ? ""
+ : completed
+ ? "Query the exported data in Seafowl"
+ : "Once you export the data to Seafowl, you can send the same query to Seafowl"
+ }
+ >
+ demo.seafowl.cloud
+
+ {loading && (
+
+ `Export to Seafowl: Started ${seconds} seconds ago...`
+ }
+ />
+ )}
+
- );
-};
diff --git a/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/StepperContext.tsx b/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/StepperContext.tsx
index 810414e..9eb3b0c 100644
--- a/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/StepperContext.tsx
+++ b/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/StepperContext.tsx
@@ -1,10 +1,9 @@
-import { createContext, useContext, useMemo } from "react";
+import { createContext, useContext } from "react";
import {
StepperState,
StepperAction,
useStepperReducer,
} from "./stepper-states";
-import type { ExportTable } from "./stepper-states";
// Define the context
const StepperContext = createContext<
@@ -35,46 +34,3 @@ export const useStepper = () => {
};
export const useStepperDebug = () => useStepper()[0].debug;
-
-/**
- * Given a function to match a candidate `ExportTable` to (presumably) an `ExportTableInput`,
- * determine if the table (which could also be a query - it's keyed by `destinationSchema`
- * and `destinationTable`) is currently exporting (`loading`) or has exported (`completed`).
- *
- * Return `{ loading, completed, unstarted }`, where:
- *
- * * `loading` is `true` if there is a match in the `exportedTablesLoading` set,
- * * `completed` is `true` if there is a match in the `exportedTablesCompleted` set
- * (or if `stepperState` is `export_complete`),
- * * `unstarted` is `true` if there is no match in either set.
- *
- */
-export const useFindMatchingExportTable = (
- isMatch: (candidateTable: ExportTable) => boolean
-) => {
- const [{ stepperState, exportedTablesLoading, exportedTablesCompleted }] =
- useStepper();
-
- const matchingCompletedTable = useMemo(
- () => Array.from(exportedTablesCompleted).find(isMatch),
- [exportedTablesCompleted, isMatch]
- );
- const matchingLoadingTable = useMemo(
- () => Array.from(exportedTablesLoading).find(isMatch),
- [exportedTablesLoading, isMatch]
- );
-
- // If the state is export_complete, we might have loaded the page directly
- // and thus we don't have the sets of exportedTablesCompleted, but we know they exist
- const exportFullyCompleted = stepperState === "export_complete";
-
- const completed = matchingCompletedTable ?? (exportFullyCompleted || false);
- const loading = matchingLoadingTable ?? false;
- const unstarted = !completed && !loading;
-
- return {
- completed,
- loading,
- unstarted,
- };
-};
diff --git a/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/export-hooks.tsx b/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/export-hooks.tsx
new file mode 100644
index 0000000..c0185d3
--- /dev/null
+++ b/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/export-hooks.tsx
@@ -0,0 +1,132 @@
+import { useEffect, useMemo } from "react";
+import type { ExportTable } from "./stepper-states";
+import { useStepper } from "./StepperContext";
+
+/**
+ * Given a function to match a candidate `ExportTable` to (presumably) an `ExportTableInput`,
+ * determine if the table (which could also be a query - it's keyed by `destinationSchema`
+ * and `destinationTable`) is currently exporting (`loading`) or has exported (`completed`).
+ *
+ * Return `{ loading, completed, unstarted }`, where:
+ *
+ * * `loading` is `true` if there is a match in the `exportedTablesLoading` set,
+ * * `completed` is `true` if there is a match in the `exportedTablesCompleted` set
+ * (or if `stepperState` is `export_complete`),
+ * * `unstarted` is `true` if there is no match in either set.
+ *
+ */
+export const useFindMatchingExportTable = (
+ isMatch: (candidateTable: ExportTable) => boolean
+) => {
+ const [{ stepperState, exportedTablesLoading, exportedTablesCompleted }] =
+ useStepper();
+
+ const matchingCompletedTable = useMemo(
+ () => Array.from(exportedTablesCompleted).find(isMatch),
+ [exportedTablesCompleted, isMatch]
+ );
+ const matchingLoadingTable = useMemo(
+ () => Array.from(exportedTablesLoading).find(isMatch),
+ [exportedTablesLoading, isMatch]
+ );
+
+ // If the state is export_complete, we might have loaded the page directly
+ // and thus we don't have the sets of exportedTablesCompleted, but we know they exist
+ const exportFullyCompleted = stepperState === "export_complete";
+
+ const completed = matchingCompletedTable ?? (exportFullyCompleted || false);
+ const loading = matchingLoadingTable ?? false;
+ const unstarted = !completed && !loading;
+
+ return {
+ completed,
+ loading,
+ unstarted,
+ };
+};
+
+export const usePollExportTasks = () => {
+ const [{ stepperState, loadingExportTasks }, dispatch] = useStepper();
+
+ useEffect(() => {
+ if (stepperState !== "awaiting_export") {
+ return;
+ }
+
+ const taskIds = Array.from(loadingExportTasks).map(({ taskId }) => taskId);
+
+ if (taskIds.length === 0) {
+ return;
+ }
+
+ const abortController = new AbortController();
+
+ const pollEachTaskOnce = () =>
+ Promise.all(
+ taskIds.map((taskId) =>
+ pollExportTaskOnce({
+ taskId,
+ onSuccess: ({ taskId }) =>
+ dispatch({
+ type: "export_task_complete",
+ completedTask: { taskId },
+ }),
+ onError: ({ taskId, error }) =>
+ dispatch({
+ type: "export_error",
+ error: `Error exporting ${taskId}: ${error.message}`,
+ }),
+ abortSignal: abortController.signal,
+ })
+ )
+ );
+
+ const interval = setInterval(pollEachTaskOnce, 3000);
+ return () => {
+ clearInterval(interval);
+ abortController.abort();
+ };
+ }, [loadingExportTasks, stepperState, dispatch]);
+};
+
+const pollExportTaskOnce = async ({
+ taskId,
+ onSuccess,
+ onError,
+ abortSignal,
+}: {
+ taskId: string;
+ onSuccess: ({ taskId }: { taskId: string }) => void;
+ onError: ({ taskId, error }: { taskId: string; error: any }) => void;
+ abortSignal: AbortSignal;
+}) => {
+ try {
+ const response = await fetch("/api/await-export-to-seafowl-task", {
+ signal: abortSignal,
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ taskId,
+ }),
+ });
+ const data = await response.json();
+
+ if (data.completed) {
+ onSuccess({ taskId });
+ } else if (data.error) {
+ if (!data.completed) {
+ console.log("WARN: Failed status, not completed:", data.error);
+ } else {
+ throw new Error(data.error);
+ }
+ }
+ } catch (error) {
+ if (error.name === "AbortError") {
+ return;
+ }
+
+ onError({ taskId, error });
+ }
+};
diff --git a/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/stepper-states.ts b/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/stepper-states.ts
index 0030053..837d824 100644
--- a/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/stepper-states.ts
+++ b/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/stepper-states.ts
@@ -10,6 +10,12 @@ export type ExportTable = {
sourceQuery?: string;
};
+// NOTE: Multiple tables can have the same taskId, so we track them separately
+// in order to not need to redundantly poll the API for each table individually
+export type ExportTask = {
+ taskId: string;
+};
+
export type StepperState = {
stepperState:
| "uninitialized"
@@ -27,6 +33,8 @@ export type StepperState = {
exportedTablesCompleted?: Set;
exportError?: string;
debug?: string | null;
+ loadingExportTasks?: Set;
+ completedExportTasks?: Set;
};
export type StepperAction =
@@ -40,6 +48,7 @@ export type StepperAction =
| { type: "import_complete" }
| { type: "start_export"; tables: ExportTable[] }
| { type: "export_table_task_complete"; completedTable: ExportTable }
+ | { type: "export_task_complete"; completedTask: ExportTask }
| { type: "export_complete" }
| { type: "export_error"; error: string }
| { type: "import_error"; error: string }
@@ -54,6 +63,8 @@ const initialState: StepperState = {
importTaskId: null,
exportedTablesLoading: new Set(),
exportedTablesCompleted: new Set(),
+ loadingExportTasks: new Set(),
+ completedExportTasks: new Set(),
importError: null,
exportError: null,
debug: null,
@@ -216,33 +227,61 @@ const stepperReducer = (
});
}
+ // The API returns a list of tables where multiple can have the same taskId
+ // We want a unique set of taskIds so that we only poll for each taskId once
+ // (but note that we're storing a set of {taskId} objects but need to compare uniqueness on taskId)
+ const loadingExportTasks = new Set(
+ Array.from(
+ new Set(
+ Array.from(tables).map(({ taskId }) => taskId)
+ )
+ ).map((taskId) => ({ taskId }))
+ );
+ const completedExportTasks = new Set();
+
return {
...state,
exportedTablesLoading,
exportedTablesCompleted,
+ loadingExportTasks,
+ completedExportTasks,
stepperState: "awaiting_export",
};
- case "export_table_task_complete":
- const { completedTable } = action;
+ case "export_task_complete":
+ const {
+ completedTask: { taskId: completedTaskId },
+ } = action;
- // We're storing a set of completedTable objects, so we need to find the matching one to remove it
- const loadingTablesAfterRemoval = new Set(state.exportedTablesLoading);
- const loadingTabletoRemove = Array.from(loadingTablesAfterRemoval).find(
- ({ taskId }) => taskId === completedTable.taskId
+ // One taskId could match multiple tables, so find reference to each of them
+ // and then use that reference to delete them from loading set and add them to completed set
+ const completedTables = Array.from(state.exportedTablesLoading).filter(
+ ({ taskId }) => taskId === completedTaskId
);
- loadingTablesAfterRemoval.delete(loadingTabletoRemove);
-
- // Then we can add the matching one to the completed table
+ const loadingTablesAfterRemoval = new Set(state.exportedTablesLoading);
const completedTablesAfterAdded = new Set(state.exportedTablesCompleted);
- completedTablesAfterAdded.add(completedTable);
+ for (const completedTable of completedTables) {
+ loadingTablesAfterRemoval.delete(completedTable);
+ completedTablesAfterAdded.add(completedTable);
+ }
+
+ // There should only be one matching task, so find it and create new updated sets
+ const completedTask = Array.from(state.loadingExportTasks).find(
+ ({ taskId }) => taskId === completedTaskId
+ );
+ const loadingTasksAfterRemoval = new Set(state.loadingExportTasks);
+ const completedTasksAfterAdded = new Set(state.completedExportTasks);
+ loadingTasksAfterRemoval.delete(completedTask);
+ completedTasksAfterAdded.add(completedTask);
return {
...state,
exportedTablesLoading: loadingTablesAfterRemoval,
exportedTablesCompleted: completedTablesAfterAdded,
+ loadingExportTasks: loadingTasksAfterRemoval,
+ completedExportTasks: completedTasksAfterAdded,
stepperState:
- loadingTablesAfterRemoval.size === 0
+ loadingTasksAfterRemoval.size === 0
? "export_complete"
: "awaiting_export",
};
@@ -264,6 +303,8 @@ const stepperReducer = (
case "export_error":
return {
...state,
+ loadingExportTasks: new Set(),
+ completedExportTasks: new Set(),
exportedTablesLoading: new Set(),
exportedTablesCompleted: new Set(),
stepperState: "import_complete",
From 9835599223c08a60974551d4ef83d253b19c0c51 Mon Sep 17 00:00:00 2001
From: Miles Richardson
Date: Fri, 23 Jun 2023 04:07:28 +0100
Subject: [PATCH 28/36] Add workaround for failed exports of queries: create a
fallback table
For each query to export, optionally provide a fallback `CREATE TABLE`
query which will run if the export job for the query fails. Implement
this by calling an API route `/api/create-fallback-table-after-failed-export`
after an export for a query fails for any reason.
This works around the bug where queries with an empty result fail
to export to Seafowl, see: https://github.com/splitgraph/seafowl/issues/423
---
.../.env.test.local | 10 +
.../ImportExportStepper/DebugPanel.tsx | 10 +-
.../ImportExportStepper/ExportPanel.tsx | 5 +
.../ImportExportStepper/export-hooks.tsx | 130 ++++++++++-
.../ImportExportStepper/stepper-states.ts | 46 +++-
.../env-vars.d.ts | 24 ++
.../lib/backend/seafowl-db.ts | 83 +++++++
.../lib/backend/splitgraph-db.ts | 1 +
.../lib/config/queries-to-export.ts | 205 +++++++++++++-----
...eate-fallback-table-after-failed-export.ts | 85 ++++++++
.../types.ts | 22 ++
11 files changed, 551 insertions(+), 70 deletions(-)
create mode 100644 examples/nextjs-import-airbyte-github-export-seafowl/lib/backend/seafowl-db.ts
create mode 100644 examples/nextjs-import-airbyte-github-export-seafowl/pages/api/create-fallback-table-after-failed-export.ts
diff --git a/examples/nextjs-import-airbyte-github-export-seafowl/.env.test.local b/examples/nextjs-import-airbyte-github-export-seafowl/.env.test.local
index 2d80ce0..85b47c9 100644
--- a/examples/nextjs-import-airbyte-github-export-seafowl/.env.test.local
+++ b/examples/nextjs-import-airbyte-github-export-seafowl/.env.test.local
@@ -19,3 +19,13 @@ GITHUB_PAT_SECRET="github_pat_**********************_***************************
# e.g. To intercept requests to Splitgraph API sent from madatdata libraries in API routes
# You can also set this by running: yarn dev-mitm (see package.json)
# MITMPROXY_ADDRESS="http://localhost:7979"
+
+# OPTIONAL: Set Seafowl environment variables to use for creating fallback tables when exports fail
+# NOTE 1: At the moment the instance URL must be https://demo.seafowl.cloud because that's where
+# the Splitgraph export API exports tables to when no instance URL is specified, and we are
+# currently not specifying the instance URL when starting exports, and only use it when creating fallback tables.
+# NOTE 2: The dbname (SEAFOWL_INSTANCE_DATABASE) MUST match NEXT_PUBLIC_SPLITGRAPH_GITHUB_ANALYTICS_META_NAMESPACE
+#
+# SEAFOWL_INSTANCE_URL="https://demo.seafowl.cloud"
+# SEAFOWL_INSTANCE_SECRET="********************************"
+# SEAFOWL_INSTANCE_DATABASE="**********"
diff --git a/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/DebugPanel.tsx b/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/DebugPanel.tsx
index bfb7336..25193f1 100644
--- a/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/DebugPanel.tsx
+++ b/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/DebugPanel.tsx
@@ -8,7 +8,15 @@ export const DebugPanel = () => {
diff --git a/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/ExportPanel.tsx b/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/ExportPanel.tsx
index cdeef09..264609b 100644
--- a/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/ExportPanel.tsx
+++ b/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/ExportPanel.tsx
@@ -87,6 +87,11 @@ export const ExportPanel = () => {
destinationTable,
destinationSchema,
sourceQuery,
+ fallbackCreateTableQuery: queriesToExport.find(
+ (q) =>
+ q.destinationSchema === destinationSchema &&
+ q.destinationTable === destinationTable
+ )?.fallbackCreateTableQuery,
})
),
...data["tables"].map(
diff --git a/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/export-hooks.tsx b/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/export-hooks.tsx
index c0185d3..9e7f7d6 100644
--- a/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/export-hooks.tsx
+++ b/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/export-hooks.tsx
@@ -46,7 +46,10 @@ export const useFindMatchingExportTable = (
};
export const usePollExportTasks = () => {
- const [{ stepperState, loadingExportTasks }, dispatch] = useStepper();
+ const [
+ { stepperState, loadingExportTasks, exportedTablesLoading },
+ dispatch,
+ ] = useStepper();
useEffect(() => {
if (stepperState !== "awaiting_export") {
@@ -71,11 +74,50 @@ export const usePollExportTasks = () => {
type: "export_task_complete",
completedTask: { taskId },
}),
- onError: ({ taskId, error }) =>
+ onError: async ({ taskId, error }) => {
+ // If the task failed but we're not going to retry, then check if
+ // there is a fallback query to create the table, and if so,
+ // create it before marking the task as complete.
+ if (!error.retryable) {
+ // NOTE: There is an implicit assumption that `exportedTablesLoading`
+ // and `loadingExportTasks` are updated at the same time, which they
+ // are, by the reducer that handles the `export_task_start` and
+ // `export_task_complete` actions.
+ const maybeExportedQueryWithCreateTableFallback = Array.from(
+ exportedTablesLoading
+ ).find(
+ (t) => t.taskId === taskId && t.fallbackCreateTableQuery
+ );
+
+ if (maybeExportedQueryWithCreateTableFallback) {
+ await createFallbackTableAfterFailedExport({
+ destinationSchema:
+ maybeExportedQueryWithCreateTableFallback.destinationSchema,
+ destinationTable:
+ maybeExportedQueryWithCreateTableFallback.destinationTable,
+ fallbackCreateTableQuery:
+ maybeExportedQueryWithCreateTableFallback.fallbackCreateTableQuery,
+
+ // On error or success, we mutate the error variable which
+ // will be passed by `dispatch` outside of this conditional.
+ onError: (errorCreatingFallbackTable) => {
+ error.message = `${error.message} (and also error creating fallback: ${errorCreatingFallbackTable.message})`;
+ },
+ onSuccess: () => {
+ error = undefined; // No error because we consider the task complete after creating the fallback table.
+ },
+ });
+ }
+ }
+
dispatch({
- type: "export_error",
- error: `Error exporting ${taskId}: ${error.message}`,
- }),
+ type: "export_task_complete",
+ completedTask: {
+ taskId,
+ error,
+ },
+ });
+ },
abortSignal: abortController.signal,
})
)
@@ -86,7 +128,7 @@ export const usePollExportTasks = () => {
clearInterval(interval);
abortController.abort();
};
- }, [loadingExportTasks, stepperState, dispatch]);
+ }, [loadingExportTasks, exportedTablesLoading, stepperState, dispatch]);
};
const pollExportTaskOnce = async ({
@@ -97,7 +139,13 @@ const pollExportTaskOnce = async ({
}: {
taskId: string;
onSuccess: ({ taskId }: { taskId: string }) => void;
- onError: ({ taskId, error }: { taskId: string; error: any }) => void;
+ onError: ({
+ taskId,
+ error,
+ }: {
+ taskId: string;
+ error: { message: string; retryable: boolean };
+ }) => void;
abortSignal: AbortSignal;
}) => {
try {
@@ -118,6 +166,7 @@ const pollExportTaskOnce = async ({
} else if (data.error) {
if (!data.completed) {
console.log("WARN: Failed status, not completed:", data.error);
+ onError({ taskId, error: { message: data.error, retryable: false } });
} else {
throw new Error(data.error);
}
@@ -127,6 +176,71 @@ const pollExportTaskOnce = async ({
return;
}
- onError({ taskId, error });
+ onError({
+ taskId,
+ error: {
+ message: `Error exporting ${taskId}: ${
+ error.message ?? error.name ?? "unknown"
+ }`,
+ retryable: true,
+ },
+ });
+ }
+};
+
+/**
+ * Call the API route to create a fallback table after a failed export.
+ *
+ * Note that both `destinationTable` and `destinationSchema` should already
+ * be included in the `fallbackCreateTableQuery`, but we need them so that
+ * the endpoint can separately `CREATE SCHEMA` and `DROP TABLE` in case the
+ * schema does not yet exist, or the table already exists (we overwrite it to
+ * be consistent with behavior of Splitgraph export API).
+ */
+const createFallbackTableAfterFailedExport = async ({
+ destinationSchema,
+ destinationTable,
+ fallbackCreateTableQuery,
+ onSuccess,
+ onError,
+}: Required<
+ Pick<
+ ExportTable,
+ "destinationSchema" | "destinationTable" | "fallbackCreateTableQuery"
+ >
+> & {
+ onSuccess: () => void;
+ onError: (error: { message: string }) => void;
+}) => {
+ try {
+ const response = await fetch(
+ "/api/create-fallback-table-after-failed-export",
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ destinationSchema,
+ destinationTable,
+ fallbackCreateTableQuery,
+ }),
+ }
+ );
+ const data = await response.json();
+ if (data.error || !data.success) {
+ console.log(
+ `FAIL: error from endpoint creating fallback table: ${data.error}`
+ );
+ onError({ message: data.error ?? "unknown" });
+ } else {
+ console.log("SUCCESS: created fallback table");
+ onSuccess();
+ }
+ } catch (error) {
+ console.log(`FAIL: caught error while creating fallback table: ${error}`);
+ onError({
+ message: `${error.message ?? error.name ?? "unknown"}`,
+ });
}
};
diff --git a/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/stepper-states.ts b/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/stepper-states.ts
index 837d824..6185105 100644
--- a/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/stepper-states.ts
+++ b/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/stepper-states.ts
@@ -8,12 +8,14 @@ export type ExportTable = {
destinationTable: string;
taskId: string;
sourceQuery?: string;
+ fallbackCreateTableQuery?: string;
};
// NOTE: Multiple tables can have the same taskId, so we track them separately
// in order to not need to redundantly poll the API for each table individually
export type ExportTask = {
taskId: string;
+ error?: { message: string; retryable: boolean };
};
export type StepperState = {
@@ -35,6 +37,7 @@ export type StepperState = {
debug?: string | null;
loadingExportTasks?: Set;
completedExportTasks?: Set;
+ tasksWithError?: Map; // taskId -> errors
};
export type StepperAction =
@@ -65,6 +68,7 @@ const initialState: StepperState = {
exportedTablesCompleted: new Set(),
loadingExportTasks: new Set(),
completedExportTasks: new Set(),
+ tasksWithError: new Map(),
importError: null,
exportError: null,
debug: null,
@@ -217,12 +221,14 @@ const stepperReducer = (
destinationTable,
destinationSchema,
sourceQuery,
+ fallbackCreateTableQuery,
taskId,
} of tables) {
exportedTablesLoading.add({
destinationTable,
destinationSchema,
sourceQuery,
+ fallbackCreateTableQuery,
taskId,
});
}
@@ -248,11 +254,47 @@ const stepperReducer = (
stepperState: "awaiting_export",
};
+ /**
+ * NOTE: A task is "completed" even if it received an error, in which case
+ * we will retry it up to maxRetryCount if `error.retryable` is `true`
+ *
+ * That is, _all tasks_ will eventually "complete," whether successfully or not.
+ */
case "export_task_complete":
const {
- completedTask: { taskId: completedTaskId },
+ completedTask: { taskId: completedTaskId, error: maybeError },
} = action;
+ const maxRetryCount = 3;
+
+ const updatedTasksWithError = new Map(state.tasksWithError);
+ const previousErrors = updatedTasksWithError.get(completedTaskId) ?? [];
+ const hadPreviousError = previousErrors.length > 0;
+
+ if (!maybeError && hadPreviousError) {
+ updatedTasksWithError.delete(completedTaskId);
+ } else if (maybeError) {
+ updatedTasksWithError.set(completedTaskId, [
+ ...previousErrors,
+ maybeError.message,
+ ]);
+ const numAttempts = updatedTasksWithError.get(completedTaskId).length;
+
+ if (maybeError.retryable && numAttempts < maxRetryCount) {
+ console.log("RETRY: ", completedTaskId, `(${numAttempts} so far)`);
+ return {
+ ...state,
+ tasksWithError: updatedTasksWithError,
+ };
+ } else {
+ console.log(
+ "FAIL: ",
+ completedTaskId,
+ `(${numAttempts} reached max ${maxRetryCount})`
+ );
+ }
+ }
+
// One taskId could match multiple tables, so find reference to each of them
// and then use that reference to delete them from loading set and add them to completed set
const completedTables = Array.from(state.exportedTablesLoading).filter(
@@ -394,7 +436,7 @@ const useMarkAsComplete = (
if (!data.status) {
throw new Error(
- "Got unexpected resposne shape when marking import/export complete"
+ "Got unexpected response shape when marking import/export complete"
);
}
diff --git a/examples/nextjs-import-airbyte-github-export-seafowl/env-vars.d.ts b/examples/nextjs-import-airbyte-github-export-seafowl/env-vars.d.ts
index 650db4e..8781703 100644
--- a/examples/nextjs-import-airbyte-github-export-seafowl/env-vars.d.ts
+++ b/examples/nextjs-import-airbyte-github-export-seafowl/env-vars.d.ts
@@ -38,6 +38,30 @@ namespace NodeJS {
*/
MITMPROXY_ADDRESS?: string;
+ /**
+ * Optionally provide the SEAFOWL_INSTANCE_URL to use for creating fallback tables
+ * when an export fails.
+ *
+ * Note that at the moment, this must only be set to https://demo.seafowl.cloud
+ * because that's where Splitgraph exports to by default, and we are not currently
+ * passing any instance URL to the Splitgraph export API.
+ */
+ SEAFOWL_INSTANCE_URL?: "https://demo.seafowl.cloud";
+
+ /**
+ * Optionally provide the SEAFOWL_INSTANCE_SECRET to use for creating fallback tables
+ * when an export fails.
+ */
+ SEAFOWL_INSTANCE_SECRET?: string;
+
+ /**
+ * Optionally provide the dbname to use for creating fallback tables
+ * when an export fails.
+ *
+ * Note this MUST match the NEXT_PUBLIC_SPLITGRAPH_GITHUB_ANALYTICS_META_NAMESPACE
+ */
+ SEAFOWL_INSTANCE_DATABASE?: string;
+
/**
* The namespace of the repository in Splitgraph where metadata is stored
* containing the state of imported GitHub repositories, which should contain
diff --git a/examples/nextjs-import-airbyte-github-export-seafowl/lib/backend/seafowl-db.ts b/examples/nextjs-import-airbyte-github-export-seafowl/lib/backend/seafowl-db.ts
new file mode 100644
index 0000000..8ae7af1
--- /dev/null
+++ b/examples/nextjs-import-airbyte-github-export-seafowl/lib/backend/seafowl-db.ts
@@ -0,0 +1,83 @@
+import { makeSeafowlHTTPContext } from "@madatdata/core";
+
+export const makeAuthenticatedSeafowlHTTPContext = () => {
+ const { instanceURL, instanceDatabase, instanceSecret } =
+ getRequiredValidAuthenticatedSeafowlInstanceConfig();
+
+ // NOTE: This config object is a mess and will be simplified in a future madatdata update
+ // It's only necessary here because we're passing a secret
+ return makeSeafowlHTTPContext({
+ database: {
+ dbname: instanceDatabase,
+ },
+ authenticatedCredential: {
+ token: instanceSecret,
+ anonymous: false,
+ },
+ host: {
+ baseUrls: {
+ sql: instanceURL,
+ gql: "...",
+ auth: "...",
+ },
+ dataHost: new URL(instanceURL).host,
+ apexDomain: "...",
+ apiHost: "...",
+ postgres: {
+ host: "127.0.0.1",
+ port: 6432,
+ ssl: false,
+ },
+ },
+ });
+};
+
+const getRequiredValidAuthenticatedSeafowlInstanceConfig = () => {
+ const instanceURL = process.env.SEAFOWL_INSTANCE_URL;
+
+ if (!instanceURL) {
+ throw new Error("Missing SEAFOWL_INSTANCE_URL");
+ }
+
+ // This could be temporary if we want to allow configuring the instance URL,
+ // but for now we export to Splitgraph using no instance URL, which means
+ // it exports to demo.seafowl.cloud, and we only use this for creating
+ // fallback tables on failed exports (which is mostly a workaround anyway)
+ if (instanceURL && instanceURL !== "https://demo.seafowl.cloud") {
+ throw new Error(`If SEAFOWL_INSTANCE_URL is set, it should be set to https://demo.seafowl.cloud,
+ because that's where Splitgraph exports to by default, and we are not currently passing
+ any instance URL to the Splitgraph export API (though we could do that).
+ `);
+ }
+
+ const instanceSecret = process.env.SEAFOWL_INSTANCE_SECRET;
+ if (!instanceSecret) {
+ throw new Error("Missing SEAFOWL_INSTANCE_SECRET");
+ }
+
+ // This is at the config level, just like SPLITGRAPH_NAMESPACE, since the two
+ // of them are supposed to match
+ const instanceDatabase = process.env.SEAFOWL_INSTANCE_DATABASE;
+ if (!instanceDatabase) {
+ throw new Error("Missing SEAFOWL_INSTANCE_DATABASE");
+ }
+
+ const META_NAMESPACE =
+ process.env.NEXT_PUBLIC_SPLITGRAPH_GITHUB_ANALYTICS_META_NAMESPACE;
+ if (!META_NAMESPACE) {
+ throw new Error(
+ "Missing NEXT_PUBLIC_SPLITGRAPH_GITHUB_ANALYTICS_META_NAMESPACE"
+ );
+ }
+
+ if (instanceDatabase !== META_NAMESPACE) {
+ throw new Error(`SEAFOWL_INSTANCE_DATABASE (${instanceDatabase}) should match
+ NEXT_PUBLIC_SPLITGRAPH_GITHUB_ANALYTICS_META_NAMESPACE (${META_NAMESPACE})`);
+ }
+
+ return {
+ instanceURL,
+ instanceSecret,
+ instanceDatabase,
+ };
+};
diff --git a/examples/nextjs-import-airbyte-github-export-seafowl/lib/backend/splitgraph-db.ts b/examples/nextjs-import-airbyte-github-export-seafowl/lib/backend/splitgraph-db.ts
index ca1f2f2..5aa95b9 100644
--- a/examples/nextjs-import-airbyte-github-export-seafowl/lib/backend/splitgraph-db.ts
+++ b/examples/nextjs-import-airbyte-github-export-seafowl/lib/backend/splitgraph-db.ts
@@ -7,6 +7,7 @@ import { defaultSplitgraphHost } from "@madatdata/core";
const SPLITGRAPH_API_KEY = process.env.SPLITGRAPH_API_KEY;
const SPLITGRAPH_API_SECRET = process.env.SPLITGRAPH_API_SECRET;
+// Throw top level error on missing keys because these are _always_ required app to run
if (!SPLITGRAPH_API_KEY || !SPLITGRAPH_API_SECRET) {
throw new Error(
"Environment variable SPLITGRAPH_API_KEY or SPLITGRAPH_API_SECRET is not set." +
diff --git a/examples/nextjs-import-airbyte-github-export-seafowl/lib/config/queries-to-export.ts b/examples/nextjs-import-airbyte-github-export-seafowl/lib/config/queries-to-export.ts
index 099d4f2..94ebabe 100644
--- a/examples/nextjs-import-airbyte-github-export-seafowl/lib/config/queries-to-export.ts
+++ b/examples/nextjs-import-airbyte-github-export-seafowl/lib/config/queries-to-export.ts
@@ -17,80 +17,111 @@ export const makeQueriesToExport = ({
sourceQuery: string;
destinationSchema: string;
destinationTable: string;
+ /**
+ * Optionally provide a DDL query to create the (empty) destination table in
+ * case the export from Splitgraph fails. This is a workaround of a bug where
+ * exports from Splitgraph to Seafowl fail if the destination table does not
+ * contain any rows. See: https://github.com/splitgraph/seafowl/issues/423
+ *
+ * This way, even if a table fails to load, we can at least reference it in subsequent
+ * analytics queries without challenges like conditionally checking if it exists.
+ */
+ fallbackCreateTableQuery?: string;
}[] => [
+ {
+ destinationSchema: seafowlDestinationSchema,
+ destinationTable: "simple_stargazers_query",
+ sourceQuery: `
+SELECT * FROM "${splitgraphSourceNamespace}/${splitgraphSourceRepository}:${splitgraphSourceImageHashOrTag}".stargazers`,
+ },
+
{
destinationSchema: seafowlDestinationSchema,
destinationTable: "monthly_user_stats",
sourceQuery: `
- WITH
+WITH
- commits AS (
- SELECT
- date_trunc('month', created_at) AS created_at_month,
- author->>'login' AS username,
- count(*) as no_commits
- FROM "${splitgraphSourceNamespace}/${splitgraphSourceRepository}:${splitgraphSourceImageHashOrTag}".commits
- GROUP BY 1, 2
- ),
+commits AS (
+ SELECT
+ date_trunc('month', created_at) AS created_at_month,
+ author->>'login' AS username,
+ count(*) as no_commits
+ FROM "${splitgraphSourceNamespace}/${splitgraphSourceRepository}:${splitgraphSourceImageHashOrTag}".commits
+ GROUP BY 1, 2
+),
- comments AS (
- SELECT
- date_trunc('month', created_at) AS created_at_month,
- "user"->>'login' AS username,
- count(*) filter (where exists(select regexp_matches(issue_url, '.*/pull/.*'))) as no_pull_request_comments,
- count(*) filter (where exists(select regexp_matches(issue_url, '.*/issue/.*'))) as no_issue_comments,
- sum(length(body)) as total_comment_length
- FROM "${splitgraphSourceNamespace}/${splitgraphSourceRepository}:${splitgraphSourceImageHashOrTag}".comments
- GROUP BY 1, 2
- ),
+comments AS (
+ SELECT
+ date_trunc('month', created_at) AS created_at_month,
+ "user"->>'login' AS username,
+ count(*) filter (where exists(select regexp_matches(issue_url, '.*/pull/.*'))) as no_pull_request_comments,
+ count(*) filter (where exists(select regexp_matches(issue_url, '.*/issue/.*'))) as no_issue_comments,
+ sum(length(body)) as total_comment_length
+ FROM "${splitgraphSourceNamespace}/${splitgraphSourceRepository}:${splitgraphSourceImageHashOrTag}".comments
+ GROUP BY 1, 2
+),
- pull_requests AS (
- WITH pull_request_creator AS (
- SELECT id, "user"->>'login' AS username
- FROM "${splitgraphSourceNamespace}/${splitgraphSourceRepository}:${splitgraphSourceImageHashOrTag}".pull_requests
- )
+pull_requests AS (
+ WITH pull_request_creator AS (
+ SELECT id, "user"->>'login' AS username
+ FROM "${splitgraphSourceNamespace}/${splitgraphSourceRepository}:${splitgraphSourceImageHashOrTag}".pull_requests
+ )
- SELECT
- date_trunc('month', updated_at) AS created_at_month,
- username,
- count(*) filter (where merged = true) AS merged_pull_requests,
- count(*) AS total_pull_requests,
- sum(additions::integer) filter (where merged = true) AS lines_added,
- sum(deletions::integer) filter (where merged = true) AS lines_deleted
- FROM "${splitgraphSourceNamespace}/${splitgraphSourceRepository}:${splitgraphSourceImageHashOrTag}".pull_request_stats
- INNER JOIN pull_request_creator USING (id)
- GROUP BY 1, 2
- ),
+ SELECT
+ date_trunc('month', updated_at) AS created_at_month,
+ username,
+ count(*) filter (where merged = true) AS merged_pull_requests,
+ count(*) AS total_pull_requests,
+ sum(additions::integer) filter (where merged = true) AS lines_added,
+ sum(deletions::integer) filter (where merged = true) AS lines_deleted
+ FROM "${splitgraphSourceNamespace}/${splitgraphSourceRepository}:${splitgraphSourceImageHashOrTag}".pull_request_stats
+ INNER JOIN pull_request_creator USING (id)
+ GROUP BY 1, 2
+),
- all_months_users AS (
- SELECT DISTINCT created_at_month, username FROM commits
- UNION SELECT DISTINCT created_at_month, username FROM comments
- UNION SELECT DISTINCT created_at_month, username FROM pull_requests
- ),
+all_months_users AS (
+ SELECT DISTINCT created_at_month, username FROM commits
+ UNION SELECT DISTINCT created_at_month, username FROM comments
+ UNION SELECT DISTINCT created_at_month, username FROM pull_requests
+),
- user_stats AS (
- SELECT
- amu.created_at_month,
- amu.username,
- COALESCE(cmt.no_commits, 0) AS no_commits,
- COALESCE(cmnt.no_pull_request_comments, 0) AS no_pull_request_comments,
- COALESCE(cmnt.no_issue_comments, 0) AS no_issue_comments,
- COALESCE(cmnt.total_comment_length, 0) AS total_comment_length,
- COALESCE(pr.merged_pull_requests, 0) AS merged_pull_requests,
- COALESCE(pr.total_pull_requests, 0) AS total_pull_requests,
- COALESCE(pr.lines_added, 0) AS lines_added,
- COALESCE(pr.lines_deleted, 0) AS lines_deleted
+user_stats AS (
+ SELECT
+ amu.created_at_month,
+ amu.username,
+ COALESCE(cmt.no_commits, 0) AS no_commits,
+ COALESCE(cmnt.no_pull_request_comments, 0) AS no_pull_request_comments,
+ COALESCE(cmnt.no_issue_comments, 0) AS no_issue_comments,
+ COALESCE(cmnt.total_comment_length, 0) AS total_comment_length,
+ COALESCE(pr.merged_pull_requests, 0) AS merged_pull_requests,
+ COALESCE(pr.total_pull_requests, 0) AS total_pull_requests,
+ COALESCE(pr.lines_added, 0) AS lines_added,
+ COALESCE(pr.lines_deleted, 0) AS lines_deleted
- FROM all_months_users amu
- LEFT JOIN commits cmt ON amu.created_at_month = cmt.created_at_month AND amu.username = cmt.username
- LEFT JOIN comments cmnt ON amu.created_at_month = cmnt.created_at_month AND amu.username = cmnt.username
- LEFT JOIN pull_requests pr ON amu.created_at_month = pr.created_at_month AND amu.username = pr.username
+ FROM all_months_users amu
+ LEFT JOIN commits cmt ON amu.created_at_month = cmt.created_at_month AND amu.username = cmt.username
+ LEFT JOIN comments cmnt ON amu.created_at_month = cmnt.created_at_month AND amu.username = cmnt.username
+ LEFT JOIN pull_requests pr ON amu.created_at_month = pr.created_at_month AND amu.username = pr.username
- ORDER BY created_at_month ASC, username ASC
- )
+ ORDER BY created_at_month ASC, username ASC
+)
- SELECT * FROM user_stats;
+SELECT * FROM user_stats;
`,
+ fallbackCreateTableQuery: `
+CREATE TABLE "${seafowlDestinationSchema}".monthly_user_stats (
+ created_at_month TIMESTAMP,
+ username VARCHAR,
+ no_commits BIGINT,
+ no_pull_request_comments BIGINT,
+ no_issue_comments BIGINT,
+ total_comment_length BIGINT,
+ merged_pull_requests BIGINT,
+ total_pull_requests BIGINT,
+ lines_added BIGINT,
+ lines_deleted BIGINT
+);
+ `,
},
{
destinationSchema: seafowlDestinationSchema,
@@ -111,6 +142,62 @@ SELECT
FROM
"${splitgraphSourceNamespace}/${splitgraphSourceRepository}:${splitgraphSourceImageHashOrTag}"."issue_reactions"
GROUP BY 1, 2 ORDER BY 2, 3 DESC;
+`,
+ fallbackCreateTableQuery: `
+CREATE TABLE "${seafowlDestinationSchema}".monthly_issue_stats (
+ issue_number BIGINT,
+ created_at_month TIMESTAMP,
+ total_reacts BIGINT,
+ no_plus_one BIGINT,
+ no_minus_one BIGINT,
+ no_laugh BIGINT,
+ no_confused BIGINT,
+ no_heart BIGINT,
+ no_hooray BIGINT,
+ no_rocket BIGINT,
+ no_eyes BIGINT
+);
`,
},
];
+
+/** A generic demo query that can be used to show off Splitgraph */
+export const genericDemoQuery = `WITH t (
+ c_int16_smallint,
+ c_int32_int,
+ c_int64_bigint,
+ c_utf8_char,
+ c_utf8_varchar,
+ c_utf8_text,
+ c_float32_float,
+ c_float32_real,
+ c_boolean_boolean,
+ c_date32_date,
+ c_timestamp_microseconds_timestamp
+
+ ) AS (
+ VALUES(
+ /* Int16 / SMALLINT */
+ 42::SMALLINT,
+ /* Int32 / INT */
+ 99::INT,
+ /* Int64 / BIGINT */
+ 420420::BIGINT,
+ /* Utf8 / CHAR */
+ 'x'::CHAR,
+ /* Utf8 / VARCHAR */
+ 'abcdefghijklmnopqrstuvwxyz'::VARCHAR,
+ /* Utf8 / TEXT */
+ 'zyxwvutsrqponmlkjihgfedcba'::TEXT,
+ /* Float32 / FLOAT */
+ 4.4::FLOAT,
+ /* Float32 / REAL */
+ 2.0::REAL,
+ /* Boolean / BOOLEAN */
+ 't'::BOOLEAN,
+ /* Date32 / DATE */
+ '1997-06-17'::DATE,
+ /* Timestamp(us) / TIMESTAMP */
+ '2018-11-11T11:11:11.111111111'::TIMESTAMP
+ )
+ ) SELECT * FROM t;`;
diff --git a/examples/nextjs-import-airbyte-github-export-seafowl/pages/api/create-fallback-table-after-failed-export.ts b/examples/nextjs-import-airbyte-github-export-seafowl/pages/api/create-fallback-table-after-failed-export.ts
new file mode 100644
index 0000000..9a2340a
--- /dev/null
+++ b/examples/nextjs-import-airbyte-github-export-seafowl/pages/api/create-fallback-table-after-failed-export.ts
@@ -0,0 +1,85 @@
+import type { NextApiRequest, NextApiResponse } from "next";
+import { makeAuthenticatedSeafowlHTTPContext } from "../../lib/backend/seafowl-db";
+import type {
+ CreateFallbackTableForFailedExportRequestShape,
+ CreateFallbackTableForFailedExportResponseData,
+} from "../../types";
+
+/**
+ curl -i \
+ -H "Content-Type: application/json" http://localhost:3000/api/create-fallback-table-after-failed-export \
+ -d '{ "taskId": "2923fd6f-2197-495a-9df1-2428a9ca8dee" }'
+ */
+export default async function handler(
+ req: NextApiRequest,
+ res: NextApiResponse
+) {
+ const { fallbackCreateTableQuery, destinationSchema, destinationTable } =
+ req.body as CreateFallbackTableForFailedExportRequestShape;
+
+ const errors = [];
+
+ if (!fallbackCreateTableQuery) {
+ errors.push("missing fallbackCreateTableQuery in request body");
+ }
+
+ if (!destinationSchema) {
+ errors.push("missing destinationSchema in request body");
+ }
+
+ if (!destinationTable) {
+ errors.push("missing destinationTable in request body");
+ }
+
+ if (typeof fallbackCreateTableQuery !== "string") {
+ errors.push("invalid fallbackCreateTableQuery in request body");
+ }
+
+ if (typeof destinationSchema !== "string") {
+ errors.push("invalid destinationSchema in request body");
+ }
+
+ if (!fallbackCreateTableQuery.includes(destinationSchema)) {
+ errors.push("fallbackCreateTableQuery must include destinationSchema");
+ }
+
+ if (typeof destinationTable !== "string") {
+ errors.push("invalid destinationTable in request body");
+ }
+
+ if (!fallbackCreateTableQuery.includes(destinationTable)) {
+ errors.push("fallbackCreateTableQuery must include destinationTable");
+ }
+
+ if (errors.length > 0) {
+ res.status(400).json({ error: errors.join(", "), success: false });
+ return;
+ }
+
+ try {
+ await createFallbackTable(
+ req.body as CreateFallbackTableForFailedExportRequestShape
+ );
+ res.status(200).json({ success: true });
+ return;
+ } catch (err) {
+ console.trace(err);
+ res.status(400).json({ error: err.message, success: false });
+ }
+}
+
+const createFallbackTable = async ({
+ fallbackCreateTableQuery,
+ destinationTable,
+ destinationSchema,
+}: CreateFallbackTableForFailedExportRequestShape) => {
+ const { client } = makeAuthenticatedSeafowlHTTPContext();
+
+ // NOTE: client.execute should never throw (on error it returns an object including .error)
+ // i.e. Even if the table doesn't exist, or if the schema already existed, we don't need to try/catch
+ await client.execute(
+ `DROP TABLE IF EXISTS "${destinationSchema}"."${destinationTable}";`
+ );
+ await client.execute(`CREATE SCHEMA "${destinationSchema}";`);
+ await client.execute(fallbackCreateTableQuery);
+};
diff --git a/examples/nextjs-import-airbyte-github-export-seafowl/types.ts b/examples/nextjs-import-airbyte-github-export-seafowl/types.ts
index f377047..6ce9024 100644
--- a/examples/nextjs-import-airbyte-github-export-seafowl/types.ts
+++ b/examples/nextjs-import-airbyte-github-export-seafowl/types.ts
@@ -20,6 +20,7 @@ export type ExportQueryInput = {
sourceQuery: string;
destinationSchema: string;
destinationTable: string;
+ fallbackCreateTableQuery?: string;
};
export type StartExportToSeafowlRequestShape =
@@ -44,3 +45,24 @@ export type StartExportToSeafowlResponseData =
}[];
}
| { error: string };
+
+export type CreateFallbackTableForFailedExportRequestShape = {
+ /**
+ * The query to execute to create the fallback table. Note that it should
+ * already include `destinationSchema` and `destinationTable` in the query,
+ * but those still need to be passed separately to the endpoint so that it
+ * can `CREATE SCHEMA` and `DROP TABLE` prior to executing the `CREATE TABLE` query.
+ */
+ fallbackCreateTableQuery: string;
+ destinationSchema: string;
+ destinationTable: string;
+};
+
+export type CreateFallbackTableForFailedExportResponseData =
+ | {
+ error: string;
+ success: false;
+ }
+ | {
+ success: true;
+ };
From c260e109bcbccb7843913a87249b6c7b76e3245f Mon Sep 17 00:00:00 2001
From: Miles Richardson
Date: Fri, 23 Jun 2023 04:17:19 +0100
Subject: [PATCH 29/36] When embedding Seafowl query of exported queries,
select from the destinationTable
The point of exporting a query from Splitgraph to Seafowl is that once
the result is in Seafowl, we can just select from the destinationTable
and forget about the original query (which might not even be compatible
with Seafowl). So make sure that when we're embedding an exported query,
we only render the query in the embedded Splitgraph query editor, and
for the embedded Seafowl Console, we render a query that simply selects
from the destinationTable.
---
.../EmbeddedQuery/EmbeddedQuery.tsx | 22 +++++++++++++++++--
.../ImportExportStepper/ExportPanel.tsx | 6 +++++
2 files changed, 26 insertions(+), 2 deletions(-)
diff --git a/examples/nextjs-import-airbyte-github-export-seafowl/components/EmbeddedQuery/EmbeddedQuery.tsx b/examples/nextjs-import-airbyte-github-export-seafowl/components/EmbeddedQuery/EmbeddedQuery.tsx
index ccc9053..1d47792 100644
--- a/examples/nextjs-import-airbyte-github-export-seafowl/components/EmbeddedQuery/EmbeddedQuery.tsx
+++ b/examples/nextjs-import-airbyte-github-export-seafowl/components/EmbeddedQuery/EmbeddedQuery.tsx
@@ -24,6 +24,7 @@ export const ExportEmbedPreviewTableOrQuery = <
importedRepository,
exportInput,
makeQuery,
+ makeSeafowlQuery,
makeMatchInputToExported,
}: {
exportInput: ExportInputShape;
@@ -33,6 +34,12 @@ export const ExportEmbedPreviewTableOrQuery = <
splitgraphRepository: string;
}
) => string;
+ makeSeafowlQuery?: (
+ tableOrQueryInput: ExportInputShape & {
+ splitgraphNamespace: string;
+ splitgraphRepository: string;
+ }
+ ) => string;
makeMatchInputToExported: (
tableOrQueryInput: ExportInputShape
) => (exported: ExportTable) => boolean;
@@ -79,7 +86,10 @@ export const ExportEmbedPreviewTableOrQuery = <
return {
anchor: "Open in Console",
href: makeSeafowlQueryHref(
- makeQuery({ ...exportInput, ...importedRepository })
+ (makeSeafowlQuery ?? makeQuery)({
+ ...exportInput,
+ ...importedRepository,
+ })
),
};
}
@@ -158,7 +168,15 @@ export const ExportEmbedPreviewTableOrQuery = <
display: selectedTab === "splitgraph" ? "none" : "block",
}}
>
-
+
+ makeSeafowlQuery({ ...exportInput, ...importedRepository })
+ : embedProps.makeQuery
+ }
+ />
)}
diff --git a/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/ExportPanel.tsx b/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/ExportPanel.tsx
index 264609b..716cbc2 100644
--- a/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/ExportPanel.tsx
+++ b/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/ExportPanel.tsx
@@ -323,7 +323,13 @@ const ExportPreview = ({
key={`export-query-preview-${exportQuery.destinationTable}-${exportQuery.destinationSchema}`}
exportInput={exportQuery}
importedRepository={{ splitgraphNamespace, splitgraphRepository }}
+ // This is the query we run on Splitgraph that we exported to Seafowl
makeQuery={({ sourceQuery }) => sourceQuery}
+ // But once it's exported, we can just select from its table in Seafowl (and
+ // besides, the sourceQuery might not be compatible with Seafowl anyway)
+ makeSeafowlQuery={({ destinationSchema, destinationTable }) =>
+ `SELECT * FROM "${destinationSchema}"."${destinationTable}";`
+ }
makeMatchInputToExported={(exportQueryInput) =>
(exportTable: ExportTable) => {
return (
From 4cc3e4142dba7b48f54446145f4658d675a763bf Mon Sep 17 00:00:00 2001
From: Miles Richardson
Date: Wed, 28 Jun 2023 17:18:55 +0100
Subject: [PATCH 30/36] Move embedded preview components to be shared with
export panel and repo page
---
.../EmbeddedQuery/EmbeddedPreviews.module.css | 13 ++
.../EmbeddedQuery/EmbeddedPreviews.tsx | 153 +++++++++++++++
.../EmbeddedQuery/EmbeddedQuery.tsx | 21 +-
.../ExportPanel.module.css | 25 ---
.../ImportExportStepper/ExportPanel.tsx | 183 ++++--------------
.../ImportExportStepper/export-hooks.tsx | 6 +-
.../ImportExportStepper/stepper-states.ts | 38 +---
.../lib/config/github-tables.ts | 33 +++-
.../lib/config/queries-to-export.ts | 21 ++
.../lib/util.ts | 36 ++++
.../types.ts | 8 +
11 files changed, 318 insertions(+), 219 deletions(-)
create mode 100644 examples/nextjs-import-airbyte-github-export-seafowl/components/EmbeddedQuery/EmbeddedPreviews.module.css
create mode 100644 examples/nextjs-import-airbyte-github-export-seafowl/components/EmbeddedQuery/EmbeddedPreviews.tsx
create mode 100644 examples/nextjs-import-airbyte-github-export-seafowl/lib/util.ts
diff --git a/examples/nextjs-import-airbyte-github-export-seafowl/components/EmbeddedQuery/EmbeddedPreviews.module.css b/examples/nextjs-import-airbyte-github-export-seafowl/components/EmbeddedQuery/EmbeddedPreviews.module.css
new file mode 100644
index 0000000..47c5be5
--- /dev/null
+++ b/examples/nextjs-import-airbyte-github-export-seafowl/components/EmbeddedQuery/EmbeddedPreviews.module.css
@@ -0,0 +1,13 @@
+.embeddedPreviewHeading {
+ margin-bottom: 0;
+}
+
+.embeddedPreviewDescription {
+ margin-bottom: 1rem;
+}
+
+.note {
+ font-size: small;
+ /* color: red !important; */
+ display: block;
+}
diff --git a/examples/nextjs-import-airbyte-github-export-seafowl/components/EmbeddedQuery/EmbeddedPreviews.tsx b/examples/nextjs-import-airbyte-github-export-seafowl/components/EmbeddedQuery/EmbeddedPreviews.tsx
new file mode 100644
index 0000000..81600a1
--- /dev/null
+++ b/examples/nextjs-import-airbyte-github-export-seafowl/components/EmbeddedQuery/EmbeddedPreviews.tsx
@@ -0,0 +1,153 @@
+import styles from "./EmbeddedPreviews.module.css";
+import type {
+ ExportTable,
+ ExportQueryInput,
+ ExportTableInput,
+} from "../../types";
+
+import { EmbedPreviewTableOrQuery } from "../EmbeddedQuery/EmbeddedQuery";
+import { ComponentProps } from "react";
+
+export const EmbeddedTablePreviewHeadingAndDescription = ({
+ exportComplete,
+}: {
+ exportComplete: boolean;
+}) => {
+ return (
+ <>
+ {!exportComplete ? (
+ <>
+
Tables to Export
+
+ These are the tables that we'll export from Splitgraph to Seafowl.
+ You can query them in Splitgraph now, and then when the export is
+ complete, you'll be able to query them in Seafowl too.
+
+ >
+ ) : (
+ <>
+
Exported Tables
+
+ We successfully exported the tables to Seafowl, so now you can query
+ them in Seafowl too.
+
+ We've prepared a few queries to export from Splitgraph to Seafowl,
+ so that we can use them to render the charts that we want.
+ Splitgraph will execute the query and insert its result into
+ Seafowl. You can query them in Splitgraph now, and then when the
+ export is complete, you'll be able to query them in Seafowl too.
+
+ >
+ ) : (
+ <>
+
Exported Queries
+
+ We successfully exported these queries from Splitgraph to Seafowl,
+ so now you can query them in Seafowl too.{" "}
+
+ Note: If some queries failed to export, it's probably because they
+ had empty result sets (e.g. the table of issue reactions)
+
+
- These are the tables that we'll export from Splitgraph to Seafowl.
- You can query them in Splitgraph now, and then when the export is
- complete, you'll be able to query them in Seafowl too.
-
- >
- ) : (
<>
-
Exported Tables
-
- We successfully exported the tables to Seafowl, so now you can query
- them in Seafowl too.
-
- We've prepared a few queries to export from Splitgraph to Seafowl,
- so that we can use them to render the charts that we want.
- Splitgraph will execute the query and insert its result into
- Seafowl. You can query them in Splitgraph now, and then when the
- export is complete, you'll be able to query them in Seafowl too.
-
- >
- ) : (
- <>
-
Exported Queries
-
- We successfully exported these queries from Splitgraph to Seafowl,
- so now you can query them in Seafowl too.{" "}
-
- Note: If some queries failed to export, it's probably because they
- had empty result sets (e.g. the table of issue reactions)
-
-
>
)}
-
- {queriesToExport
- .filter((_) => true)
- .map((exportQuery) => (
- sourceQuery}
- // But once it's exported, we can just select from its table in Seafowl (and
- // besides, the sourceQuery might not be compatible with Seafowl anyway)
- makeSeafowlQuery={({ destinationSchema, destinationTable }) =>
- `SELECT * FROM "${destinationSchema}"."${destinationTable}";`
- }
- makeMatchInputToExported={(exportQueryInput) =>
- (exportTable: ExportTable) => {
- return (
- exportTable.destinationSchema ===
- exportQueryInput.destinationSchema &&
- exportTable.destinationTable ===
- exportQueryInput.destinationTable
- );
- }}
- />
- ))}
- >
+
);
};
diff --git a/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/export-hooks.tsx b/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/export-hooks.tsx
index 9e7f7d6..310f9db 100644
--- a/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/export-hooks.tsx
+++ b/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/export-hooks.tsx
@@ -1,5 +1,5 @@
import { useEffect, useMemo } from "react";
-import type { ExportTable } from "./stepper-states";
+import type { ExportTable } from "../../types";
import { useStepper } from "./StepperContext";
/**
@@ -34,8 +34,8 @@ export const useFindMatchingExportTable = (
// and thus we don't have the sets of exportedTablesCompleted, but we know they exist
const exportFullyCompleted = stepperState === "export_complete";
- const completed = matchingCompletedTable ?? (exportFullyCompleted || false);
- const loading = matchingLoadingTable ?? false;
+ const completed = !!matchingCompletedTable ?? (exportFullyCompleted || false);
+ const loading = !!matchingLoadingTable ?? false;
const unstarted = !completed && !loading;
return {
diff --git a/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/stepper-states.ts b/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/stepper-states.ts
index 6185105..fc2ac40 100644
--- a/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/stepper-states.ts
+++ b/examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/stepper-states.ts
@@ -1,15 +1,9 @@
import { useRouter, type NextRouter } from "next/router";
import { ParsedUrlQuery } from "querystring";
import { useEffect, useReducer } from "react";
+import type { ExportTable } from "../../types";
export type GitHubRepository = { namespace: string; repository: string };
-
-export type ExportTable = {
- destinationSchema: string;
- destinationTable: string;
- taskId: string;
- sourceQuery?: string;
- fallbackCreateTableQuery?: string;
-};
+import { getQueryParamAsString, requireKeys } from "../../lib/util";
// NOTE: Multiple tables can have the same taskId, so we track them separately
// in order to not need to redundantly poll the API for each table individually
@@ -74,21 +68,6 @@ const initialState: StepperState = {
debug: null,
};
-const getQueryParamAsString = (
- query: ParsedUrlQuery,
- key: string
-): T | null => {
- if (Array.isArray(query[key]) && query[key].length > 0) {
- throw new Error(`expected only one query param but got multiple: ${key}`);
- }
-
- if (!(key in query)) {
- return null;
- }
-
- return query[key] as T;
-};
-
const queryParamParsers: {
[K in keyof StepperState]: (query: ParsedUrlQuery) => StepperState[K];
} = {
@@ -111,19 +90,6 @@ const queryParamParsers: {
debug: (query) => getQueryParamAsString(query, "debug"),
};
-const requireKeys = >(
- obj: T,
- requiredKeys: (keyof T)[]
-) => {
- const missingKeys = requiredKeys.filter(
- (requiredKey) => !(requiredKey in obj)
- );
-
- if (missingKeys.length > 0) {
- throw new Error("missing required keys: " + missingKeys.join(", "));
- }
-};
-
const stepperStateValidators: {
[K in StepperState["stepperState"]]: (stateFromQuery: StepperState) => void;
} = {
diff --git a/examples/nextjs-import-airbyte-github-export-seafowl/lib/config/github-tables.ts b/examples/nextjs-import-airbyte-github-export-seafowl/lib/config/github-tables.ts
index 92e3903..76813cb 100644
--- a/examples/nextjs-import-airbyte-github-export-seafowl/lib/config/github-tables.ts
+++ b/examples/nextjs-import-airbyte-github-export-seafowl/lib/config/github-tables.ts
@@ -1,3 +1,6 @@
+import { useMemo } from "react";
+import type { ExportTableInput } from "../../types";
+
/**
* List of GitHub table names that we want to import with the Airbyte connector
* into Splitgraph. By default, there are 163 tables available. But we only want
@@ -7,7 +10,8 @@
* Note that Airbyte will still import tables that depend on these tables due
* to foreign keys, and will also import airbyte metaata tables.
*/
-export const relevantGitHubTableNamesForImport = `commits
+export const relevantGitHubTableNamesForImport = `stargazers
+commits
comments
pull_requests
pull_request_stats
@@ -15,6 +19,33 @@ issue_reactions`
.split("\n")
.filter((t) => !!t);
+export const splitgraphTablesToExportToSeafowl = [
+ "stargazers",
+ "stargazers_user",
+];
+
+export const useTablesToExport = ({
+ splitgraphNamespace,
+ splitgraphRepository,
+}: {
+ splitgraphNamespace: string;
+ splitgraphRepository: string;
+}) => {
+ return useMemo(
+ () =>
+ splitgraphTablesToExportToSeafowl.map((tableName) => ({
+ namespace: splitgraphNamespace,
+ repository: splitgraphRepository,
+ table: tableName,
+ })),
+ [
+ splitgraphNamespace,
+ splitgraphRepository,
+ splitgraphTablesToExportToSeafowl,
+ ]
+ );
+};
+
/**
* List of "downstream" GitHub table names that will be imported by default by
* the `airbyte-github` connector, given the list of `relevantGitHubTableNamesForImport`,
diff --git a/examples/nextjs-import-airbyte-github-export-seafowl/lib/config/queries-to-export.ts b/examples/nextjs-import-airbyte-github-export-seafowl/lib/config/queries-to-export.ts
index 94ebabe..3b26679 100644
--- a/examples/nextjs-import-airbyte-github-export-seafowl/lib/config/queries-to-export.ts
+++ b/examples/nextjs-import-airbyte-github-export-seafowl/lib/config/queries-to-export.ts
@@ -1,3 +1,6 @@
+import { useMemo } from "react";
+import type { ExportQueryInput } from "../../types";
+
/**
* Return a a list of queries to export from Splitgraph to Seafowl, given the
* source repository (where the GitHub data was imported into), and the destination
@@ -161,6 +164,24 @@ CREATE TABLE "${seafowlDestinationSchema}".monthly_issue_stats (
},
];
+export const useQueriesToExport = ({
+ splitgraphNamespace,
+ splitgraphRepository,
+}: {
+ splitgraphNamespace: string;
+ splitgraphRepository: string;
+}) => {
+ return useMemo(
+ () =>
+ makeQueriesToExport({
+ splitgraphSourceRepository: splitgraphRepository,
+ splitgraphSourceNamespace: splitgraphNamespace,
+ seafowlDestinationSchema: `${splitgraphNamespace}/${splitgraphRepository}`,
+ }),
+ [splitgraphRepository, splitgraphNamespace]
+ );
+};
+
/** A generic demo query that can be used to show off Splitgraph */
export const genericDemoQuery = `WITH t (
c_int16_smallint,
diff --git a/examples/nextjs-import-airbyte-github-export-seafowl/lib/util.ts b/examples/nextjs-import-airbyte-github-export-seafowl/lib/util.ts
new file mode 100644
index 0000000..e908061
--- /dev/null
+++ b/examples/nextjs-import-airbyte-github-export-seafowl/lib/util.ts
@@ -0,0 +1,36 @@
+import { useRouter } from "next/router";
+import type { ParsedUrlQuery } from "querystring";
+
+export const useDebug = () => {
+ const { query } = useRouter();
+
+ return query.debug;
+};
+
+export const getQueryParamAsString = (
+ query: ParsedUrlQuery,
+ key: string
+): T | null => {
+ if (Array.isArray(query[key]) && query[key].length > 0) {
+ throw new Error(`expected only one query param but got multiple: ${key}`);
+ }
+
+ if (!(key in query)) {
+ return null;
+ }
+
+ return query[key] as T;
+};
+
+export const requireKeys = >(
+ obj: T,
+ requiredKeys: (keyof T)[]
+) => {
+ const missingKeys = requiredKeys.filter(
+ (requiredKey) => !(requiredKey in obj)
+ );
+
+ if (missingKeys.length > 0) {
+ throw new Error("missing required keys: " + missingKeys.join(", "));
+ }
+};
diff --git a/examples/nextjs-import-airbyte-github-export-seafowl/types.ts b/examples/nextjs-import-airbyte-github-export-seafowl/types.ts
index 6ce9024..8b962b6 100644
--- a/examples/nextjs-import-airbyte-github-export-seafowl/types.ts
+++ b/examples/nextjs-import-airbyte-github-export-seafowl/types.ts
@@ -10,6 +10,14 @@ export interface TargetSplitgraphRepo {
splitgraphRepository: string;
}
+export type ExportTable = {
+ destinationSchema: string;
+ destinationTable: string;
+ taskId: string;
+ sourceQuery?: string;
+ fallbackCreateTableQuery?: string;
+};
+
export type ExportTableInput = {
namespace: string;
repository: string;
From ab57659109bf3bf57ce48045503f83410db9ece0 Mon Sep 17 00:00:00 2001
From: Miles Richardson
Date: Wed, 28 Jun 2023 17:19:32 +0100
Subject: [PATCH 31/36] Add `reduceRows` method to `useSqlPlot` for case where
mapping isn't enough
---
.../RepositoryAnalytics/useSqlPlot.tsx | 34 ++++++++++++++++---
1 file changed, 29 insertions(+), 5 deletions(-)
diff --git a/examples/nextjs-import-airbyte-github-export-seafowl/components/RepositoryAnalytics/useSqlPlot.tsx b/examples/nextjs-import-airbyte-github-export-seafowl/components/RepositoryAnalytics/useSqlPlot.tsx
index 3a2fc5d..50c7043 100644
--- a/examples/nextjs-import-airbyte-github-export-seafowl/components/RepositoryAnalytics/useSqlPlot.tsx
+++ b/examples/nextjs-import-airbyte-github-export-seafowl/components/RepositoryAnalytics/useSqlPlot.tsx
@@ -15,10 +15,12 @@ import { useMemo } from "react";
export const useSqlPlot = <
RowShape extends UnknownObjectShape,
SqlParams extends object,
- MappedRow extends UnknownObjectShape
+ MappedRow extends UnknownObjectShape,
+ ReducedRow extends UnknownObjectShape
>({
sqlParams,
mapRows,
+ reduceRows,
buildQuery,
makePlotOptions,
isRenderable,
@@ -34,6 +36,11 @@ export const useSqlPlot = <
* to a `Date` object.
*/
mapRows?: (row: RowShape) => MappedRow;
+
+ /**
+ * An optional function to transform the mapped rows into a different aggregation
+ */
+ reduceRows?: (rows: MappedRow[]) => ReducedRow[];
/**
* A builder function that returns a SQL query given a set of parameters, which
* will be the parameters passed as the `sqlParams` parameter.
@@ -43,7 +50,7 @@ export const useSqlPlot = <
* A function to call after receiving the result of the SQL query (and mapping
* its rows if applicable), to create the options given to Observable {@link Plot.plot}
*/
- makePlotOptions: (rows: MappedRow[]) => Plot.PlotOptions;
+ makePlotOptions: (rows: ReducedRow[]) => Plot.PlotOptions;
/**
* A function to call to determine if the chart is renderable. This is helpful
* during server side rendering, when Observable Plot doesn't typically work well,
@@ -73,10 +80,27 @@ export const useSqlPlot = <
);
}, [response, error]);
- const plotOptions = useMemo(() => makePlotOptions(mappedRows), [mappedRows]);
+ const reducedRows = useMemo(() => {
+ if (mappedRows.length === 0) {
+ return [];
+ }
+
+ if (!reduceRows) {
+ return mappedRows as unknown as ReducedRow[];
+ }
+
+ return reduceRows(mappedRows);
+ }, [mappedRows]);
+
+ console.log(JSON.stringify(reducedRows));
+
+ const plotOptions = useMemo(
+ () => makePlotOptions(reducedRows),
+ [reducedRows]
+ );
useEffect(() => {
- if (mappedRows === undefined) {
+ if (reducedRows === undefined) {
return;
}
@@ -90,7 +114,7 @@ export const useSqlPlot = <
}
return () => plot.remove();
- }, [mappedRows]);
+ }, [reducedRows]);
const renderPlot = useCallback(
() => ,
From 15d14e2b311226cfdd4865a65949f0ee20b4c6aa Mon Sep 17 00:00:00 2001
From: Miles Richardson
Date: Wed, 28 Jun 2023 17:22:05 +0100
Subject: [PATCH 32/36] Add stacked bar chart of issue reactions by month with
bars broken down by reaction type
---
.../components/RepositoryAnalytics/Charts.tsx | 3 +
.../charts/MonthlyIssueStats.tsx | 144 ++++++++++++++++++
2 files changed, 147 insertions(+)
create mode 100644 examples/nextjs-import-airbyte-github-export-seafowl/components/RepositoryAnalytics/charts/MonthlyIssueStats.tsx
diff --git a/examples/nextjs-import-airbyte-github-export-seafowl/components/RepositoryAnalytics/Charts.tsx b/examples/nextjs-import-airbyte-github-export-seafowl/components/RepositoryAnalytics/Charts.tsx
index 6eea242..93c8674 100644
--- a/examples/nextjs-import-airbyte-github-export-seafowl/components/RepositoryAnalytics/Charts.tsx
+++ b/examples/nextjs-import-airbyte-github-export-seafowl/components/RepositoryAnalytics/Charts.tsx
@@ -6,6 +6,7 @@ import { SqlProvider, makeSeafowlHTTPContext } from "@madatdata/react";
import { useMemo } from "react";
import { StargazersChart } from "./charts/StargazersChart";
+import { MonthlyIssueStatsTable } from "./charts/MonthlyIssueStats";
export interface ChartsProps {
importedRepository: ImportedRepository;
@@ -29,7 +30,9 @@ export const Charts = ({ importedRepository }: ChartsProps) => {