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:
		
							parent
							
								
									822099271a
								
							
						
					
					
						commit
						bec624eb54
					
				@ -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',
 | 
			
		||||
 | 
			
		||||
@ -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 {};
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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 {};
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
@ -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 {};
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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 = (() => {
 | 
			
		||||
 | 
			
		||||
@ -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') {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user