From bec624eb54bc4ba1114806d75ff5aaa80f0737b2 Mon Sep 17 00:00:00 2001 From: ChengLei Shao Date: Sun, 2 Apr 2023 14:32:20 +0800 Subject: [PATCH] fix: infer view column type with alias (#1634) * fix: infer view column type with alias * fix: build * fix: type * fix: test --- .../src/__tests__/view/view-inference.test.ts | 42 +++++++++++++++ .../query-interface/mysql-query-interface.ts | 23 ++++---- .../postgres-query-interface.ts | 54 ++++++++++++++++++- .../src/query-interface/query-interface.ts | 8 +-- .../query-interface/sqlite-query-interface.ts | 23 ++++---- .../core/database/src/view/view-inference.ts | 2 +- .../http-api/view-collection.test.ts | 1 + 7 files changed, 126 insertions(+), 27 deletions(-) diff --git a/packages/core/database/src/__tests__/view/view-inference.test.ts b/packages/core/database/src/__tests__/view/view-inference.test.ts index f5e9f5052..d8de7ac24 100644 --- a/packages/core/database/src/__tests__/view/view-inference.test.ts +++ b/packages/core/database/src/__tests__/view/view-inference.test.ts @@ -15,6 +15,48 @@ describe('view inference', function () { await db.close(); }); + it('should infer field with alias', async () => { + if (db.options.dialect !== 'postgres') return; + + const UserCollection = db.collection({ + name: 'users', + fields: [ + { + name: 'id', + type: 'bigInt', + interface: 'bigInt', + }, + { + name: 'name', + type: 'string', + interface: 'test', + }, + ], + }); + + await db.sync(); + + const viewName = 'user_posts'; + + const dropViewSQL = `DROP VIEW IF EXISTS ${viewName}`; + await db.sequelize.query(dropViewSQL); + + const viewSQL = ` + CREATE VIEW ${viewName} as SELECT 1 as const_field, users.id as user_id_field, users.name FROM ${UserCollection.quotedTableName()} as users + `; + + await db.sequelize.query(viewSQL); + + const inferredFields = await ViewFieldInference.inferFields({ + db, + viewName, + viewSchema: 'public', + }); + + expect(inferredFields['user_id_field'].source).toBe('users.id'); + expect(inferredFields['name'].source).toBe('users.name'); + }); + it('should infer collection fields', async () => { const UserCollection = db.collection({ name: 'users', diff --git a/packages/core/database/src/query-interface/mysql-query-interface.ts b/packages/core/database/src/query-interface/mysql-query-interface.ts index 887995728..ee8189855 100644 --- a/packages/core/database/src/query-interface/mysql-query-interface.ts +++ b/packages/core/database/src/query-interface/mysql-query-interface.ts @@ -31,13 +31,13 @@ export default class MysqlQueryInterface extends QueryInterface { return await this.db.sequelize.query(sql, { type: 'SELECT' }); } - async viewColumnUsage(options: { viewName: string; schema?: string }): Promise< - Array<{ + async viewColumnUsage(options: { viewName: string; schema?: string }): Promise<{ + [view_column_name: string]: { column_name: string; table_name: string; table_schema?: string; - }> - > { + }; + }> { try { const viewDefinition = await this.db.sequelize.query(`SHOW CREATE VIEW ${options.viewName}`, { type: 'SELECT' }); const createView = viewDefinition[0]['Create View']; @@ -52,18 +52,21 @@ export default class MysqlQueryInterface extends QueryInterface { const results = []; for (const column of columns) { if (column.expr.type === 'column_ref') { - results.push({ - column_name: column.expr.column, - table_name: column.expr.table, - }); + results.push([ + column.as || column.expr.column, + { + column_name: column.expr.column, + table_name: column.expr.table, + }, + ]); } } - return results; + return Object.fromEntries(results); } catch (e) { this.db.logger.warn(e); - return []; + return {}; } } } diff --git a/packages/core/database/src/query-interface/postgres-query-interface.ts b/packages/core/database/src/query-interface/postgres-query-interface.ts index eb5e054e8..e730adc41 100644 --- a/packages/core/database/src/query-interface/postgres-query-interface.ts +++ b/packages/core/database/src/query-interface/postgres-query-interface.ts @@ -1,5 +1,6 @@ import QueryInterface from './query-interface'; import { Collection } from '../collection'; +import sqlParser from '../sql-parser'; export default class PostgresQueryInterface extends QueryInterface { constructor(db) { @@ -32,7 +33,13 @@ export default class PostgresQueryInterface extends QueryInterface { return await this.db.sequelize.query(sql, { type: 'SELECT' }); } - async viewColumnUsage(options) { + async viewColumnUsage(options): Promise<{ + [view_column_name: string]: { + column_name: string; + table_name: string; + table_schema?: string; + }; + }> { const { viewName, schema = 'public' } = options; const sql = ` SELECT * @@ -41,6 +48,49 @@ export default class PostgresQueryInterface extends QueryInterface { AND view_name = '${viewName}'; `; - return (await this.db.sequelize.query(sql, { type: 'SELECT' })) as any; + const columnUsages = (await this.db.sequelize.query(sql, { type: 'SELECT' })) as Array<{ + column_name: string; + table_name: string; + table_schema: string; + }>; + + const viewDefQuery = await this.db.sequelize.query( + ` + SELECT viewname, definition +FROM pg_views +WHERE schemaname = '${schema}' AND viewname = '${viewName}'; + `, + { type: 'SELECT' }, + ); + + const def = viewDefQuery[0]['definition']; + try { + const { ast } = sqlParser.parse(def); + const columns = ast[0].columns; + + const usages = columns + .map((column) => { + const fieldAlias = column.as || column.expr.column; + const columnUsage = columnUsages.find( + (columnUsage) => + columnUsage.column_name === column.expr.column && columnUsage.table_name === column.expr.table, + ); + + return [ + fieldAlias, + columnUsage + ? { + ...columnUsage, + } + : null, + ]; + }) + .filter(([, columnUsage]) => columnUsage !== null); + + return Object.fromEntries(usages); + } catch (e) { + this.db.logger.warn(e); + return {}; + } } } diff --git a/packages/core/database/src/query-interface/query-interface.ts b/packages/core/database/src/query-interface/query-interface.ts index 8792a24cc..1572d4edf 100644 --- a/packages/core/database/src/query-interface/query-interface.ts +++ b/packages/core/database/src/query-interface/query-interface.ts @@ -13,13 +13,13 @@ export default abstract class QueryInterface { abstract listViews(); - abstract viewColumnUsage(options: { viewName: string; schema?: string }): Promise< - Array<{ + abstract viewColumnUsage(options: { viewName: string; schema?: string }): Promise<{ + [view_column_name: string]: { column_name: string; table_name: string; table_schema?: string; - }> - >; + }; + }>; async dropAll(options) { if (options.drop !== true) return; diff --git a/packages/core/database/src/query-interface/sqlite-query-interface.ts b/packages/core/database/src/query-interface/sqlite-query-interface.ts index 137b75789..92458cd94 100644 --- a/packages/core/database/src/query-interface/sqlite-query-interface.ts +++ b/packages/core/database/src/query-interface/sqlite-query-interface.ts @@ -33,13 +33,13 @@ export default class SqliteQueryInterface extends QueryInterface { }); } - async viewColumnUsage(options: { viewName: string; schema?: string }): Promise< - Array<{ + async viewColumnUsage(options: { viewName: string; schema?: string }): Promise<{ + [view_column_name: string]: { column_name: string; table_name: string; table_schema?: string; - }> - > { + }; + }> { try { const viewDefinition = await this.db.sequelize.query( `SELECT sql FROM sqlite_master WHERE name = '${options.viewName}' AND type = 'view'`, @@ -60,17 +60,20 @@ export default class SqliteQueryInterface extends QueryInterface { const results = []; for (const column of columns) { if (column.expr.type === 'column_ref') { - results.push({ - column_name: column.expr.column, - table_name: column.expr.table, - }); + results.push([ + column.as || column.expr.column, + { + column_name: column.expr.column, + table_name: column.expr.table, + }, + ]); } } - return results; + return Object.fromEntries(results); } catch (e) { this.db.logger.warn(e); - return []; + return {}; } } } diff --git a/packages/core/database/src/view/view-inference.ts b/packages/core/database/src/view/view-inference.ts index efa08c19e..ae497c555 100644 --- a/packages/core/database/src/view/view-inference.ts +++ b/packages/core/database/src/view/view-inference.ts @@ -33,7 +33,7 @@ export class ViewFieldInference { // @ts-ignore return Object.fromEntries( Object.entries(columns).map(([name, column]) => { - const usage = columnUsage.find((item) => item.column_name === name); + const usage = columnUsage[name]; if (usage) { const collectionField = (() => { diff --git a/packages/plugins/collection-manager/src/__tests__/http-api/view-collection.test.ts b/packages/plugins/collection-manager/src/__tests__/http-api/view-collection.test.ts index 5e7a3c947..451357db8 100644 --- a/packages/plugins/collection-manager/src/__tests__/http-api/view-collection.test.ts +++ b/packages/plugins/collection-manager/src/__tests__/http-api/view-collection.test.ts @@ -67,6 +67,7 @@ SELECT * FROM numbers; expect(response.status).toBe(200); const data = response.body.data; + if (app.db.options.dialect === 'mysql') { expect(data.fields.n.type).toBe('bigInt'); } else if (app.db.options.dialect == 'postgres') {