Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 7 additions & 58 deletions api-doc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -530,15 +530,16 @@ paths:
required: true
schema:
$ref: "#/components/schemas/IdentifierPattern"
- name: fetchTableSchema
- name: tableNames
in: query
description: |
When `false`, returns tables with `resource` set but does not load per-table column metadata (no Malloy `fetchTableSchema`).
Omitted or `null` is treated as `true`. Use `false` for fast explorer listing; call get-table with `fetchTableSchema=true` for details.
List of table names to filter results. When provided, only returns metadata
for the specified tables. When omitted, returns all tables in the schema.
required: false
schema:
type: boolean
default: true
type: array
items:
type: string
responses:
"200":
description: A list of table names available in the specified schema
Expand Down Expand Up @@ -758,6 +759,7 @@ paths:
$ref: "#/components/responses/InternalServerError"
"503":
$ref: "#/components/responses/ServiceUnavailable"

/projects/{projectName}/connections/{connectionName}/sqlTemporaryTable:
post:
tags:
Expand Down Expand Up @@ -855,59 +857,6 @@ paths:
"503":
$ref: "#/components/responses/ServiceUnavailable"

# TODO: Remove this endpoint. This is deprecated and replaced by /projects/{projectName}/connections/{connectionName}/table resource.
/projects/{projectName}/connections/{connectionName}/tableSource:
get:
tags:
- connections
operationId: get-tablesource
summary: Get table source information
deprecated: true
description: |
Retrieves information about a specific table or view from the database connection.
This includes table schema, column definitions, and metadata. The table can be specified
by either tableKey or tablePath parameters, depending on the database type.
parameters:
- name: projectName
in: path
description: Name of the project
required: true
schema:
$ref: "#/components/schemas/IdentifierPattern"
- name: connectionName
in: path
description: Name of the connection
required: true
schema:
$ref: "#/components/schemas/IdentifierPattern"
- name: tableKey
in: query
description: Table key
required: false
schema:
type: string
- name: tablePath
in: query
description: Table path
required: false
schema:
type: string
responses:
"200":
description: Table source information
content:
application/json:
schema:
$ref: "#/components/schemas/TableSource"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalServerError"
"503":
$ref: "#/components/responses/ServiceUnavailable"

# TODO: Remove this endpoint.
/projects/{projectName}/connections/{connectionName}/queryData:
get:
Expand Down
1 change: 0 additions & 1 deletion packages/sdk/src/components/Project/ConnectionExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,6 @@ function TablesInSchema({
projectName,
connectionName,
schemaName,
false,
),
});

Expand Down
116 changes: 56 additions & 60 deletions packages/server/src/controller/connection.controller.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
import { Connection, RunSQLOptions } from "@malloydata/malloy";
import { Connection, RunSQLOptions, TableSourceDef } from "@malloydata/malloy";
import { PersistSQLResults } from "@malloydata/malloy/connection";
import { components } from "../api";
import { BadRequestError, ConnectionError } from "../errors";
import { logger } from "../logger";
import { testConnectionConfig } from "../service/connection";
import { ConnectionService } from "../service/connection_service";
import {
getConnectionTableSource,
getSchemasForConnection,
getTablesForSchema,
listTablesForSchema,
} from "../service/db_utils";
import { ProjectStore } from "../service/project_store";

type ApiConnection = components["schemas"]["Connection"];
type ApiConnectionStatus = components["schemas"]["ConnectionStatus"];
type ApiSqlSource = components["schemas"]["SqlSource"];
type ApiTableSource = components["schemas"]["TableSource"];
type ApiTable = components["schemas"]["Table"];
type ApiQueryData = components["schemas"]["QueryData"];
type ApiTemporaryTable = components["schemas"]["TemporaryTable"];
Expand All @@ -29,28 +27,6 @@ const AZURE_DATA_EXTENSIONS = [
".ndjson",
];

/**
* `fetchTableSchema` query param: default true when omitted, null, or empty.
* Only explicit false/0 disables schema fetch.
*/
export function parseFetchTableSchemaQueryParam(raw: unknown): boolean {
if (raw === undefined || raw === null) {
return true;
}
const v = Array.isArray(raw) ? raw[0] : raw;
if (v === "" || v === undefined) {
return true;
}
if (typeof v === "boolean") {
return v;
}
const s = String(v).trim().toLowerCase();
if (s === "false" || s === "0") {
return false;
}
return true;
}

/**
* Validates an Azure URL against the three supported patterns:
* 1. Single file: path/file.parquet
Expand Down Expand Up @@ -144,6 +120,52 @@ export class ConnectionController {
}
}

/**
* Fetches a table's schema via the Malloy connection's fetchTableSchema,
* returning an ApiTable with columns and the raw source JSON.
*/
private async fetchTable(
malloyConnection: Connection,
tableKey: string,
tablePath: string,
): Promise<ApiTable> {
try {
const source = await (
malloyConnection as Connection & {
fetchTableSchema: (
tableKey: string,
tablePath: string,
) => Promise<TableSourceDef | undefined>;
}
).fetchTableSchema(tableKey, tablePath);
if (!source) {
throw new ConnectionError(`Table ${tablePath} not found`);
}

return {
source: JSON.stringify(source),
resource: tablePath,
columns: (source.fields || []).map((f) => ({
name: f.name,
type: f.type,
})),
};
} catch (error) {
const errorMessage =
error instanceof Error
? error.message
: typeof error === "string"
? error
: JSON.stringify(error);
logger.error("fetchTableSchema error", {
error,
tableKey,
tablePath,
});
throw new ConnectionError(errorMessage);
}
}

