fix: infer view column type with alias (#1634)

* fix: infer view column type with alias

* fix: build

* fix: type

* fix: test
This commit is contained in:
ChengLei Shao 2023-04-02 14:32:20 +08:00 committed by GitHub
parent 822099271a
commit bec624eb54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 126 additions and 27 deletions

View File

@ -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',

View File

@ -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 {};
}
}
}

View File

@ -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 {};
}
}
}

View File

@ -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;

View File

@ -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 {};
}
}
}

View File

@ -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 = (() => {

View File

@ -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') {