diff --git a/graphql-server/schema.gql b/graphql-server/schema.gql index 69edff00..6aa15c51 100644 --- a/graphql-server/schema.gql +++ b/graphql-server/schema.gql @@ -39,13 +39,13 @@ type Index { } type Table { - _id: ID! - databaseName: String! - refName: String! tableName: String! columns: [Column!]! foreignKeys: [ForeignKey!]! indexes: [Index!]! + _id: ID! + databaseName: String! + refName: String! } type TableNames { diff --git a/graphql-server/src/branches/branch.model.ts b/graphql-server/src/branches/branch.model.ts index b79b7bee..01701da2 100644 --- a/graphql-server/src/branches/branch.model.ts +++ b/graphql-server/src/branches/branch.model.ts @@ -45,7 +45,7 @@ export function fromDoltBranchesRow( _id: `databases/${databaseName}/branches/${b.name}`, databaseName, branchName: b.name, - head: b.head, + head: b.hash, lastUpdated: convertToUTCDate(b.latest_commit_date), lastCommitter: b.latest_committer, tableNames: tns, diff --git a/graphql-server/src/branches/branch.resolver.ts b/graphql-server/src/branches/branch.resolver.ts index fe8846d1..3e67ec55 100644 --- a/graphql-server/src/branches/branch.resolver.ts +++ b/graphql-server/src/branches/branch.resolver.ts @@ -59,7 +59,7 @@ export class BranchResolver { async branch(@Args() args: BranchArgs): Promise { const conn = this.conn.connection(); const res = await conn.getBranch(args); - if (!res.length) return undefined; + if (!res?.length) return undefined; return fromDoltBranchesRow(args.databaseName, res[0]); } diff --git a/graphql-server/src/connections/connection.resolver.ts b/graphql-server/src/connections/connection.resolver.ts index 6f32541e..24b79b7f 100644 --- a/graphql-server/src/connections/connection.resolver.ts +++ b/graphql-server/src/connections/connection.resolver.ts @@ -1,9 +1,9 @@ import { Resolver } from "@nestjs/graphql"; import * as mysql from "mysql2/promise"; import { DataSource } from "typeorm"; +import { QueryFactory } from "../queryFactory"; import { DoltQueryFactory } from "../queryFactory/dolt"; import { MySQLQueryFactory } from "../queryFactory/mysql"; -import { QueryFactory } from "../queryFactory/types"; export class WorkbenchConfig { hideDoltFeatures: boolean; diff --git a/graphql-server/src/diffSummaries/diffSummary.resolver.ts b/graphql-server/src/diffSummaries/diffSummary.resolver.ts index 1142588b..3ec2979f 100644 --- a/graphql-server/src/diffSummaries/diffSummary.resolver.ts +++ b/graphql-server/src/diffSummaries/diffSummary.resolver.ts @@ -1,7 +1,7 @@ import { Args, ArgsType, Field, Query, Resolver } from "@nestjs/graphql"; import { ConnectionResolver } from "../connections/connection.resolver"; import { checkArgs } from "../diffStats/diffStat.resolver"; -import { QueryFactory } from "../queryFactory/types"; +import { QueryFactory } from "../queryFactory"; import { DBArgs } from "../utils/commonTypes"; import { CommitDiffType } from "./diffSummary.enums"; import { DiffSummary, fromDoltDiffSummary } from "./diffSummary.model"; diff --git a/graphql-server/src/queryFactory/dolt/index.ts b/graphql-server/src/queryFactory/dolt/index.ts index bc72e312..ed30da4d 100644 --- a/graphql-server/src/queryFactory/dolt/index.ts +++ b/graphql-server/src/queryFactory/dolt/index.ts @@ -1,65 +1,152 @@ +import { QueryFactory } from ".."; import { SortBranchesBy } from "../../branches/branch.enum"; +import * as column from "../../columns/column.model"; import { CommitDiffType } from "../../diffSummaries/diffSummary.enums"; +import * as foreignKey from "../../indexes/foreignKey.model"; +import * as index from "../../indexes/index.model"; import { convertToStringForQuery } from "../../rowDiffs/rowDiff.enums"; import { SchemaType } from "../../schemas/schema.enums"; -import { systemTableValues } from "../../systemTables/systemTable.enums"; +import { + DoltSystemTable, + systemTableValues, +} from "../../systemTables/systemTable.enums"; +import { TableDetails } from "../../tables/table.model"; import { ROW_LIMIT, handleTableNotFound } from "../../utils"; import { MySQLQueryFactory } from "../mysql"; import * as myqh from "../mysql/queries"; +import { mapTablesRes } from "../mysql/utils"; import * as t from "../types"; import * as qh from "./queries"; import { handleRefNotFound, unionCols } from "./utils"; export class DoltQueryFactory extends MySQLQueryFactory - implements t.QueryFactory + implements QueryFactory { isDolt = true; - async getTableNames(args: t.RefArgs, filterSystemTables?: boolean): t.PR { + async getTableNames( + args: t.RefArgs, + filterSystemTables?: boolean, + ): Promise { return this.queryMultiple( async query => { - const tables = await query(myqh.listTablesQuery, []); + const res: t.RawRows = await query(myqh.listTablesQuery, []); + const tables = mapTablesRes(res); if (filterSystemTables) return tables; - const systemTables: Array = await Promise.all( + const systemTables: Array = await Promise.all( systemTableValues.map(async st => { const cols = await handleTableNotFound(async () => - query(myqh.columnsQuery, [st]), + query(qh.columnsQuery, [st]), ); if (cols) { - return { [`Tables_in_${args.databaseName}`]: `${st}` }; + return `${st}`; } return undefined; }), ); - return [...tables, ...(systemTables.filter(st => !!st) as t.RawRows)]; + return [...tables, ...(systemTables.filter(st => !!st) as string[])]; }, args.databaseName, args.refName, ); } + // TODO: Why does qr.getTable() not work for foreign keys for Dolt? + async getTableInfo(args: t.TableArgs): Promise { + return this.queryMultiple( + async query => getTableInfoWithQR(query, args), + args.databaseName, + args.refName, + ); + } + + async getTables(args: t.RefArgs, tns: string[]): Promise { + return this.queryMultiple( + async query => { + const tableInfos = await Promise.all( + tns.map(async name => { + const tableInfo = await getTableInfoWithQR(query, { + ...args, + tableName: name, + }); + return tableInfo; + }), + ); + return tableInfos; + }, + args.databaseName, + args.refName, + ); + } + + async getTablePKColumns(args: t.TableArgs): Promise { + const res: t.RawRows = await this.query( + qh.tableColsQuery, + [args.tableName], + args.databaseName, + args.refName, + ); + return res.filter(c => c.Key === "PRI").map(c => c.Field); + } + async getSchemas(args: t.RefArgs, type?: SchemaType): t.UPR { - const q = qh.getDoltSchemasQuery(!!type); - const p = type ? [type] : []; - return handleTableNotFound(async () => - this.query(q, p, args.databaseName, args.refName), + return this.queryForBuilder( + async em => { + let sel = em + .createQueryBuilder() + .select("*") + .from(DoltSystemTable.SCHEMAS, ""); + if (type) { + sel = sel.where(`${DoltSystemTable.SCHEMAS}.type = :type`, { + type, + }); + } + return handleTableNotFound(async () => sel.getRawMany()); + }, + args.databaseName, + args.refName, ); } async getProcedures(args: t.RefArgs): t.UPR { - return handleTableNotFound(async () => - this.query(qh.doltProceduresQuery, [], args.databaseName, args.refName), + return this.queryForBuilder( + async em => { + const sel = em + .createQueryBuilder() + .select("*") + .from(DoltSystemTable.PROCEDURES, ""); + return handleTableNotFound(async () => sel.getRawMany()); + }, + args.databaseName, + args.refName, ); } - async getBranch(args: t.BranchArgs): t.PR { - return this.query(qh.branchQuery, [args.branchName], args.databaseName); + async getBranch(args: t.BranchArgs): t.UPR { + return this.queryForBuilder( + async em => + em + .createQueryBuilder() + .select("*") + .from("dolt_branches", "") + .where(`dolt_branches.name = :name`, { + name: args.branchName, + }) + .getRawOne(), + args.databaseName, + ); } async getBranches(args: t.DBArgs & { sortBy?: SortBranchesBy }): t.PR { - return this.query(qh.getBranchesQuery(args.sortBy), [], args.databaseName); + return this.queryForBuilder(async em => { + let sel = em.createQueryBuilder().select("*").from("dolt_branches", ""); + if (args.sortBy) { + sel = sel.addOrderBy(qh.getOrderByColForBranches(args.sortBy), "DESC"); + } + return sel.getRawMany(); + }, args.databaseName); } async createNewBranch(args: t.BranchArgs & { fromRefName: string }): t.PR { @@ -98,7 +185,7 @@ export class DoltQueryFactory ); } - async getDiffStat(args: t.RefsArgs & { tableName?: string }): t.PR { + async getDiffStat(args: t.RefsMaybeTableArgs): t.PR { return this.query( qh.getDiffStatQuery(!!args.tableName), [args.fromRefName, args.toRefName, args.tableName], @@ -107,7 +194,7 @@ export class DoltQueryFactory ); } - async getThreeDotDiffStat(args: t.RefsArgs & { tableName?: string }): t.PR { + async getThreeDotDiffStat(args: t.RefsMaybeTableArgs): t.PR { return this.query( qh.getThreeDotDiffStatQuery(!!args.tableName), [`${args.toRefName}...${args.fromRefName}`, args.tableName], @@ -116,7 +203,7 @@ export class DoltQueryFactory ); } - async getDiffSummary(args: t.RefsArgs & { tableName?: string }): t.PR { + async getDiffSummary(args: t.RefsMaybeTableArgs): t.PR { return this.query( qh.getDiffSummaryQuery(!!args.tableName), [args.fromRefName, args.toRefName, args.tableName], @@ -125,9 +212,7 @@ export class DoltQueryFactory ); } - async getThreeDotDiffSummary( - args: t.RefsArgs & { tableName?: string }, - ): t.PR { + async getThreeDotDiffSummary(args: t.RefsMaybeTableArgs): t.PR { return this.query( qh.getThreeDotDiffSummaryQuery(!!args.tableName), [`${args.toRefName}...${args.fromRefName}`, args.tableName], @@ -136,7 +221,7 @@ export class DoltQueryFactory ); } - async getSchemaPatch(args: t.RefsArgs & { tableName: string }): t.PR { + async getSchemaPatch(args: t.RefsTableArgs): t.PR { return this.query( qh.schemaPatchQuery, [args.fromRefName, args.toRefName, args.tableName], @@ -145,7 +230,7 @@ export class DoltQueryFactory ); } - async getThreeDotSchemaPatch(args: t.RefsArgs & { tableName: string }): t.PR { + async getThreeDotSchemaPatch(args: t.RefsTableArgs): t.PR { return this.query( qh.threeDotSchemaPatchQuery, [`${args.toRefName}...${args.fromRefName}`, args.tableName], @@ -154,7 +239,7 @@ export class DoltQueryFactory ); } - async getSchemaDiff(args: t.RefsArgs & { tableName: string }): t.PR { + async getSchemaDiff(args: t.RefsTableArgs): t.PR { return this.query( qh.schemaDiffQuery, [args.fromRefName, args.toRefName, args.tableName], @@ -163,7 +248,7 @@ export class DoltQueryFactory ); } - async getThreeDotSchemaDiff(args: t.RefsArgs & { tableName: string }): t.PR { + async getThreeDotSchemaDiff(args: t.RefsTableArgs): t.PR { return this.query( qh.threeDotSchemaDiffQuery, [`${args.toRefName}...${args.fromRefName}`, args.tableName], @@ -173,21 +258,56 @@ export class DoltQueryFactory } async getDocs(args: t.RefArgs): t.UPR { - return handleTableNotFound(async () => - this.query(qh.docsQuery, [], args.databaseName, args.refName), + return this.queryForBuilder( + async em => { + const sel = em + .createQueryBuilder() + .select("*") + .from(DoltSystemTable.DOCS, ""); + return handleTableNotFound(async () => sel.getRawMany()); + }, + args.databaseName, + args.refName, ); } async getStatus(args: t.RefArgs): t.PR { - return this.query(qh.statusQuery, [], args.databaseName, args.refName); + return this.queryForBuilder( + async em => + em + .createQueryBuilder() + .select("*") + .from("dolt_status", "") + .getRawMany(), + args.databaseName, + args.refName, + ); } - async getTag(args: t.TagArgs): t.PR { - return this.query(qh.tagQuery, [args.tagName], args.databaseName); + async getTag(args: t.TagArgs): t.UPR { + return this.queryForBuilder( + async em => + em + .createQueryBuilder() + .select("*") + .from("dolt_tags", "") + .where("dolt_tags.tag_name", { tag_name: args.tagName }) + .getRawOne(), + args.databaseName, + ); } async getTags(args: t.DBArgs): t.PR { - return this.query(qh.tagsQuery, [], args.databaseName); + return this.queryForBuilder( + async em => + em + .createQueryBuilder() + .select("*") + .from("dolt_tags", "") + .orderBy("dolt_tags.date", "DESC") + .getRawMany(), + args.databaseName, + ); } async createNewTag( @@ -310,3 +430,21 @@ export class DoltQueryFactory ); } } + +async function getTableInfoWithQR( + query: t.ParQuery, + args: t.TableArgs, +): Promise { + const columns = await query(qh.columnsQuery, [args.tableName]); + const fkRows = await query(qh.foreignKeysQuery, [ + args.tableName, + `${args.databaseName}/${args.refName}`, + ]); + const idxRows = await query(qh.indexQuery, [args.tableName]); + return { + tableName: args.tableName, + columns: columns.map(c => column.fromDoltRowRes(c, args.tableName)), + foreignKeys: foreignKey.fromDoltRowsRes(fkRows), + indexes: index.fromDoltRowsRes(idxRows), + }; +} diff --git a/graphql-server/src/queryFactory/dolt/queries.ts b/graphql-server/src/queryFactory/dolt/queries.ts index 0d44b2b8..e57e9420 100644 --- a/graphql-server/src/queryFactory/dolt/queries.ts +++ b/graphql-server/src/queryFactory/dolt/queries.ts @@ -1,30 +1,36 @@ import { SortBranchesBy } from "../../branches/branch.enum"; -import { DoltSystemTable } from "../../systemTables/systemTable.enums"; -import { getOrderByFromCols, getPKColsForRowsQuery } from "../mysql/queries"; import { RawRows } from "../types"; -export const getDoltSchemasQuery = (hasWhereCause = false): string => - `SELECT * FROM ${DoltSystemTable.SCHEMAS}${ - hasWhereCause ? " WHERE type = ?" : "" - }`; +// TABLE -export const doltProceduresQuery = `SELECT * FROM ${DoltSystemTable.PROCEDURES}`; +export const columnsQuery = `DESCRIBE ??`; -// BRANCHES +export const foreignKeysQuery = `SELECT * FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE +WHERE table_name=? AND table_schema=? +AND referenced_table_schema IS NOT NULL`; + +export const indexQuery = `SELECT + table_name, + index_name, + GROUP_CONCAT(comment) as COMMENTS, + GROUP_CONCAT(non_unique) AS NON_UNIQUES, + GROUP_CONCAT(column_name ORDER BY seq_in_index) AS COLUMNS +FROM information_schema.statistics +WHERE table_name=? AND index_name!="PRIMARY" +GROUP BY index_name;`; -export const branchQuery = `SELECT * FROM dolt_branches WHERE name=?`; +export const tableColsQuery = `SHOW FULL TABLES WHERE table_type = 'BASE TABLE'`; -export const getBranchesQuery = (sortBy?: SortBranchesBy) => - `SELECT * FROM dolt_branches ${getOrderByForBranches(sortBy)}`; +// BRANCHES export const callNewBranch = `CALL DOLT_BRANCH(?, ?)`; export const callDeleteBranch = `CALL DOLT_BRANCH("-D", ?)`; -function getOrderByForBranches(sortBy?: SortBranchesBy): string { +export function getOrderByColForBranches(sortBy?: SortBranchesBy): string { switch (sortBy) { case SortBranchesBy.LastUpdated: - return "ORDER BY latest_commit_date DESC "; + return "latest_commit_date"; default: return ""; } @@ -61,24 +67,12 @@ export const schemaDiffQuery = `SELECT * FROM DOLT_SCHEMA_DIFF(?, ?, ?)`; export const threeDotSchemaDiffQuery = `SELECT * FROM DOLT_SCHEMA_DIFF(?, ?)`; -// DOCS - -export const docsQuery = `SELECT * FROM dolt_docs`; - // PULLS export const callMerge = `CALL DOLT_MERGE(?, "--no-ff", "-m", ?)`; -// STATUS - -export const statusQuery = `SELECT * FROM dolt_status`; - // TAGS -export const tagsQuery = `SELECT * FROM dolt_tags ORDER BY date DESC`; - -export const tagQuery = `SELECT * FROM dolt_tags WHERE tag_name=?`; - export const callDeleteTag = `CALL DOLT_TAG("-d", ?)`; export const getCallNewTag = (hasMessage = false, hasAuthor = false) => @@ -91,6 +85,16 @@ export function getAuthorNameString(hasAuthor: boolean): string { return `, "--author", ?`; } +// Creates ORDER BY statement with column parameters +// i.e. ORDER BY ::col1, ::col2 +function getOrderByFromCols(numCols: number): string { + if (!numCols) return ""; + const pkCols = Array.from({ length: numCols }) + .map(() => `? ASC`) + .join(", "); + return pkCols === "" ? "" : `ORDER BY ${pkCols} `; +} + export const getRowsQueryAsOf = ( columns: RawRows, ): { q: string; cols: string[] } => { @@ -103,6 +107,12 @@ export const getRowsQueryAsOf = ( }; }; +function getPKColsForRowsQuery(cs: RawRows): string[] { + const pkCols = cs.filter(col => col.Key === "PRI"); + const cols = pkCols.map(c => c.Field); + return cols; +} + export const tableColsQueryAsOf = `DESCRIBE ?? AS OF ?`; export function getTableCommitDiffQuery( diff --git a/graphql-server/src/queryFactory/index.ts b/graphql-server/src/queryFactory/index.ts new file mode 100644 index 00000000..9cdb8c8e --- /dev/null +++ b/graphql-server/src/queryFactory/index.ts @@ -0,0 +1,133 @@ +import { DataSource, EntityManager, QueryRunner } from "typeorm"; +import { SortBranchesBy } from "../branches/branch.enum"; +import { CommitDiffType } from "../diffSummaries/diffSummary.enums"; +import { SchemaType } from "../schemas/schema.enums"; +import { TableDetails } from "../tables/table.model"; +import * as t from "./types"; + +export declare class QueryFactory { + ds: DataSource | undefined; + + isDolt: boolean; + + constructor(ds: DataSource | undefined); + + // UTILS + + getDS(): DataSource; + + getQR(): QueryRunner; + + handleAsyncQuery(work: (qr: QueryRunner) => Promise): Promise; + + query( + q: string, + p: t.Params, + dbName?: string, + refName?: string, + ): Promise; + + queryMultiple( + executeQuery: (pq: t.ParQuery) => Promise, + dbName?: string, + refName?: string, + ): Promise; + + queryForBuilder( + executeQuery: (em: EntityManager) => Promise, + dbName?: string, + refName?: string, + ): Promise; + + queryQR( + executeQuery: (qr: QueryRunner) => Promise, + dbName?: string, + refName?: string, + ): Promise; + + // QUERIES + + databases(): t.PR; + + currentDatabase(): Promise; + + createDatabase(args: t.DBArgs): Promise; + + getTableNames( + args: t.RefArgs, + filterSystemTables?: boolean, + ): Promise; + + getTables(args: t.RefArgs, tns: string[]): Promise; + + getTableInfo(args: t.TableArgs): Promise; + + getTablePKColumns(args: t.TableArgs): Promise; + + getTableRows(args: t.TableArgs, page: t.TableRowPagination): t.PR; + + getSqlSelect(args: t.RefArgs & { queryString: string }): t.PR; + + getSchemas(args: t.RefArgs, type?: SchemaType): t.UPR; + + getProcedures(args: t.RefArgs): t.UPR; + + // DOLT-SPECIFIC QUERIES + + getBranch(args: t.BranchArgs): t.UPR; + + getBranches(args: t.DBArgs & { sortBy?: SortBranchesBy }): t.PR; + + createNewBranch(args: t.BranchArgs & { fromRefName: string }): t.PR; + + callDeleteBranch(args: t.BranchArgs): t.PR; + + getLogs(args: t.RefArgs, offset: number): t.PR; + + getTwoDotLogs(args: t.RefsArgs): t.PR; + + getDiffStat(args: t.RefsMaybeTableArgs): t.PR; + + getThreeDotDiffStat(args: t.RefsMaybeTableArgs): t.PR; + + getDiffSummary(args: t.RefsMaybeTableArgs): t.PR; + + getThreeDotDiffSummary(args: t.RefsMaybeTableArgs): t.PR; + + getSchemaPatch(args: t.RefsTableArgs): t.PR; + + getThreeDotSchemaPatch(args: t.RefsTableArgs): t.PR; + + getSchemaDiff(args: t.RefsTableArgs): t.PR; + + getThreeDotSchemaDiff(args: t.RefsTableArgs): t.PR; + + getDocs(args: t.RefArgs): t.UPR; + + getStatus(args: t.RefArgs): t.PR; + + getTag(args: t.TagArgs): t.UPR; + + getTags(args: t.DBArgs): t.PR; + + createNewTag( + args: t.TagArgs & { + fromRefName: string; + message?: string; + }, + ): t.PR; + + callDeleteTag(args: t.TagArgs): t.PR; + + callMerge(args: t.BranchesArgs): Promise; + + resolveRefs( + args: t.RefsArgs & { type?: CommitDiffType }, + ): Promise<{ fromCommitId: string; toCommitId: string }>; + + getOneSidedRowDiff( + args: t.TableArgs & { offset: number }, + ): Promise<{ rows: t.RawRows; columns: t.RawRows }>; + + getRowDiffs(args: t.RowDiffArgs): t.DiffRes; +} diff --git a/graphql-server/src/queryFactory/mysql/index.ts b/graphql-server/src/queryFactory/mysql/index.ts index 4897e377..b14a4d35 100644 --- a/graphql-server/src/queryFactory/mysql/index.ts +++ b/graphql-server/src/queryFactory/mysql/index.ts @@ -1,15 +1,18 @@ /* eslint-disable class-methods-use-this */ +import { EntityManager, QueryRunner } from "typeorm"; +import { QueryFactory } from ".."; import { SchemaType } from "../../schemas/schema.enums"; +import { TableDetails } from "../../tables/table.model"; import { ROW_LIMIT } from "../../utils"; import { BaseQueryFactory } from "../base"; import * as t from "../types"; import * as qh from "./queries"; -import { notDoltError } from "./utils"; +import { convertToTableDetails, mapTablesRes, notDoltError } from "./utils"; export class MySQLQueryFactory extends BaseQueryFactory - implements t.QueryFactory + implements QueryFactory { isDolt = false; @@ -48,58 +51,86 @@ export class MySQLQueryFactory }); } - async databases(): t.PR { - return this.query(qh.databasesQuery, []); + async queryForBuilder( + executeQuery: (em: EntityManager) => Promise, + dbName?: string, + refName?: string, + ): Promise { + return this.handleAsyncQuery(async qr => { + if (dbName) { + await qr.query(qh.useDB(dbName, refName, this.isDolt)); + } + + return executeQuery(qr.manager); + }); } - async getTableNames(args: t.RefArgs): t.PR { - return this.query(qh.listTablesQuery, [], args.databaseName); + async queryQR( + executeQuery: (qr: QueryRunner) => Promise, + dbName?: string, + refName?: string, + ): Promise { + return this.handleAsyncQuery(async qr => { + if (dbName) { + await qr.query(qh.useDB(dbName, refName, this.isDolt)); + } + + return executeQuery(qr); + }); } - async getTableInfo(args: t.TableArgs): t.SPR { - return this.queryMultiple( - async query => getTableInfoWithQR(query, args, this.isDolt), + async databases(): t.PR { + return this.query(qh.databasesQuery, []); + } + + async getTableNames(args: t.RefArgs): Promise { + const res: t.RawRows = await this.query( + qh.listTablesQuery, + [], args.databaseName, - args.refName, ); + return mapTablesRes(res); } - async getTables(args: t.RefArgs, tns: string[]): t.PR { - return this.queryMultiple(async query => { - const tableInfos = await Promise.all( - tns.map(async name => { - const row = await getTableInfoWithQR( - query, - { - ...args, - tableName: name, - }, - this.isDolt, - ); - return row; - }), - ); - return tableInfos; + async getTableInfo(args: t.TableArgs): Promise { + return this.queryQR(async qr => { + const table = await qr.getTable(args.tableName); + if (!table) return undefined; + return convertToTableDetails(table); }, args.databaseName); } - async getTableColumns(args: t.TableArgs): t.PR { - return this.query( - qh.tableColsQuery, - [args.tableName], - args.databaseName, - args.refName, - ); + async getTables(args: t.RefArgs, tns: string[]): Promise { + return this.queryQR(async qr => { + const tables = await qr.getTables(tns); + return tables.map(convertToTableDetails); + }, args.databaseName); + } + + async getTablePKColumns(args: t.TableArgs): Promise { + return this.queryQR(async qr => { + const table = await qr.getTable(args.tableName); + if (!table) return []; + return table.columns.filter(c => c.isPrimary).map(c => c.name); + }, args.databaseName); } async getTableRows(args: t.TableArgs, page: t.TableRowPagination): t.PR { - const { q, cols } = qh.getRowsQuery(page.columns); - return this.query( - q, - [args.tableName, ...cols, ROW_LIMIT + 1, page.offset], - args.databaseName, - args.refName, - ); + return this.queryForBuilder(async em => { + let build = em + .createQueryBuilder() + .select("*") + .from(args.tableName, args.tableName); + + page.pkCols.forEach(col => { + build = build.addOrderBy(col, "ASC"); + }); + + return build + .limit(ROW_LIMIT + 1) + .offset(page.offset) + .getRawMany(); + }); } async getSqlSelect(args: t.RefArgs & { queryString: string }): t.PR { @@ -141,7 +172,7 @@ export class MySQLQueryFactory // DOLT QUERIES NOT IMPLEMENTED FOR MYSQL // Returns static branch - async getBranch(args: t.BranchArgs): t.PR { + async getBranch(args: t.BranchArgs): t.UPR { return [ { name: args.branchName, @@ -153,7 +184,8 @@ export class MySQLQueryFactory } async getBranches(args: t.DBArgs): t.PR { - return this.getBranch({ ...args, branchName: "main" }); + const branch = await this.getBranch({ ...args, branchName: "main" }); + return branch ?? []; } async createNewBranch(_: t.BranchArgs & { fromRefName: string }): t.PR { @@ -188,21 +220,19 @@ export class MySQLQueryFactory throw notDoltError("get three dot diff summary"); } - async getSchemaPatch(_args: t.RefsArgs & { tableName: string }): t.PR { + async getSchemaPatch(_args: t.RefsTableArgs): t.PR { throw notDoltError("get schema patch"); } - async getThreeDotSchemaPatch( - _args: t.RefsArgs & { tableName: string }, - ): t.PR { + async getThreeDotSchemaPatch(_args: t.RefsTableArgs): t.PR { throw notDoltError("get three dot schema patch"); } - async getSchemaDiff(_args: t.RefsArgs & { tableName: string }): t.PR { + async getSchemaDiff(_args: t.RefsTableArgs): t.PR { throw notDoltError("get schema diff"); } - async getThreeDotSchemaDiff(_args: t.RefsArgs & { tableName: string }): t.PR { + async getThreeDotSchemaDiff(_args: t.RefsTableArgs): t.PR { throw notDoltError("get three dot schema diff"); } @@ -214,7 +244,7 @@ export class MySQLQueryFactory throw notDoltError("get status"); } - async getTag(_args: t.TagArgs): t.PR { + async getTag(_args: t.TagArgs): t.UPR { throw notDoltError("get tag"); } @@ -254,17 +284,3 @@ export class MySQLQueryFactory throw notDoltError("get row sided diffs"); } } - -async function getTableInfoWithQR( - query: t.ParQuery, - args: t.TableArgs, - isDolt: boolean, -): t.SPR { - const columns = await query(qh.columnsQuery, [args.tableName]); - const fkRows = await query(qh.foreignKeysQuery, [ - args.tableName, - isDolt ? `${args.databaseName}/${args.refName}` : args.databaseName, - ]); - const idxRows = await query(qh.indexQuery, [args.tableName]); - return { name: args.tableName, columns, fkRows, idxRows }; -} diff --git a/graphql-server/src/queryFactory/mysql/queries.ts b/graphql-server/src/queryFactory/mysql/queries.ts index 09568f80..82ea66ee 100644 --- a/graphql-server/src/queryFactory/mysql/queries.ts +++ b/graphql-server/src/queryFactory/mysql/queries.ts @@ -1,5 +1,3 @@ -import { RawRows } from "../types"; - // Cannot use params here for the database revision. It will incorrectly // escape refs with dots export function useDB(dbName: string, refName?: string, isDolt = true): string { @@ -11,50 +9,8 @@ export function useDB(dbName: string, refName?: string, isDolt = true): string { export const databasesQuery = `SHOW DATABASES`; -export const columnsQuery = `DESCRIBE ??`; - -export const foreignKeysQuery = `SELECT * FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE table_name=? AND table_schema=? AND referenced_table_schema IS NOT NULL`; - -export const indexQuery = `SELECT - table_name, - index_name, - GROUP_CONCAT(comment) as COMMENTS, - GROUP_CONCAT(non_unique) AS NON_UNIQUES, - GROUP_CONCAT(column_name ORDER BY seq_in_index) AS COLUMNS -FROM information_schema.statistics -WHERE table_name=? AND index_name!="PRIMARY" -GROUP BY index_name;`; - export const listTablesQuery = `SHOW FULL TABLES WHERE table_type = 'BASE TABLE'`; -export const tableColsQuery = `SHOW FULL TABLES WHERE table_type = 'BASE TABLE'`; - -export const getRowsQuery = ( - columns: RawRows, -): { q: string; cols: string[] } => { - const cols = getPKColsForRowsQuery(columns); - return { - q: `SELECT * FROM ?? ${getOrderByFromCols(cols.length)}LIMIT ? OFFSET ?`, - cols, - }; -}; - -export function getPKColsForRowsQuery(cs: RawRows): string[] { - const pkCols = cs.filter(col => col.Key === "PRI"); - const cols = pkCols.map(c => c.Field); - return cols; -} - -// Creates ORDER BY statement with column parameters -// i.e. ORDER BY ::col1, ::col2 -export function getOrderByFromCols(numCols: number): string { - if (!numCols) return ""; - const pkCols = Array.from({ length: numCols }) - .map(() => `? ASC`) - .join(", "); - return pkCols === "" ? "" : `ORDER BY ${pkCols} `; -} - export const getViewsQuery = `SELECT TABLE_SCHEMA, TABLE_NAME FROM information_schema.tables WHERE TABLE_TYPE = 'VIEW' AND TABLE_SCHEMA = ?`; diff --git a/graphql-server/src/queryFactory/mysql/utils.ts b/graphql-server/src/queryFactory/mysql/utils.ts index cb9d4f63..6120545d 100644 --- a/graphql-server/src/queryFactory/mysql/utils.ts +++ b/graphql-server/src/queryFactory/mysql/utils.ts @@ -1,3 +1,77 @@ +import { Table, TableForeignKey } from "typeorm"; +import { ForeignKey } from "../../indexes/foreignKey.model"; +import { TableDetails } from "../../tables/table.model"; +import { RawRows } from "../types"; + export function notDoltError(action: string): Error { return new Error(`Cannot ${action} on non-Dolt database`); } + +export function mapTablesRes(tables: RawRows): string[] { + return tables + .map(row => { + // Can't use row[`Tables_in_${args.databaseName}`] because the column + // header can sometimes be `Tables_in_[dbName]/[branchName]` + const val = Object.keys(row).map(k => + k.startsWith("Tables_in_") ? row[k] : undefined, + )[0]; + return val; + }) + .filter(t => !!t && t !== "undefined"); +} + +export function convertToTableDetails(t: Table): TableDetails { + return { + tableName: t.name, + columns: t.columns.map(c => { + return { + name: c.name, + isPrimaryKey: c.isPrimary, + type: `${c.type}${c.length ? `(${c.length})` : ""}${ + c.unsigned ? " unsigned" : "" + }`, + constraints: [{ notNull: !c.isNullable }], + sourceTable: t.name, + }; + }), + foreignKeys: convertForeignKeys(t.foreignKeys, t.name), + indexes: t.indices.map(i => { + return { + name: i.name ?? "", + type: getIndexType(i.columnNames, i.isUnique), + comment: "", + columns: i.columnNames.map(c => { + return { name: c }; + }), + }; + }), + }; +} + +function getIndexType(cols: string[], nonUnique: boolean): string { + if (cols.length > 1) return "Composite"; + if (!nonUnique) return "Unique"; + return ""; +} + +function convertForeignKeys( + fks: TableForeignKey[], + tableName: string, +): ForeignKey[] { + const nameMap: Record = {}; + fks.forEach(fk => { + const name = fk.name ?? `${tableName}/${fk.columnNames[0]}`; + nameMap[name] = { + tableName, + columnName: fk.columnNames[0], + referencedTableName: fk.referencedTableName, + foreignKeyColumn: fk.referencedColumnNames.map((col, i) => { + return { + referencedColumnName: col, + referrerColumnIndex: i, + }; + }), + }; + }); + return Object.values(nameMap); +} diff --git a/graphql-server/src/queryFactory/types.ts b/graphql-server/src/queryFactory/types.ts index f3e88786..a7806ec0 100644 --- a/graphql-server/src/queryFactory/types.ts +++ b/graphql-server/src/queryFactory/types.ts @@ -1,8 +1,4 @@ -import { DataSource, QueryRunner } from "typeorm"; -import { SortBranchesBy } from "../branches/branch.enum"; -import { CommitDiffType } from "../diffSummaries/diffSummary.enums"; import { DiffRowType } from "../rowDiffs/rowDiff.enums"; -import { SchemaType } from "../schemas/schema.enums"; export type DBArgs = { databaseName: string }; export type RefArgs = DBArgs & { refName: string }; @@ -20,6 +16,8 @@ export type RefsArgs = DBArgs & { toRefName: string; refName?: string; }; +export type RefsMaybeTableArgs = RefsArgs & { tableName?: string }; +export type RefsTableArgs = RefsArgs & { tableName: string }; export type RowDiffArgs = DBArgs & { refName?: string; tableName: string; @@ -39,113 +37,6 @@ export type UPR = Promise; export type Params = Array | undefined; export type ParQuery = (q: string, p?: Params) => PR; -export type TableRowPagination = { columns: RawRow[]; offset: number }; +export type TableRowPagination = { pkCols: string[]; offset: number }; export type DiffRes = Promise<{ colsUnion: RawRows; diff: RawRows }>; export type CommitsRes = Promise<{ fromCommitId: string; toCommitId: string }>; - -export declare class QueryFactory { - ds: DataSource | undefined; - - isDolt: boolean; - - constructor(ds: DataSource | undefined); - - // UTILS - - getDS(): DataSource; - - getQR(): QueryRunner; - - handleAsyncQuery(work: (qr: QueryRunner) => Promise): Promise; - - query(q: string, p: Params, dbName?: string, refName?: string): Promise; - - queryMultiple( - executeQuery: (pq: ParQuery) => Promise, - dbName?: string, - refName?: string, - ): Promise; - - // QUERIES - - databases(): PR; - - currentDatabase(): Promise; - - createDatabase(args: DBArgs): Promise; - - getTableNames(args: RefArgs, filterSystemTables?: boolean): PR; - - getTables(args: RefArgs, tns: string[]): PR; - - getTableInfo(args: TableArgs): SPR; - - getTableColumns(args: TableArgs): PR; - - getTableRows(args: TableArgs, page: TableRowPagination): PR; - - getSqlSelect(args: RefArgs & { queryString: string }): PR; - - getSchemas(args: RefArgs, type?: SchemaType): UPR; - - getProcedures(args: RefArgs): UPR; - - // DOLT-SPECIFIC QUERIES - - getBranch(args: BranchArgs): PR; - - getBranches(args: DBArgs & { sortBy?: SortBranchesBy }): PR; - - createNewBranch(args: BranchArgs & { fromRefName: string }): PR; - - callDeleteBranch(args: BranchArgs): PR; - - getLogs(args: RefArgs, offset: number): PR; - - getTwoDotLogs(args: RefsArgs): PR; - - getDiffStat(args: RefsArgs & { tableName?: string }): PR; - - getThreeDotDiffStat(args: RefsArgs & { tableName?: string }): PR; - - getDiffSummary(args: RefsArgs & { tableName?: string }): PR; - - getThreeDotDiffSummary(args: RefsArgs & { tableName?: string }): PR; - - getSchemaPatch(args: RefsArgs & { tableName: string }): PR; - - getThreeDotSchemaPatch(args: RefsArgs & { tableName: string }): PR; - - getSchemaDiff(args: RefsArgs & { tableName: string }): PR; - - getThreeDotSchemaDiff(args: RefsArgs & { tableName: string }): PR; - - getDocs(args: RefArgs): UPR; - - getStatus(args: RefArgs): PR; - - getTag(args: TagArgs): PR; - - getTags(args: DBArgs): PR; - - createNewTag( - args: TagArgs & { - fromRefName: string; - message?: string; - }, - ): PR; - - callDeleteTag(args: TagArgs): PR; - - callMerge(args: BranchesArgs): Promise; - - resolveRefs( - args: RefsArgs & { type?: CommitDiffType }, - ): Promise<{ fromCommitId: string; toCommitId: string }>; - - getOneSidedRowDiff( - args: TableArgs & { offset: number }, - ): Promise<{ rows: RawRows; columns: RawRows }>; - - getRowDiffs(args: RowDiffArgs): DiffRes; -} diff --git a/graphql-server/src/rows/row.resolver.ts b/graphql-server/src/rows/row.resolver.ts index f00c3639..df79a928 100644 --- a/graphql-server/src/rows/row.resolver.ts +++ b/graphql-server/src/rows/row.resolver.ts @@ -19,10 +19,10 @@ export class RowResolver { @Query(_returns => RowList) async rows(@Args() args: ListRowsArgs): Promise { const conn = this.conn.connection(); - const columns = await conn.getTableColumns(args); + const pkCols = await conn.getTablePKColumns(args); const offset = args.offset ?? 0; const rows = await conn.getTableRows(args, { - columns, + pkCols, offset, }); return fromDoltListRowRes(rows, offset); diff --git a/graphql-server/src/tables/table.model.ts b/graphql-server/src/tables/table.model.ts index ed700a4d..ed45c2eb 100644 --- a/graphql-server/src/tables/table.model.ts +++ b/graphql-server/src/tables/table.model.ts @@ -2,19 +2,9 @@ import { Field, ID, ObjectType } from "@nestjs/graphql"; import * as column from "../columns/column.model"; import * as foreignKey from "../indexes/foreignKey.model"; import * as index from "../indexes/index.model"; -import { RawRow } from "../queryFactory/types"; @ObjectType() -export class Table { - @Field(_type => ID) - _id: string; - - @Field() - databaseName: string; - - @Field() - refName: string; - +export class TableDetails { @Field() tableName: string; @@ -28,6 +18,18 @@ export class Table { indexes: index.Index[]; } +@ObjectType() +export class Table extends TableDetails { + @Field(_type => ID) + _id: string; + + @Field() + databaseName: string; + + @Field() + refName: string; +} + @ObjectType() export class TableNames { @Field(_type => [String]) @@ -37,18 +39,12 @@ export class TableNames { export function fromDoltRowRes( databaseName: string, refName: string, - tableName: string, - columns: RawRow[], - fkRows: RawRow[], - idxRows: RawRow[], + details: TableDetails, ): Table { return { - _id: `databases/${databaseName}/refs/${refName}/tables/${tableName}`, + ...details, + _id: `databases/${databaseName}/refs/${refName}/tables/${details.tableName}`, databaseName, refName, - tableName, - columns: columns.map(c => column.fromDoltRowRes(c, tableName)), - foreignKeys: foreignKey.fromDoltRowsRes(fkRows), - indexes: index.fromDoltRowsRes(idxRows), }; } diff --git a/graphql-server/src/tables/table.resolver.ts b/graphql-server/src/tables/table.resolver.ts index 9d23a8d5..ad2e37e0 100644 --- a/graphql-server/src/tables/table.resolver.ts +++ b/graphql-server/src/tables/table.resolver.ts @@ -3,7 +3,6 @@ import { ConnectionResolver } from "../connections/connection.resolver"; import { handleTableNotFound } from "../utils"; import { RefArgs, TableArgs } from "../utils/commonTypes"; import { Table, TableNames, fromDoltRowRes } from "./table.model"; -import { mapTablesRes } from "./utils"; @ArgsType() class ListTableArgs extends RefArgs { @@ -19,38 +18,25 @@ export class TableResolver { async table(@Args() args: TableArgs): Promise { const conn = this.conn.connection(); const res = await conn.getTableInfo(args); - return fromDoltRowRes( - args.databaseName, - args.refName, - args.tableName, - res.columns, - res.fkRows, - res.idxRows, - ); + if (!res) { + throw new Error("no such table in database"); + } + return fromDoltRowRes(args.databaseName, args.refName, res); } @Query(_returns => TableNames) async tableNames(@Args() args: ListTableArgs): Promise { const conn = this.conn.connection(); const res = await conn.getTableNames(args, args.filterSystemTables); - return { list: mapTablesRes(res) }; + return { list: res }; } @Query(_returns => [Table]) async tables(@Args() args: ListTableArgs): Promise { const conn = this.conn.connection(); const tableNames = await conn.getTableNames(args, args.filterSystemTables); - const res = await conn.getTables(args, mapTablesRes(tableNames)); - return res.map(t => - fromDoltRowRes( - args.databaseName, - args.refName, - t.name, - t.columns, - t.fkRows, - t.idxRows, - ), - ); + const res = await conn.getTables(args, tableNames); + return res.map(t => fromDoltRowRes(args.databaseName, args.refName, t)); } // Utils diff --git a/graphql-server/src/tables/utils.ts b/graphql-server/src/tables/utils.ts deleted file mode 100644 index 734e0cef..00000000 --- a/graphql-server/src/tables/utils.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { RawRows } from "../queryFactory/types"; - -export function mapTablesRes(tables: RawRows): string[] { - return tables - .map(row => { - // Can't use row[`Tables_in_${args.databaseName}`] because the column - // header can sometimes be `Tables_in_[dbName]/[branchName]` - const val = Object.keys(row).map(k => - k.startsWith("Tables_in_") ? row[k] : undefined, - )[0]; - return val; - }) - .filter(t => !!t && t !== "undefined"); -} diff --git a/graphql-server/src/tags/tag.resolver.ts b/graphql-server/src/tags/tag.resolver.ts index 45aa279b..7b362473 100644 --- a/graphql-server/src/tags/tag.resolver.ts +++ b/graphql-server/src/tags/tag.resolver.ts @@ -48,7 +48,7 @@ export class TagResolver { async tag(@Args() args: TagArgs): Promise { const conn = this.conn.connection(); const res = await conn.getTag(args); - if (!res.length) return undefined; + if (!res?.length) return undefined; return fromDoltRowRes(args.databaseName, res[0]); }