public async getConnection(
projectName: string,
connectionName: string,
Expand Down Expand Up @@ -183,7 +205,7 @@ export class ConnectionController {
projectName: string,
connectionName: string,
schemaName: string,
fetchTableSchema = true,
tableNames?: string[],
): Promise<ApiTable[]> {
const project = await this.projectStore.getProject(projectName, false);
const connection = project.getApiConnection(connectionName);
Expand All @@ -192,11 +214,11 @@ export class ConnectionController {
connectionName,
);

return getTablesForSchema(
return listTablesForSchema(
connection,
schemaName,
malloyConnection,
fetchTableSchema,
tableNames,
);
}

Expand Down Expand Up @@ -231,20 +253,6 @@ export class ConnectionController {
}
}

public async getConnectionTableSource(
projectName: string,
connectionName: string,
tableKey: string,
tablePath: string,
): Promise<ApiTableSource> {
const malloyConnection = await this.getMalloyConnection(
projectName,
connectionName,
);

return getConnectionTableSource(malloyConnection, tableKey, tablePath);
}

public async getTable(
projectName: string,
connectionName: string,
Expand All @@ -259,6 +267,8 @@ export class ConnectionController {
const project = await this.projectStore.getProject(projectName, false);
const connection = project.getApiConnection(connectionName);

// TODO: Move this database connection logic to the db_utils.ts file -- and
// ultimately into a connection-specific class.
if (connection.type === "ducklake") {
if (tablePath.split(".").length === 1) {
// tablePath is just the table name, construct full path
Expand Down Expand Up @@ -306,16 +316,12 @@ export class ConnectionController {
);
const fullFileUrl = `${dirPath}${fileName}${queryString}`;

const tableSource = await getConnectionTableSource(
const table = await this.fetchTable(
malloyConnection,
fileName,
fullFileUrl,
);
return {
resource: tablePath,
columns: tableSource.columns,
source: tableSource.source,
};
return { ...table, resource: tablePath };
}
}
}
Expand All @@ -325,17 +331,7 @@ export class ConnectionController {
throw new Error(`Invalid tablePath: ${tablePath}`);
}

const tableSource = await getConnectionTableSource(
malloyConnection,
tableKey, // tableKey is the table name
tablePath,
);

return {
resource: tablePath,
columns: tableSource.columns,
source: tableSource.source,
};
return this.fetchTable(malloyConnection, tableKey, tablePath);
}

public async getConnectionQueryData(
Expand Down
35 changes: 10 additions & 25 deletions packages/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@ import { createProxyMiddleware } from "http-proxy-middleware";
import { AddressInfo } from "net";
import * as path from "path";
import { CompileController } from "./controller/compile.controller";
import {
ConnectionController,
parseFetchTableSchemaQueryParam,
} from "./controller/connection.controller";
import { ConnectionController } from "./controller/connection.controller";
import { DatabaseController } from "./controller/database.controller";
import { ModelController } from "./controller/model.controller";
import { PackageController } from "./controller/package.controller";
Expand All @@ -33,6 +30,14 @@ import { logger, loggerMiddleware } from "./logger";

import { initializeMcpServer } from "./mcp/server";
import { ProjectStore } from "./service/project_store";

/** Normalize an Express query param into a string[] or undefined. */
export function normalizeQueryArray(value: unknown): string[] | undefined {
if (value === undefined || value === null) return undefined;
if (Array.isArray(value)) return value.map(String);
return [String(value)];
}

// Parse command line arguments
function parseArgs() {
const args = process.argv.slice(2);
Expand Down Expand Up @@ -470,7 +475,7 @@ app.get(
req.params.projectName,
req.params.connectionName,
req.params.schemaName,
parseFetchTableSchemaQueryParam(req.query.fetchTableSchema),
normalizeQueryArray(req.query.tableNames),
);
res.status(200).json(results);
} catch (error) {
Expand Down Expand Up @@ -542,26 +547,6 @@ app.post(
},
);

app.get(
`${API_PREFIX}/projects/:projectName/connections/:connectionName/tableSource`,
async (req, res) => {
try {
res.status(200).json(
await connectionController.getConnectionTableSource(
req.params.projectName,
req.params.connectionName,
req.query.tableKey as string,
req.query.tablePath as string,
),
);
} catch (error) {
logger.error(error);
const { json, status } = internalErrorToHttpError(error as Error);
res.status(status).json(json);
}
},
);

/**
* @deprecated Use /projects/:projectName/connections/:connectionName/queryData POST method instead
*/
Expand Down
Loading
Loading