Skip to content

Commit

Permalink
Merge pull request #31 from sercanvirlan/feat/add_is_unique_flag
Browse files Browse the repository at this point in the history
feat: add is_unique flag for mysql and postgres, partially fixes #20
  • Loading branch information
rijkvanzanten authored Mar 5, 2021
2 parents 0b0dd01 + 4825354 commit f6e9fd3
Show file tree
Hide file tree
Showing 23 changed files with 8,337 additions and 81 deletions.
1 change: 1 addition & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ jobs:

- run: "while ! docker-compose logs mysql | grep -q 'mysqld: ready for connections.'; do sleep 2; done"
- run: "while ! docker-compose logs postgres | grep -q 'database system is ready to accept connections'; do sleep 2; done"
- run: "while ! docker-compose logs mssql | grep -q 'SQL Server is now ready for client connections'; do sleep 2; done"

- run: npm test
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,6 @@ dist
# TernJS port file
.tern-port

.DS_Store
.DS_Store
.vscode

35 changes: 35 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,38 @@ services:
- 5101:5432
volumes:
- ./test/seed/postgres.sql:/docker-entrypoint-initdb.d/seed.sql
mssql:
image: microsoft/mssql-server-linux
restart: always
environment:
- ACCEPT_EULA=Y
- SA_PASSWORD=Test@123
working_dir: /usr/src/app
command: sh -c ' chmod +x ./scripts/mssql-entrypoint.sh; ./scripts/mssql-entrypoint.sh & /opt/mssql/bin/sqlservr;'
ports:
- 1433:1433
volumes:
- mssql-volume:/var/opt/mssql/
- ./test:/usr/src/app

# Note: you need to be logged in to DockerHub, and accept the EULA for oracle/database-enterprise to work
# ... no clue how to get around that in GH Actions for tests ...
# See https://github.com/oracle/docker-images/issues/1156 for more on this
# oracle:
# image: store/oracle/database-enterprise:12.2.0.1
# restart: always
# ports:
# - 1521:32769
# - 5500:32768
# tty: true
# This is wat `knex` sues, but seems to crash with "ORA-01012: not logged on"
# oracledbxe:
# image: quillbuilduser/oracle-18-xe
# container_name: oracledbxe_container
# ports:
# - '21521:1521'
# environment:
# - ORACLE_ALLOW_REMOTE=true

