All files postgres-schema-parser.ts

89.09% Statements 49/55
76.92% Branches 30/39
100% Functions 6/6
90.57% Lines 48/53

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173                    2x   2x 2x                   2x   2x 3x     2x 3x 3x         3x   3x                 3x 5x         5x   5x 5x 6x           6x           3x                                                           3x 1x 1x 1x 1x     1x 1x 1x 1x           3x                 3x 14x 14x 14x 14x 14x 1x     14x   14x   14x 3x 3x           14x             2x               2x 16x   3x 3x     3x          
/*
 * Deepkit Framework
 * Copyright (C) 2021 Deepkit UG, Marc J. Schmidt
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the MIT License.
 *
 * You should have received a copy of the MIT License along with this program.
 */
 
import { DatabaseModel, ForeignKey, Table, parseType, SchemaParser } from '@deepkit/sql';
 
export class PostgresSchemaParser extends SchemaParser {
    protected defaultPrecisions = {
        'char': 1,
        'character': 1,
        'integer': 32,
        'bigint': 64,
        'smallint': 16,
        'double precision': 53
    };
 
    async parse(database: DatabaseModel, limitTableNames?: string[]) {
        await this.parseTables(database, limitTableNames);
 
        for (const table of database.tables) {
            await this.addColumns(table);
        }
 
        for (const table of database.tables) {
            await this.addIndexes(table);
            await this.addForeignKeys(database, table);
        }
    }
 
    protected async addIndexes(table: Table) {
        const oid = `'"${table.schemaName || 'public'}"."${table.getName()}"'::regclass`;
 
        const indexes = await this.connection.execAndReturnAll(`
        SELECT DISTINCT ON (cls.relname) cls.relname as idxname, indkey, idx.indisunique as unique, idx.indisprimary as primary
        FROM pg_index idx
                 JOIN pg_class cls ON cls.oid = indexrelid
        WHERE indrelid = ${oid}
          AND NOT indisprimary
        ORDER BY cls.relname
        `);
 
        for (const row of indexes) {
            Iif (row.primary) {
                table.getColumn(row.column_name).isPrimaryKey = true;
                continue;
            }
 
            const index = table.addIndex(row.idxname, row.unique);
 
            const attnums = row.indkey.split(' ');
            for (const attnum of attnums) {
                const column = await this.connection.execAndReturnSingle(`
                    SELECT a.attname
                    FROM pg_catalog.pg_class c JOIN pg_catalog.pg_attribute a ON a.attrelid = c.oid
                    WHERE c.oid = ${oid} AND a.attnum = ${attnum} AND NOT a.attisdropped
                    ORDER BY a.attnum
                `);
                index.addColumn(column.attname);
            }
        }
    }
 
    protected async addForeignKeys(database: DatabaseModel, table: Table) {
        const rows = await this.connection.execAndReturnAll(`
        select kcu.table_name      as table_name,
               kcu.constraint_name as constraint_name,
               rel_kcu.table_name  as referenced_table_name,
               rel_kcu.constraint_schema  as referenced_schema_name,
               kcu.column_name     as column_name,
               rel_kcu.column_name as referenced_column_name,
               kcu.constraint_name,
               rco.update_rule,
               rco.delete_rule
        from information_schema.table_constraints tco
                 join information_schema.key_column_usage kcu
                      on tco.constraint_schema = kcu.constraint_schema
                          and tco.constraint_name = kcu.constraint_name
                 join information_schema.referential_constraints rco
                      on tco.constraint_schema = rco.constraint_schema
                          and tco.constraint_name = rco.constraint_name
                 join information_schema.key_column_usage rel_kcu
                      on rco.unique_constraint_schema = rel_kcu.constraint_schema
                          and rco.unique_constraint_name = rel_kcu.constraint_name
                          and kcu.ordinal_position = rel_kcu.ordinal_position
        where tco.table_name = '${table.getName()}'
          and tco.table_schema = '${table.schemaName || 'public'}'
          and tco.constraint_schema = tco.table_schema
          and tco.constraint_type = 'FOREIGN KEY'
        order by kcu.table_schema, kcu.table_name, kcu.ordinal_position
        `);
 
        let lastId: string | undefined;
        let foreignKey: ForeignKey | undefined;
        for (const row of rows) {
            Eif (row.constraint_name !== lastId) {
                lastId = row.constraint_name;
                const foreignTable = database.getTable(row.referenced_table_name);
                foreignKey = table.addForeignKey(row.constraint_name, foreignTable);
            }
 
            Eif (foreignKey) {
                foreignKey.addReference(row.column_name, row.referenced_column_name);
                foreignKey.onUpdate = row.update_rule;
                foreignKey.onDelete = row.delete_rule;
            }
        }
    }
 
    protected async addColumns(table: Table) {
        const rows = await this.connection.execAndReturnAll(`
        SELECT
            column_name, data_type, column_default, is_nullable,
            numeric_precision, numeric_scale, character_maximum_length
        FROM information_schema.columns
        WHERE
            table_schema = '${table.schemaName || 'public'}' AND table_name = '${table.getName()}'
        `);
 
        for (const row of rows) {
            const column = table.addColumn(row.column_name);
            parseType(column, row.data_type);
            const size = row.character_maximum_length || row.numeric_precision;
            const scale = row.numeric_scale;
            if (size && column.type && size !== this.defaultPrecisions[column.type]) {
                column.size = size;
            }
 
            Iif (scale) column.scale = scale;
 
            column.isNotNull = row.is_nullable === 'NO';
 
            if ('string' === typeof row.column_default) {
                Eif (row.column_default.includes('nextval(') && row.column_default.includes('::regclass')) {
                    column.isAutoIncrement = true;
                } else {
                    column.defaultValue = JSON.parse(row.column_default);
                }
            }
 
            Iif (row.data_type.includes('SERIAL')) {
                column.isAutoIncrement = true;
            }
        }
    }
 
    protected async parseTables(database: DatabaseModel, limitTableNames?: string[]) {
        const rows = await this.connection.execAndReturnAll(`
            select table_name, table_schema as schema_name
            from information_schema.tables where table_schema not like 'pg_%' and table_schema = current_schema()
            and table_name != 'geometry_columns' and table_name != 'spatial_ref_sys' and table_type != 'VIEW'
            and table_schema = '${database.schemaName || 'public'}' 
            order by table_name
        `);
 
        for (const row of rows) {
            if (limitTableNames && !limitTableNames.includes(row.table_name)) continue;
 
            const table = database.addTable(row.table_name);
            Iif (row.schema_name !== 'public') {
                table.schemaName = row.schema_name || database.schemaName;
            } else {
                table.schemaName = database.schemaName;
            }
        }
    }
}