Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add is_unique flag for mysql and postgres, partially fixes #20 #31

Merged
merged 26 commits into from
Mar 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ae9ca0e
feat: add is_unique flag for mysql and postgres, partially fixes #20
Feb 22, 2021
ca7e708
fix: fake false is_unique flags for mssql, oracle, sqlite
Feb 22, 2021
fb2b9d7
fix: postgrest test
Feb 22, 2021
514f066
fix: postgrest test
Feb 22, 2021
c65ce44
fix: get test results from github
Feb 22, 2021
f2d34ea
fix: get test results from github
Feb 22, 2021
ea170c9
fix: duplicate query removed
Feb 23, 2021
75e8e43
feat: add mssql tests
Feb 23, 2021
6ea6e3c
feat: add mssql github action
Feb 23, 2021
7d28362
feat: add mssql github action
Feb 23, 2021
e2320c3
fix: stale build canceller
Feb 23, 2021
4ba7c30
feat: sqlite coverrage
Feb 23, 2021
1ba0051
feat: sqlite coverrage
Feb 23, 2021
c8a9a54
feat: sqlite coverage
Feb 23, 2021
e9f08e8
feat: sqlite coverage
Feb 23, 2021
b075fb3
fix: mssql script fixes
Feb 23, 2021
e8ebd03
fix: mssql script fixes
Feb 23, 2021
00a1320
fix: db connectors move devdep, oracle work not successfull
Feb 24, 2021
d388806
Ignore vscode + sqlite db
rijkvanzanten Mar 5, 2021
6b2eee1
Update package-lock to v2
rijkvanzanten Mar 5, 2021
0c5bafa
Remove ignored files from repo
rijkvanzanten Mar 5, 2021
bd2a59a
Don't ignore sqlite test db
rijkvanzanten Mar 5, 2021
e00f40a
Merge branch 'feat/add_is_unique_flag' of github.com:sercanvirlan/kne…
rijkvanzanten Mar 5, 2021
61fa8e0
Re-add sqlite test db
rijkvanzanten Mar 5, 2021
45c3fdb
Add updated note on Oracle test support
rijkvanzanten Mar 5, 2021
4825354
Document attempts for future research
rijkvanzanten Mar 5, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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