volumes:
mssql-volume:
125 changes: 90 additions & 35 deletions lib/dialects/mssql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type RawColumn = {
NUMERIC_PRECISION: number | null;
NUMERIC_SCALE: number | null;
IS_NULLABLE: 'YES' | 'NO';
IS_UNIQUE: 'YES' | 'NO';
COLLATION_NAME: string | null;
CONSTRAINT_TABLE_NAME: string | null;
CONSTRAINT_COLUMN_NAME: string | null;
Expand Down Expand Up @@ -158,46 +159,86 @@ export default class MSSQL implements SchemaInspector {
'pk.PK_SET',
'rc.UPDATE_RULE',
'rc.DELETE_RULE',
'rc.MATCH_OPTION'
'rc.MATCH_OPTION',
'cu.IS_UNIQUE'
)
.from(dbName + '.INFORMATION_SCHEMA.COLUMNS as c')
.from(dbName + '.INFORMATION_SCHEMA.COLUMNS AS c')
.joinRaw(
`left join (
select CONSTRAINT_NAME AS CONSTRAINT_NAME, TABLE_NAME as CONSTRAINT_TABLE_NAME, COLUMN_NAME AS CONSTRAINT_COLUMN_NAME, CONSTRAINT_CATALOG, CONSTRAINT_SCHEMA, PK_SET = CASE
WHEN CONSTRAINT_NAME like '%pk%' THEN 'PRIMARY'
ELSE NULL
END from ${dbName}.INFORMATION_SCHEMA.KEY_COLUMN_USAGE
) as pk
`
LEFT JOIN (
SELECT
CONSTRAINT_NAME AS CONSTRAINT_NAME,
TABLE_NAME AS CONSTRAINT_TABLE_NAME,
COLUMN_NAME AS CONSTRAINT_COLUMN_NAME,
CONSTRAINT_CATALOG,
CONSTRAINT_SCHEMA,
PK_SET = CASE WHEN CONSTRAINT_NAME LIKE '%pk%'
THEN 'PRIMARY'
ELSE NULL
END
FROM ${dbName}.INFORMATION_SCHEMA.KEY_COLUMN_USAGE
) as pk
ON [c].[TABLE_NAME] = [pk].[CONSTRAINT_TABLE_NAME]
AND [c].[TABLE_CATALOG] = [pk].[CONSTRAINT_CATALOG]
AND [c].[COLUMN_NAME] = [pk].[CONSTRAINT_COLUMN_NAME]
AND [c].[TABLE_CATALOG] = [pk].[CONSTRAINT_CATALOG]
AND [c].[COLUMN_NAME] = [pk].[CONSTRAINT_COLUMN_NAME]
`
)
.joinRaw(
`left join (
select CONSTRAINT_NAME,CONSTRAINT_CATALOG, CONSTRAINT_SCHEMA, MATCH_OPTION, DELETE_RULE, UPDATE_RULE from ${dbName}.INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS
) as rc
ON [pk].[CONSTRAINT_NAME] = [rc].[CONSTRAINT_NAME]
AND [pk].[CONSTRAINT_CATALOG] = [rc].[CONSTRAINT_CATALOG]
AND [pk].[CONSTRAINT_SCHEMA] = [rc].[CONSTRAINT_SCHEMA]`
`
LEFT JOIN (
SELECT
CONSTRAINT_NAME,
CONSTRAINT_CATALOG,
CONSTRAINT_SCHEMA,
MATCH_OPTION,
DELETE_RULE,
UPDATE_RULE
FROM ${dbName}.INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS
) AS rc
ON [pk].[CONSTRAINT_NAME] = [rc].[CONSTRAINT_NAME]
AND [pk].[CONSTRAINT_CATALOG] = [rc].[CONSTRAINT_CATALOG]
AND [pk].[CONSTRAINT_SCHEMA] = [rc].[CONSTRAINT_SCHEMA]
`
)
.joinRaw(
`
LEFT JOIN
(SELECT
COLUMNPROPERTY(object_id(TABLE_NAME), COLUMN_NAME, 'IsIdentity') as EXTRA,
TABLE_NAME,
COLUMN_NAME,
TABLE_CATALOG
FROM
INFORMATION_SCHEMA.COLUMNS
WHERE
COLUMNPROPERTY(object_id(TABLE_NAME), COLUMN_NAME, 'IsIdentity') = 1) AS ac
ON [c].[TABLE_NAME] = [ac].[TABLE_NAME]
AND [c].[TABLE_CATALOG] = [ac].[TABLE_CATALOG]
AND [c].[COLUMN_NAME] = [ac].[COLUMN_NAME]
`
LEFT JOIN (
SELECT
COLUMNPROPERTY(object_id(TABLE_NAME), COLUMN_NAME, 'IsIdentity') AS EXTRA,
TABLE_NAME,
COLUMN_NAME,
TABLE_CATALOG
FROM
INFORMATION_SCHEMA.COLUMNS
WHERE
COLUMNPROPERTY(object_id(TABLE_NAME), COLUMN_NAME, 'IsIdentity') = 1) AS ac
ON [c].[TABLE_NAME] = [ac].[TABLE_NAME]
AND [c].[TABLE_CATALOG] = [ac].[TABLE_CATALOG]
AND [c].[COLUMN_NAME] = [ac].[COLUMN_NAME]
`
)
.joinRaw(
`
LEFT JOIN (
SELECT
Tab.*,
IS_UNIQUE = CASE
WHEN CONSTRAINT_TYPE = 'UNIQUE'
THEN 'YES'
ELSE NULL
END
FROM
INFORMATION_SCHEMA.TABLE_CONSTRAINTS Tab,
INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE Col
WHERE
Col.Constraint_Name = Tab.Constraint_Name
AND Col.Table_Name = Tab.Table_Name
AND Tab.CONSTRAINT_TYPE = 'UNIQUE'
) AS cu
ON [c].[TABLE_NAME] = [cu].[Table_Name]
AND [c].[COLUMN_NAME] = [cu].[Constraint_Name]
AND [c].[TABLE_CATALOG] =[cu].[TABLE_CATALOG]
`
)
.where({
'c.TABLE_CATALOG': this.knex.client.database(),
Expand All @@ -221,8 +262,10 @@ export default class MSSQL implements SchemaInspector {
precision: rawColumn.NUMERIC_PRECISION,
scale: rawColumn.NUMERIC_SCALE,
is_nullable: rawColumn.IS_NULLABLE === 'YES',
is_unique: rawColumn.IS_UNIQUE === 'YES',
is_primary_key: rawColumn.PK_SET === 'PRIMARY',
has_auto_increment: rawColumn.EXTRA === '1',
has_auto_increment: rawColumn.PK_SET === 'PRIMARY',
// TODO: contraints column name and table name have some issues
foreign_key_column: rawColumn.CONSTRAINT_COLUMN_NAME,
foreign_key_table: rawColumn.CONSTRAINT_TABLE_NAME,
} as Column;
Expand All @@ -241,8 +284,10 @@ export default class MSSQL implements SchemaInspector {
precision: rawColumn.NUMERIC_PRECISION,
scale: rawColumn.NUMERIC_SCALE,
is_nullable: rawColumn.IS_NULLABLE === 'YES',
is_unique: rawColumn.IS_UNIQUE === 'YES',
is_primary_key: rawColumn.PK_SET === 'PRIMARY',
has_auto_increment: rawColumn.EXTRA === '1',
has_auto_increment: rawColumn.PK_SET === 'PRIMARY',
// TODO: contraints column name and table name have some issues
foreign_key_column: rawColumn.CONSTRAINT_COLUMN_NAME,
foreign_key_table: rawColumn.CONSTRAINT_TABLE_NAME,
};
Expand Down Expand Up @@ -277,8 +322,18 @@ export default class MSSQL implements SchemaInspector {
*/
async primary(table: string) {
const results = await this.knex.raw(
`SELECT Col.Column_Name from INFORMATION_SCHEMA.TABLE_CONSTRAINTS Tab, INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE Col WHERE Col.Constraint_Name = Tab.Constraint_Name AND Col.Table_Name = Tab.Table_Name AND Constraint_Type = 'PRIMARY KEY' AND Col.Table_Name = '${table}'`
`SELECT
Col.Column_Name
FROM
INFORMATION_SCHEMA.TABLE_CONSTRAINTS Tab,
INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE Col
WHERE
Col.Constraint_Name = Tab.Constraint_Name
AND Col.Table_Name = Tab.Table_Name
AND Constraint_Type = 'PRIMARY KEY'
AND Col.Table_Name = '${table}'`
);
return results[0]['Column_Name'] as string;
const columnName = results.length > 0 ? results[0]['Column_Name'] : null;
return columnName as string;
}
}
4 changes: 3 additions & 1 deletion lib/dialects/mysql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type RawColumn = {
DELETE_RULE: string | null;

/** @TODO Extend with other possible values */
COLUMN_KEY: 'PRI' | null;
COLUMN_KEY: 'PRI' | 'UNI' | null;
EXTRA: 'auto_increment' | null;
CONSTRAINT_NAME: 'PRIMARY' | null;
};
Expand Down Expand Up @@ -210,6 +210,7 @@ export default class MySQL implements SchemaInspector {
precision: rawColumn.NUMERIC_PRECISION,
scale: rawColumn.NUMERIC_SCALE,
is_nullable: rawColumn.IS_NULLABLE === 'YES',
is_unique: rawColumn.COLUMN_KEY === 'UNI',
is_primary_key: rawColumn.CONSTRAINT_NAME === 'PRIMARY',
has_auto_increment: rawColumn.EXTRA === 'auto_increment',
foreign_key_column: rawColumn.REFERENCED_COLUMN_NAME,
Expand All @@ -233,6 +234,7 @@ export default class MySQL implements SchemaInspector {
precision: rawColumn.NUMERIC_PRECISION,
scale: rawColumn.NUMERIC_SCALE,
is_nullable: rawColumn.IS_NULLABLE === 'YES',
is_unique: rawColumn.COLUMN_KEY === 'UNI',
is_primary_key: rawColumn.CONSTRAINT_NAME === 'PRIMARY',
has_auto_increment: rawColumn.EXTRA === 'auto_increment',
foreign_key_column: rawColumn.REFERENCED_COLUMN_NAME,
Expand Down
4 changes: 3 additions & 1 deletion lib/dialects/oracledb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ type RawColumn = {
DATA_PRECISION: number | null;
DATA_SCALE: number | null;
NULLABLE: 'YES' | 'NO';
UNIQUE: 'YES' | 'NO';
COLUMN_COMMENT: string | null;
REFERENCED_TABLE_NAME: string | null;
REFERENCED_COLUMN_NAME: string | null;
UPDATE_RULE: string | null;
DELETE_RULE: string | null;

/** @TODO Extend with other possible values */
COLUMN_KEY: 'PRI' | null;
COLUMN_KEY: 'PRI' | 'UNI' | null;
CONTRAINT_NAME: string | null;
CONSTRAINT_TYPE: 'P' | null;
};
Expand Down Expand Up @@ -206,6 +207,7 @@ export default class oracleDB implements SchemaInspector {
precision: rawColumn.DATA_PRECISION,
scale: rawColumn.DATA_SCALE,
is_nullable: rawColumn.NULLABLE === 'YES',
is_unique: false,
is_primary_key: rawColumn.CONSTRAINT_TYPE === 'P',
has_auto_increment: rawColumn.DATA_DEFAULT,
foreign_key_column: rawColumn.REFERENCED_COLUMN_NAME,
Expand Down
16 changes: 16 additions & 0 deletions lib/dialects/postgres.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type RawColumn = {
column_default: any | null;
character_maximum_length: number | null;
is_nullable: 'YES' | 'NO';
is_unique: 'YES' | 'NO';
is_primary: null | 'YES';
numeric_precision: null | number;
numeric_scale: null | number;
Expand Down Expand Up @@ -223,6 +224,19 @@ export default class Postgres implements SchemaInspector {
.andWhere(knex.raw('pg_index.indisprimary'))
.as('is_primary'),

knex
.select(knex.raw(`'YES'`))
.from('pg_index')
.join('pg_attribute', function () {
this.on('pg_attribute.attrelid', '=', 'pg_index.indrelid').andOn(
knex.raw('pg_attribute.attnum = any(pg_index.indkey)')
);
})
.whereRaw('pg_index.indrelid = c.table_name::regclass')
.andWhere(knex.raw('pg_attribute.attname = c.column_name'))
.andWhere(knex.raw('pg_index.indisunique'))
.as('is_unique'),

knex
.select(
knex.raw(
Expand Down Expand Up @@ -289,6 +303,7 @@ export default class Postgres implements SchemaInspector {
precision: rawColumn.numeric_precision,
scale: rawColumn.numeric_scale,
is_nullable: rawColumn.is_nullable === 'YES',
is_unique: rawColumn.is_unique === 'YES',
is_primary_key: rawColumn.is_primary === 'YES',
has_auto_increment: rawColumn.serial !== null,
foreign_key_column: rawColumn.referenced_column_name,
Expand All @@ -314,6 +329,7 @@ export default class Postgres implements SchemaInspector {
precision: rawColumn.numeric_precision,
scale: rawColumn.numeric_scale,
is_nullable: rawColumn.is_nullable === 'YES',
is_unique: rawColumn.is_unique === 'YES',
is_primary_key: rawColumn.is_primary === 'YES',
has_auto_increment: rawColumn.serial !== null,
foreign_key_column: rawColumn.referenced_column_name,
Expand Down
16 changes: 12 additions & 4 deletions lib/dialects/sqlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { SchemaInspector } from '../types/schema-inspector';
import { Table } from '../types/table';
import { Column } from '../types/column';
import extractMaxLength from '../utils/extract-max-length';
import extractType from '../utils/extract-type';

type RawColumn = {
cid: number;
name: string;
type: string;
notnull: 0 | 1;
unique: 0 | 1;
dflt_value: any;
pk: 0 | 1;
};
Expand Down Expand Up @@ -123,20 +125,26 @@ export default class SQLite implements SchemaInspector {
{ table: string; from: string; to: string }[]
>(`PRAGMA foreign_key_list(??)`, table);

const indexList = await this.knex.raw<
{ name: string; unique: boolean }[]
>(`PRAGMA index_list(??)`, table);

return columns.map(
(raw): Column => {
const foreignKey = foreignKeys.find((fk) => fk.from === raw.name);
const index = indexList.find((fk) => fk.name === raw.name);

return {
name: raw.name,
table: table,
type: raw.type,
type: extractType(raw.type),
default_value: raw.dflt_value,
max_length: extractMaxLength(raw.dflt_value),
max_length: extractMaxLength(raw.type),
/** @NOTE SQLite3 doesn't support precision/scale */
precision: null,
scale: null,
is_nullable: raw.notnull === 0,
is_unique: !!index?.unique,
is_primary_key: raw.pk === 1,
has_auto_increment:
raw.pk === 1 &&
Expand Down Expand Up @@ -182,12 +190,12 @@ export default class SQLite implements SchemaInspector {
/**
* Get the primary key column for the given table
*/
async primary(table: string): Promise<string> {
async primary(table: string) {
const columns = await this.knex.raw<RawColumn[]>(
`PRAGMA table_info(??)`,
table
);
const pkColumn = columns.find((col) => col.pk !== 0);
return pkColumn!.name;
return pkColumn ? pkColumn.name : null;
}
}
1 change: 1 addition & 0 deletions lib/types/column.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface Column {
scale: number | null;

is_nullable: boolean;
is_unique: boolean;
is_primary_key: boolean;
has_auto_increment: boolean;
foreign_key_column: string | null;
Expand Down
7 changes: 7 additions & 0 deletions lib/utils/extract-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Extracts the type out of a given datatype
* For example: `varchar(32)` => varchar
*/
export default function extractType(type: string): string {
return type.replace(/[^a-zA-Z]/g, '').toLowerCase();
}
Loading

0 comments on commit f6e9fd3

Please sign in to comment.