Merge pull request #42 from agnosticeng/remote-engine
feat: use remote ch
This commit is contained in:
@@ -1,12 +0,0 @@
|
||||
import { exec } from './query';
|
||||
|
||||
export async function init() {
|
||||
await exec(`
|
||||
CREATE DATABASE IF NOT EXISTS agx;
|
||||
USE agx;
|
||||
`);
|
||||
}
|
||||
|
||||
export { Sources } from './sources.svelte';
|
||||
export type * from './types';
|
||||
export { exec };
|
||||
@@ -1,13 +0,0 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import type { CHResponse } from './types';
|
||||
|
||||
export async function exec(query: string) {
|
||||
try {
|
||||
const r: string = await invoke('query', { query });
|
||||
if (!r) return;
|
||||
|
||||
return JSON.parse(r) as CHResponse;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { exec } from './query';
|
||||
import type { Source } from './types';
|
||||
|
||||
const LIST_SOURCES_QUERY = `select
|
||||
t.name as name,
|
||||
t.engine as engine,
|
||||
groupArray(map(
|
||||
'name', c.name,
|
||||
'type', c.type
|
||||
)) as columns
|
||||
from system.tables as t
|
||||
inner join system.columns as c on t.name = c.table
|
||||
where database = 'agx'
|
||||
group by t.name, t.engine
|
||||
`;
|
||||
|
||||
export class Sources {
|
||||
#tables = $state.raw<Source[]>([]);
|
||||
|
||||
constructor() {
|
||||
this.fetch();
|
||||
}
|
||||
|
||||
public get tables() {
|
||||
return this.#tables;
|
||||
}
|
||||
|
||||
async fetch() {
|
||||
const response = await exec(LIST_SOURCES_QUERY);
|
||||
if (response) this.#tables = response.data as Source[];
|
||||
}
|
||||
}
|
||||
15
src/lib/ch-engine/types.d.ts
vendored
15
src/lib/ch-engine/types.d.ts
vendored
@@ -1,15 +0,0 @@
|
||||
export type CHResponse = {
|
||||
meta: Array<ColumnDescriptor>;
|
||||
data: Array<{ [key: string]: any }>;
|
||||
};
|
||||
|
||||
export interface ColumnDescriptor {
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface Source {
|
||||
name: string;
|
||||
engine: string;
|
||||
columns: ColumnDescriptor[];
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import type { Table } from '$lib/olap-engine';
|
||||
|
||||
import SearchBar from '$lib/components/SearchBar.svelte';
|
||||
import { get_app_context } from '$lib/context';
|
||||
import Database from '$lib/icons/Database.svelte';
|
||||
import Table from '$lib/icons/Table.svelte';
|
||||
import TableC from '$lib/icons/Table.svelte';
|
||||
import {
|
||||
filter,
|
||||
remove_nullable,
|
||||
@@ -10,12 +11,16 @@
|
||||
SOURCE_TYPE_SHORT_NAME_MAP
|
||||
} from './utils';
|
||||
|
||||
const { sources } = get_app_context();
|
||||
type Props = {
|
||||
tables?: Table[];
|
||||
};
|
||||
|
||||
let { tables = [] }: Props = $props();
|
||||
|
||||
let loading = $state(false);
|
||||
|
||||
let search = $state<string>('');
|
||||
const filtered = $derived(filter(sources.tables, search));
|
||||
const filtered = $derived(filter(tables, search));
|
||||
</script>
|
||||
|
||||
<SearchBar bind:value={search} />
|
||||
@@ -23,9 +28,7 @@
|
||||
<button
|
||||
disabled={loading}
|
||||
onclick={() => {
|
||||
if (loading) return;
|
||||
loading = true;
|
||||
sources.fetch().finally(() => (loading = false));
|
||||
// @todo: reload
|
||||
}}>Refresh</button
|
||||
>
|
||||
</div>
|
||||
@@ -36,7 +39,7 @@
|
||||
{#if source.engine === 'MergeTree'}
|
||||
<Database size="15" />
|
||||
{:else}
|
||||
<Table size="15" />
|
||||
<TableC size="15" />
|
||||
{/if}
|
||||
<h3>{source.name}</h3>
|
||||
<span class="Tag" style:background-color={SOURCE_TYPE_COLOR_MAP[source.engine]}>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type Source } from '$lib/ch-engine';
|
||||
import { type Table } from '$lib/olap-engine';
|
||||
|
||||
export function filter(sources: Source[], search: string) {
|
||||
export function filter(sources: Table[], search: string) {
|
||||
if (!search) return sources;
|
||||
const search_ = search.toLowerCase();
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { ColumnDescriptor } from '$lib/ch-engine';
|
||||
import type { Table } from '$lib/ch-engine';
|
||||
import { sql } from '@codemirror/lang-sql';
|
||||
import { Compartment, EditorState } from '@codemirror/state';
|
||||
import { EditorView, keymap, placeholder } from '@codemirror/view';
|
||||
@@ -7,20 +7,20 @@
|
||||
import './codemirror.css';
|
||||
import { default_extensions, default_keymaps } from './extensions';
|
||||
import { ProxyDialect } from './SQLDialect';
|
||||
import { schema_to_completions } from './utils';
|
||||
import { schema_to_completions, sources_to_schema } from './utils';
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
onExec?: () => unknown;
|
||||
schema?: { [table_name: string]: ColumnDescriptor[] };
|
||||
tables?: Table[];
|
||||
};
|
||||
|
||||
let { value = $bindable(''), onExec, schema = {} }: Props = $props();
|
||||
let { value = $bindable(''), onExec, tables = [] }: Props = $props();
|
||||
|
||||
let container: HTMLDivElement;
|
||||
let editor_view: EditorView;
|
||||
const dialect_compartment = new Compartment();
|
||||
const sql_schema = $derived(schema_to_completions(schema));
|
||||
const sql_schema = $derived(schema_to_completions(sources_to_schema(tables)));
|
||||
|
||||
$effect(() => {
|
||||
editor_view = new EditorView({ parent: container });
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { ColumnDescriptor, Source } from '$lib/ch-engine';
|
||||
import type { ColumnDescriptor, Table } from '$lib/ch-engine';
|
||||
import type { Completion } from '@codemirror/autocomplete';
|
||||
|
||||
export function sources_to_schema(sources: Source[]): {
|
||||
[table_name: string]: ColumnDescriptor[];
|
||||
} {
|
||||
export type Schema = { [table_name: string]: ColumnDescriptor[] };
|
||||
|
||||
export function sources_to_schema(sources: Table[]): Schema {
|
||||
return sources.reduce((acc, k) => {
|
||||
if (k.columns.length) return { ...acc, [k.name]: k.columns };
|
||||
return acc;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { Table } from '$lib/ch-engine';
|
||||
import Datasets from './Datasets/Datasets.svelte';
|
||||
|
||||
type Tab = 'sources' | 'queries' | 'history';
|
||||
@@ -7,6 +8,12 @@
|
||||
function switch_to(next_tab: Tab) {
|
||||
tab = next_tab;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
tables?: Table[];
|
||||
};
|
||||
|
||||
let { tables = [] }: Props = $props();
|
||||
</script>
|
||||
|
||||
<section>
|
||||
@@ -16,7 +23,7 @@
|
||||
<button aria-current={tab === 'history'} onclick={() => switch_to('history')}>History</button>
|
||||
</nav>
|
||||
{#if tab === 'sources'}
|
||||
<Datasets />
|
||||
<Datasets {tables} />
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
|
||||
29
src/lib/olap-engine/engine-chdb.ts
Normal file
29
src/lib/olap-engine/engine-chdb.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import type { OLAPEngine, Table, OLAPResponse } from './index';
|
||||
|
||||
import CLICKHOUSE_GET_SCHEMA from './queries/clickhouse_get_schema.sql?raw';
|
||||
import CLICKHOUSE_INIT_DB from './queries/clickhouse_init_db.sql?raw';
|
||||
|
||||
export class CHDBEngine implements OLAPEngine {
|
||||
async init() {
|
||||
await this.exec(CLICKHOUSE_INIT_DB);
|
||||
}
|
||||
|
||||
async exec(query: string) {
|
||||
try {
|
||||
const r: string = await invoke('query', { query });
|
||||
if (!r) return;
|
||||
|
||||
return JSON.parse(r) as OLAPResponse;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async getSchema() {
|
||||
const response = await this.exec(CLICKHOUSE_GET_SCHEMA);
|
||||
if (!response) return [];
|
||||
return response.data as Table[];
|
||||
}
|
||||
}
|
||||
32
src/lib/olap-engine/engine-remote.ts
Normal file
32
src/lib/olap-engine/engine-remote.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { OLAPEngine, Table, OLAPResponse } from './index';
|
||||
|
||||
import CLICKHOUSE_GET_SCHEMA from './queries/clickhouse_get_schema.sql?raw';
|
||||
|
||||
export class RemoteEngine implements OLAPEngine {
|
||||
async init() {}
|
||||
|
||||
async exec(query: string) {
|
||||
try {
|
||||
const response = await fetch('https://proxy.agx.app/query', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: query
|
||||
});
|
||||
const r = await response.text();
|
||||
if (!r) return;
|
||||
|
||||
return JSON.parse(r) as OLAPResponse;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async getSchema() {
|
||||
const response = await this.exec(CLICKHOUSE_GET_SCHEMA);
|
||||
if (!response) return [];
|
||||
return response.data as Table[];
|
||||
}
|
||||
}
|
||||
26
src/lib/olap-engine/index.ts
Normal file
26
src/lib/olap-engine/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { CHDBEngine } from './engine-chdb';
|
||||
import { RemoteEngine } from './engine-remote';
|
||||
|
||||
export type OLAPResponse = {
|
||||
meta: Array<ColumnDescriptor>;
|
||||
data: Array<{ [key: string]: any }>;
|
||||
};
|
||||
|
||||
export interface ColumnDescriptor {
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface Table {
|
||||
name: string;
|
||||
engine: string;
|
||||
columns: ColumnDescriptor[];
|
||||
}
|
||||
|
||||
export interface OLAPEngine {
|
||||
init(): Promise<void>;
|
||||
exec(query: string): Promise<OLAPResponse | undefined>;
|
||||
getSchema(): Promise<Table[]>;
|
||||
}
|
||||
|
||||
export const engine = new RemoteEngine() as OLAPEngine;
|
||||
11
src/lib/olap-engine/queries/clickhouse_get_schema.sql
Normal file
11
src/lib/olap-engine/queries/clickhouse_get_schema.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
SELECT
|
||||
t.name AS name,
|
||||
t.engine AS engine,
|
||||
groupArray(map(
|
||||
'name', c.name,
|
||||
'type', c.type
|
||||
)) AS columns
|
||||
FROM system.tables AS t
|
||||
INNER JOIN system.columns AS c ON t.name = c.table
|
||||
WHERE database = currentDatabase()
|
||||
GROUP BY t.name, t.engine
|
||||
2
src/lib/olap-engine/queries/clickhouse_init_db.sql
Normal file
2
src/lib/olap-engine/queries/clickhouse_init_db.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
CREATE DATABASE IF NOT EXISTS agx;
|
||||
USE agx;
|
||||
4
src/lib/types.d.ts
vendored
4
src/lib/types.d.ts
vendored
@@ -1,7 +1,5 @@
|
||||
import type { Sources } from './ch-engine';
|
||||
|
||||
type MaybePromise<T> = T | Promise<T>;
|
||||
|
||||
export type AppContext = {
|
||||
sources: Sources;
|
||||
tables: Table[];
|
||||
};
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { exec, Sources, type CHResponse } from '$lib/ch-engine';
|
||||
import { Editor, sources_to_schema } from '$lib/components/Editor';
|
||||
import { engine, type OLAPResponse } from '$lib/olap-engine';
|
||||
import type { Table } from '$lib/olap-engine';
|
||||
|
||||
import { Editor } from '$lib/components/Editor';
|
||||
import Result from '$lib/components/Result.svelte';
|
||||
import SideBar from '$lib/components/SideBar.svelte';
|
||||
import { SplitPane } from '$lib/components/SplitPane';
|
||||
import WindowTitleBar from '$lib/components/WindowTitleBar.svelte';
|
||||
import { set_app_context } from '$lib/context';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let response = $state.raw<CHResponse>();
|
||||
let response = $state.raw<OLAPResponse>();
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
@@ -18,12 +19,16 @@
|
||||
async function handleExec() {
|
||||
if (loading) return;
|
||||
loading = true;
|
||||
response = await exec(query).finally(() => (loading = false));
|
||||
response = await engine.exec(query).finally(() => (loading = false));
|
||||
}
|
||||
|
||||
const sources = new Sources();
|
||||
let tables = $state<Table[]>([]);
|
||||
|
||||
set_app_context({ sources });
|
||||
$effect(() => {
|
||||
engine.getSchema().then((t) => {
|
||||
tables = t;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<WindowTitleBar>
|
||||
@@ -35,16 +40,12 @@
|
||||
<section class="screen">
|
||||
<SplitPane orientation="horizontal" position="242px" min="242px" max="40%">
|
||||
{#snippet a()}
|
||||
<SideBar />
|
||||
<SideBar {tables} />
|
||||
{/snippet}
|
||||
{#snippet b()}
|
||||
<SplitPane orientation="vertical" min="20%" max="80%" --color="hsl(0deg 0% 12%)">
|
||||
{#snippet a()}
|
||||
<Editor
|
||||
bind:value={query}
|
||||
onExec={handleExec}
|
||||
schema={sources_to_schema(sources.tables)}
|
||||
/>
|
||||
<Editor bind:value={query} onExec={handleExec} {tables} />
|
||||
{/snippet}
|
||||
{#snippet b()}
|
||||
<Result {response} />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { init } from '$lib/ch-engine';
|
||||
import { engine } from '$lib/olap-engine';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async () => {
|
||||
await init();
|
||||
await engine.init();
|
||||
|
||||
return {};
|
||||
}) satisfies PageLoad;
|
||||
|
||||
Reference in New Issue
Block a user