Include schema package

This commit is contained in:
rijkvanzanten
2020-11-09 11:21:43 -05:00
parent ad56b8b556
commit 427bd8564e
24 changed files with 6457 additions and 1 deletions

Submodule packages/schema deleted from e14f525d53

View File

@@ -0,0 +1,6 @@
root = true
[*]
end_of_line = lf
indent_style = space
indent_size = 2

View File

@@ -0,0 +1,31 @@
name: Tests
on: [push, pull_request]
defaults:
run:
shell: bash
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: docker-compose up -d
- run: npm ci
- 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: npm test

106
packages/schema/.gitignore vendored Normal file
View File

@@ -0,0 +1,106 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
.DS_Store

21
packages/schema/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 knex
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

217
packages/schema/README.md Normal file
View File

@@ -0,0 +1,217 @@
# @directus/schema
Utility for extracting information about the database schema
## Usage
The package is initialized by passing it an instance of Knex:
```ts
import knex from 'knex';
import schema from '@directus/schema';
const database = knex({
client: 'mysql',
connection: {
host: '127.0.0.1',
user: 'your_database_user',
password: 'your_database_password',
database: 'myapp_test',
charset: 'utf8',
},
});
const inspector = schema(database);
export default inspector;
```
## Examples
```ts
import inspector from './inspector';
async function logTables() {
const tables = await inspector.tables();
console.log(tables);
}
```
## API
Note: MySQL doesn't support the `schema` parameter, as schema and database are ambiguous in MySQL.
Note 2: Some database types might return slightly more information than others. See the type files for a specific overview what to expect from driver to driver.
Note 3: MSSQL doesn't support comment for either tables or columns
### Tables
#### `tables(): Promise<string[]>`
Retrieve all tables in the current database.
```ts
await inspector.tables();
// => ['articles', 'images', 'reviews']
```
#### `tableInfo(table?: string): Promise<Table | Table[]>`
Retrieve the table info for the given table, or all tables if no table is specified
```ts
await inspector.tableInfo('articles');
// => {
// name: 'articles',
// schema: 'project',
// comment: 'Informational blog posts'
// }
await inspector.tableInfo();
// => [
// {
// name: 'articles',
// schema: 'project',
// comment: 'Informational blog posts'
// },
// { ... },
// { ... }
// ]
```
#### `hasTable(table: string): Promise<boolean>`
Check if a table exists in the current database.
```ts
await inspector.hasTable('articles');
// => true
```
### Columns
#### `columns(table?: string): Promise<{ table: string, column: string }[]>`
Retrieve all columns in a given table, or all columns if no table is specified
```ts
await inspector.columns();
// => [
// {
// "table": "articles",
// "column": "id"
// },
// {
// "table": "articles",
// "column": "title"
// },
// {
// "table": "images",
// "column": "id"
// }
// ]
await inspector.columns('articles');
// => [
// {
// "table": "articles",
// "column": "id"
// },
// {
// "table": "articles",
// "column": "title"
// }
// ]
```
#### `columnInfo(table?: string, column?: string): Promise<Column[] | Column>`
Retrieve all columns from a given table. Returns all columns if `table` parameter is undefined.
```ts
await inspector.columnInfo('articles');
// => [
// {
// name: "id",
// table: "articles",
// type: "VARCHAR",
// defaultValue: null,
// maxLength: null,
// isNullable: false,
// isPrimaryKey: true,
// hasAutoIncrement: true,
// foreignKeyColumn: null,
// foreignKeyTable: null,
// comment: "Primary key for the articles collection"
// },
// { ... },
// { ... }
// ]
await inspector.columnInfo('articles', 'id');
// => {
// name: "id",
// table: "articles",
// type: "VARCHAR",
// defaultValue: null,
// maxLength: null,
// isNullable: false,
// isPrimaryKey: true,
// hasAutoIncrement: true,
// foreignKeyColumn: null,
// foreignKeyTable: null,
// comment: "Primary key for the articles collection"
// }
```
#### `primary(table: string): Promise<string>`
Retrieve the primary key column for a given table
```ts
await inspector.primary('articles');
// => "id"
```
### Misc.
#### `withSchema(schema: string): void`
_Not supported in MySQL_
Set the schema to use. Note: this is set on the inspector instance and only has to be done once:
```ts
inspector.withSchema('my-schema');
```
## Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
Please make sure to update tests as appropriate.
### Tests
First start docker containers:
```shell
$ docker-compose up -d
```
Then run tests:
```shell
$ npm test
```
Standard mocha filter (grep) can be used:
```shell
$ npm test -- -g '.tableInfo'
```
## License
[MIT](https://choosealicense.com/licenses/mit/)

View File

@@ -0,0 +1,22 @@
version: '3.1'
services:
mysql:
image: mysql:5.7
command: --default-authentication-plugin=mysql_native_password
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: test_db
ports:
- 5100:3306
volumes:
- ./test/seed/mysql.sql:/docker-entrypoint-initdb.d/seed.sql
postgres:
image: postgres:12.3
restart: always
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: test_db
ports:
- 5101:5432
volumes:
- ./test/seed/postgres.sql:/docker-entrypoint-initdb.d/seed.sql

3476
packages/schema/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,51 @@
{
"name": "@directus/schema",
"version": "0.0.25",
"description": "Utility for extracting information about existing DB schema",
"main": "dist/lib/index.js",
"types": "dist/lib/index.d.ts",
"scripts": {
"build": "tsc",
"prepare": "npm run build",
"lint": "prettier --check .",
"test": "npm run lint && ts-mocha test/**/*.spec.ts"
},
"repository": {
"type": "git",
"url": "git+https://github.com/directus/next.git"
},
"keywords": [
"sql",
"knex",
"schema",
"mysql",
"postgresql",
"sqlite3",
"javascript"
],
"author": "Rijk van Zanten <rijkvanzanten@me.com>",
"license": "GPL-3.0",
"bugs": {
"url": "https://github.com/directus/next/issues"
},
"homepage": "https://github.com/directus/next#readme",
"devDependencies": {
"@types/chai": "^4.2.14",
"@types/lodash.flatten": "^4.4.6",
"@types/mocha": "^8.0.3",
"@types/node": "^14.0.13",
"chai": "^4.2.0",
"husky": "^4.2.5",
"knex": "^0.21.1",
"lint-staged": "^10.2.11",
"mocha": "^8.2.0",
"mysql": "^2.18.1",
"pg": "^8.4.0",
"prettier": "^2.0.5",
"ts-mocha": "^7.0.0",
"typescript": "^3.9.5"
},
"dependencies": {
"lodash.flatten": "^4.4.0"
}
}

View File

@@ -0,0 +1,284 @@
import Knex from 'knex';
import { Schema } from '../types/schema-inspector';
import { Table } from '../types/table';
import { Column } from '../types/column';
type RawTable = {
TABLE_NAME: string;
TABLE_SCHEMA: string;
TABLE_CATALOG: string;
};
type RawColumn = {
TABLE_NAME: string;
COLUMN_NAME: string;
COLUMN_DEFAULT: any | null;
DATA_TYPE: string;
CHARACTER_MAXIMUM_LENGTH: number | null;
NUMERIC_PRECISION: number | null;
NUMERIC_SCALE: number | null;
IS_NULLABLE: 'YES' | 'NO';
COLLATION_NAME: string | null;
CONSTRAINT_TABLE_NAME: string | null;
CONSTRAINT_COLUMN_NAME: string | null;
EXTRA: string | null;
UPDATE_RULE: string | null;
DELETE_RULE: string | null;
/** @TODO Extend with other possible values */
COLUMN_KEY: 'PRI' | null;
PK_SET: 'PRIMARY' | null;
};
export default class MSSQL implements Schema {
knex: Knex;
constructor(knex: Knex) {
this.knex = knex;
}
// Tables
// ===============================================================================================
/**
* List all existing tables in the current schema/database
*/
async tables() {
const records = await this.knex
.select<{ TABLE_NAME: string }[]>('TABLE_NAME')
.from('INFORMATION_SCHEMA.TABLES')
.where({
TABLE_TYPE: 'BASE TABLE',
TABLE_CATALOG: this.knex.client.database(),
});
return records.map(({ TABLE_NAME }) => TABLE_NAME);
}
/**
* Get the table info for a given table. If table parameter is undefined, it will return all tables
* in the current schema/database
*/
tableInfo(): Promise<Table[]>;
tableInfo(table: string): Promise<Table>;
async tableInfo<T>(table?: string) {
const query = this.knex
.select('TABLE_NAME', 'TABLE_SCHEMA', 'TABLE_CATALOG', 'TABLE_TYPE')
.from('information_schema.tables')
.where({
TABLE_CATALOG: this.knex.client.database(),
TABLE_TYPE: 'BASE TABLE',
});
if (table) {
const rawTable: RawTable = await query
.andWhere({ table_name: table })
.first();
return {
name: rawTable.TABLE_NAME,
schema: rawTable.TABLE_SCHEMA,
catalog: rawTable.TABLE_CATALOG,
} as T extends string ? Table : Table[];
}
const records: RawTable[] = await query;
return records.map(
(rawTable): Table => {
return {
name: rawTable.TABLE_NAME,
schema: rawTable.TABLE_SCHEMA,
catalog: rawTable.TABLE_CATALOG,
};
}
) as T extends string ? Table : Table[];
}
/**
* Check if a table exists in the current schema/database
*/
async hasTable(table: string): Promise<boolean> {
const result = await this.knex
.count<{ count: 0 | 1 }>({ count: '*' })
.from('information_schema.tables')
.where({ TABLE_CATALOG: this.knex.client.database(), table_name: table })
.first();
return (result && result.count === 1) || false;
}
// Columns
// ===============================================================================================
/**
* Get all the available columns in the current schema/database. Can be filtered to a specific table
*/
async columns(table?: string) {
const query = this.knex
.select<{ TABLE_NAME: string; COLUMN_NAME: string }[]>(
'TABLE_NAME',
'COLUMN_NAME'
)
.from('INFORMATION_SCHEMA.COLUMNS')
.where({ TABLE_CATALOG: this.knex.client.database() });
if (table) {
query.andWhere({ TABLE_NAME: table });
}
const records = await query;
return records.map(({ TABLE_NAME, COLUMN_NAME }) => ({
table: TABLE_NAME,
column: COLUMN_NAME,
}));
}
/**
* Get the column info for all columns, columns in a given table, or a specific column.
*/
columnInfo(): Promise<Column[]>;
columnInfo(table: string): Promise<Column[]>;
columnInfo(table: string, column: string): Promise<Column>;
async columnInfo<T>(table?: string, column?: string) {
const dbName = this.knex.client.database();
const query = this.knex
.select(
'c.TABLE_NAME',
'c.COLUMN_NAME',
'c.COLUMN_DEFAULT',
'c.DATA_TYPE',
'c.CHARACTER_MAXIMUM_LENGTH',
'c.NUMERIC_PRECISION',
'c.NUMERIC_SCALE',
'c.IS_NULLABLE',
'c.COLLATION_NAME',
'pk.CONSTRAINT_TABLE_NAME',
'pk.CONSTRAINT_COLUMN_NAME',
'pk.CONSTRAINT_NAME',
'pk.PK_SET',
'rc.UPDATE_RULE',
'rc.DELETE_RULE',
'rc.MATCH_OPTION'
)
.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
ON [c].[TABLE_NAME] = [pk].[CONSTRAINT_TABLE_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]`
)
.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]
`
)
.where({
'c.TABLE_CATALOG': this.knex.client.database(),
});
if (table) {
query.andWhere({ 'c.TABLE_NAME': table });
}
if (column) {
const rawColumn: RawColumn = await query
.andWhere({ 'c.column_name': column })
.first();
return {
name: rawColumn.COLUMN_NAME,
table: rawColumn.TABLE_NAME,
type: rawColumn.DATA_TYPE,
default_value: parseDefault(rawColumn.COLUMN_DEFAULT),
max_length: rawColumn.CHARACTER_MAXIMUM_LENGTH,
precision: rawColumn.NUMERIC_PRECISION,
scale: rawColumn.NUMERIC_SCALE,
is_nullable: rawColumn.IS_NULLABLE === 'YES',
is_primary_key: rawColumn.PK_SET === 'PRIMARY',
has_auto_increment: rawColumn.EXTRA === '1',
foreign_key_column: rawColumn.CONSTRAINT_COLUMN_NAME,
foreign_key_table: rawColumn.CONSTRAINT_TABLE_NAME,
} as Column;
}
const records: RawColumn[] = await query;
return records.map(
(rawColumn): Column => {
return {
name: rawColumn.COLUMN_NAME,
table: rawColumn.TABLE_NAME,
type: rawColumn.DATA_TYPE,
default_value: parseDefault(rawColumn.COLUMN_DEFAULT),
max_length: rawColumn.CHARACTER_MAXIMUM_LENGTH,
precision: rawColumn.NUMERIC_PRECISION,
scale: rawColumn.NUMERIC_SCALE,
is_nullable: rawColumn.IS_NULLABLE === 'YES',
is_primary_key: rawColumn.PK_SET === 'PRIMARY',
has_auto_increment: rawColumn.EXTRA === '1',
foreign_key_column: rawColumn.CONSTRAINT_COLUMN_NAME,
foreign_key_table: rawColumn.CONSTRAINT_TABLE_NAME,
};
}
) as Column[];
function parseDefault(value: any) {
// MariaDB returns string NULL for not-nullable varchar fields
if (value === 'NULL' || value === 'null') return null;
return value;
}
}
/**
* Check if a table exists in the current schema/database
*/
async hasColumn(table: string, column: string): Promise<boolean> {
const { count } = this.knex
.count<{ count: 0 | 1 }>({ count: '*' })
.from('information_schema.tables')
.where({
TABLE_CATALOG: this.knex.client.database(),
TABLE_NAME: table,
COLUMN_NAME: column,
})
.first();
return !!count;
}
/**
* Get the primary key column for the given table
*/
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}'`
);
return results[0]['Column_Name'] as string;
}
}

View File

@@ -0,0 +1,283 @@
import Knex from 'knex';
import { Schema } from '../types/schema-inspector';
import { Table } from '../types/table';
import { Column } from '../types/column';
type RawTable = {
TABLE_NAME: string;
TABLE_SCHEMA: string;
TABLE_COMMENT: string | null;
ENGINE: string;
TABLE_COLLATION: string;
};
type RawColumn = {
TABLE_NAME: string;
COLUMN_NAME: string;
COLUMN_DEFAULT: any | null;
DATA_TYPE: string;
CHARACTER_MAXIMUM_LENGTH: number | null;
NUMERIC_PRECISION: number | null;
NUMERIC_SCALE: number | null;
IS_NULLABLE: 'YES' | 'NO';
COLLATION_NAME: string | null;
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;
EXTRA: 'auto_increment' | null;
CONSTRAINT_NAME: 'PRIMARY' | null;
};
export default class MySQL implements Schema {
knex: Knex;
constructor(knex: Knex) {
this.knex = knex;
}
// Tables
// ===============================================================================================
/**
* List all existing tables in the current schema/database
*/
async tables() {
const records = await this.knex
.select<{ TABLE_NAME: string }[]>('TABLE_NAME')
.from('INFORMATION_SCHEMA.TABLES')
.where({
TABLE_TYPE: 'BASE TABLE',
TABLE_SCHEMA: this.knex.client.database(),
});
return records.map(({ TABLE_NAME }) => TABLE_NAME);
}
/**
* Get the table info for a given table. If table parameter is undefined, it will return all tables
* in the current schema/database
*/
tableInfo(): Promise<Table[]>;
tableInfo(table: string): Promise<Table>;
async tableInfo<T>(table?: string) {
const query = this.knex
.select(
'TABLE_NAME',
'ENGINE',
'TABLE_SCHEMA',
'TABLE_COLLATION',
'TABLE_COMMENT'
)
.from('information_schema.tables')
.where({
table_schema: this.knex.client.database(),
table_type: 'BASE TABLE',
});
if (table) {
const rawTable: RawTable = await query
.andWhere({ table_name: table })
.first();
return {
name: rawTable.TABLE_NAME,
schema: rawTable.TABLE_SCHEMA,
comment: rawTable.TABLE_COMMENT,
collation: rawTable.TABLE_COLLATION,
engine: rawTable.ENGINE,
} as T extends string ? Table : Table[];
}
const records: RawTable[] = await query;
return records.map(
(rawTable): Table => {
return {
name: rawTable.TABLE_NAME,
schema: rawTable.TABLE_SCHEMA,
comment: rawTable.TABLE_COMMENT,
collation: rawTable.TABLE_COLLATION,
engine: rawTable.ENGINE,
};
}
) as T extends string ? Table : Table[];
}
/**
* Check if a table exists in the current schema/database
*/
async hasTable(table: string): Promise<boolean> {
const result = await this.knex
.count<{ count: 0 | 1 }>({ count: '*' })
.from('information_schema.tables')
.where({ table_schema: this.knex.client.database(), table_name: table })
.first();
return (result && result.count === 1) || false;
}
// Columns
// ===============================================================================================
/**
* Get all the available columns in the current schema/database. Can be filtered to a specific table
*/
async columns(table?: string) {
const query = this.knex
.select<{ TABLE_NAME: string; COLUMN_NAME: string }[]>(
'TABLE_NAME',
'COLUMN_NAME'
)
.from('INFORMATION_SCHEMA.COLUMNS')
.where({ TABLE_SCHEMA: this.knex.client.database() });
if (table) {
query.andWhere({ TABLE_NAME: table });
}
const records = await query;
return records.map(({ TABLE_NAME, COLUMN_NAME }) => ({
table: TABLE_NAME,
column: COLUMN_NAME,
}));
}
/**
* Get the column info for all columns, columns in a given table, or a specific column.
*/
columnInfo(): Promise<Column[]>;
columnInfo(table: string): Promise<Column[]>;
columnInfo(table: string, column: string): Promise<Column>;
async columnInfo<T>(table?: string, column?: string) {
const query = this.knex
.select(
'c.TABLE_NAME',
'c.COLUMN_NAME',
'c.COLUMN_DEFAULT',
'c.DATA_TYPE',
'c.CHARACTER_MAXIMUM_LENGTH',
'c.IS_NULLABLE',
'c.COLUMN_KEY',
'c.EXTRA',
'c.COLLATION_NAME',
'c.COLUMN_COMMENT',
'c.NUMERIC_PRECISION',
'c.NUMERIC_SCALE',
'fk.REFERENCED_TABLE_NAME',
'fk.REFERENCED_COLUMN_NAME',
'fk.CONSTRAINT_NAME',
'rc.UPDATE_RULE',
'rc.DELETE_RULE',
'rc.MATCH_OPTION'
)
.from('INFORMATION_SCHEMA.COLUMNS as c')
.leftJoin('INFORMATION_SCHEMA.KEY_COLUMN_USAGE as fk', function () {
this.on('c.TABLE_NAME', '=', 'fk.TABLE_NAME')
.andOn('fk.COLUMN_NAME', '=', 'c.COLUMN_NAME')
.andOn('fk.CONSTRAINT_SCHEMA', '=', 'c.TABLE_SCHEMA');
})
.leftJoin(
'INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS as rc',
function () {
this.on('rc.TABLE_NAME', '=', 'fk.TABLE_NAME')
.andOn('rc.CONSTRAINT_NAME', '=', 'fk.CONSTRAINT_NAME')
.andOn('rc.CONSTRAINT_SCHEMA', '=', 'fk.CONSTRAINT_SCHEMA');
}
)
.where({
'c.TABLE_SCHEMA': this.knex.client.database(),
});
if (table) {
query.andWhere({ 'c.TABLE_NAME': table });
}
if (column) {
const rawColumn: RawColumn = await query
.andWhere({ 'c.column_name': column })
.first();
return {
name: rawColumn.COLUMN_NAME,
table: rawColumn.TABLE_NAME,
type: rawColumn.DATA_TYPE,
default_value: parseDefault(rawColumn.COLUMN_DEFAULT),
max_length: rawColumn.CHARACTER_MAXIMUM_LENGTH,
precision: rawColumn.NUMERIC_PRECISION,
scale: rawColumn.NUMERIC_SCALE,
is_nullable: rawColumn.IS_NULLABLE === 'YES',
is_primary_key: rawColumn.CONSTRAINT_NAME === 'PRIMARY',
has_auto_increment: rawColumn.EXTRA === 'auto_increment',
foreign_key_column: rawColumn.REFERENCED_COLUMN_NAME,
foreign_key_table: rawColumn.REFERENCED_TABLE_NAME,
comment: rawColumn.COLUMN_COMMENT,
// onDelete: rawColumn.DELETE_RULE,
// onUpdate: rawColumn.UPDATE_RULE,
} as Column;
}
const records: RawColumn[] = await query;
return records.map(
(rawColumn): Column => {
return {
name: rawColumn.COLUMN_NAME,
table: rawColumn.TABLE_NAME,
type: rawColumn.DATA_TYPE,
default_value: parseDefault(rawColumn.COLUMN_DEFAULT),
max_length: rawColumn.CHARACTER_MAXIMUM_LENGTH,
precision: rawColumn.NUMERIC_PRECISION,
scale: rawColumn.NUMERIC_SCALE,
is_nullable: rawColumn.IS_NULLABLE === 'YES',
is_primary_key: rawColumn.CONSTRAINT_NAME === 'PRIMARY',
has_auto_increment: rawColumn.EXTRA === 'auto_increment',
foreign_key_column: rawColumn.REFERENCED_COLUMN_NAME,
foreign_key_table: rawColumn.REFERENCED_TABLE_NAME,
comment: rawColumn.COLUMN_COMMENT,
// onDelete: rawColumn.DELETE_RULE,
// onUpdate: rawColumn.UPDATE_RULE,
};
}
) as Column[];
function parseDefault(value: any) {
// MariaDB returns string NULL for not-nullable varchar fields
if (value === 'NULL' || value === 'null') return null;
return value;
}
}
/**
* Check if a table exists in the current schema/database
*/
async hasColumn(table: string, column: string): Promise<boolean> {
const result = await this.knex
.count<{ count: 0 | 1 }>('*', { as: 'count' })
.from('information_schema.columns')
.where({
table_schema: this.knex.client.database(),
table_name: table,
column_name: column,
})
.first();
return !!(result && result.count);
}
/**
* Get the primary key column for the given table
*/
async primary(table: string) {
const results = await this.knex.raw(
`SHOW KEYS FROM ?? WHERE Key_name = 'PRIMARY'`,
table
);
if (results.length && results[0].length) {
return results[0][0]['Column_name'] as string;
}
return null;
}
}

View File

@@ -0,0 +1,252 @@
import Knex from 'knex';
import { Schema } from '../types/schema-inspector';
import { Table } from '../types/table';
import { Column } from '../types/column';
type RawTable = {
TABLE_NAME: string;
SCHEMA_NAME: string;
};
type RawColumn = {
TABLE_NAME: string;
COLUMN_NAME: string;
DATA_DEFAULT: any | null;
DATA_TYPE: string;
DATA_LENGTH: number | null;
DATA_PRECISION: number | null;
DATA_SCALE: number | null;
NULLABLE: '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;
CONTRAINT_NAME: string | null;
CONSTRAINT_TYPE: 'P' | null;
};
export default class oracleDB implements Schema {
knex: Knex;
constructor(knex: Knex) {
this.knex = knex;
}
// Tables
// ===============================================================================================
/**
* List all existing tables in the current schema/database
*/
async tables() {
const records = await this.knex
.select<{ TABLE_NAME: string }[]>('TABLE_NAME')
.from('DBA_TABLES');
return records.map(({ TABLE_NAME }) => TABLE_NAME);
}
/**
* Get the table info for a given table. If table parameter is undefined, it will return all tables
* in the current schema/database
*/
tableInfo(): Promise<Table[]>;
tableInfo(table: string): Promise<Table>;
async tableInfo<T>(table?: string) {
const query = this.knex.select('TABLE_NAME', 'OWNER').from('DBA_TABLES');
if (table) {
const rawTable: RawTable = await query
.andWhere({ TABLE_NAME: table })
.first();
return {
name: rawTable.TABLE_NAME,
schema: rawTable.SCHEMA_NAME,
} as T extends string ? Table : Table[];
}
const records: RawTable[] = await query;
return records.map(
(rawTable): Table => {
return {
name: rawTable.TABLE_NAME,
schema: rawTable.SCHEMA_NAME,
};
}
) as T extends string ? Table : Table[];
}
/**
* Check if a table exists in the current schema/database
*/
async hasTable(table: string): Promise<boolean> {
const result = await this.knex
.count<{ count: 0 | 1 }>({ count: '*' })
.from('DBA_TABLES')
.where({ TABLE_NAME: table })
.first();
return (result && result.count === 1) || false;
}
// Columns
// ===============================================================================================
/**
* Get all the available columns in the current schema/database. Can be filtered to a specific table
*/
async columns(table?: string) {
const query = this.knex
.select<{ TABLE_NAME: string; COLUMN_NAME: string }[]>(
'TABLE_NAME',
'COLUMN_NAME'
)
.from('DBA_TAB_COLUMNS');
if (table) {
query.andWhere({ TABLE_NAME: table });
}
const records = await query;
return records.map(({ TABLE_NAME, COLUMN_NAME }) => ({
table: TABLE_NAME,
column: COLUMN_NAME,
}));
}
/**
* Get the column info for all columns, columns in a given table, or a specific column.
*/
columnInfo(): Promise<Column[]>;
columnInfo(table: string): Promise<Column[]>;
columnInfo(table: string, column: string): Promise<Column>;
async columnInfo<T>(table?: string, column?: string) {
const query = this.knex
.select(
'c.TABLE_NAME',
'c.COLUMN_NAME',
'c.DATA_DEFAULT',
'c.DATA_TYPE',
'c.DATA_LENGTH',
'c.DATA_PRECISION',
'c.DATA_SCALE',
'c.NULLABLE',
'pk.CONSTRAINT_NAME',
'pk.CONSTRAINT_TYPE',
'cm.COMMENTS AS COLUMN_COMMENT',
'fk.TABLE_NAME as REFERENCE_TABLE_NAME',
'fk.COLUMN_NAME as REFERENCED_COLUMN_NAME',
'rc.DELETE_RULE',
'rc.SEARCH_CONDITION'
)
.from('DBA_TAB_COLUMNS as c')
.leftJoin('DBA_COL_COMMENTS as cm', function () {
this.on('c.TABLE_NAME', '=', 'cm.TABLE_NAME')
.andOn('cm.COLUMN_NAME', '=', 'c.COLUMN_NAME')
.andOn('cm.OWNER', '=', 'c.OWNER');
})
.leftJoin('all_constraints as pk', function () {
this.on('c.TABLE_NAME', '=', 'pk.TABLE_NAME')
.andOn('c.CONSTRAINT_NAME', '=', 'pk.CONSTRAINT_NAME')
.andOn('c.OWNER', '=', 'pk.OWNER');
})
.where({ 'pk.CONSTRAINT_TYPE': 'P' })
.leftJoin('all_constraints as fk', function () {
this.on('c.TABLE_NAME', '=', 'fk.TABLE_NAME')
.andOn('c.CONSTRAINT_NAME', '=', 'fk.CONSTRAINT_NAME')
.andOn('c.OWNER', '=', 'fk.OWNER');
})
.where({ 'fk.CONSTRAINT_TYPE': 'R' })
.leftJoin('all_constraints as rc', function () {
this.on('c.TABLE_NAME', '=', 'rc.TABLE_NAME')
.andOn('c.CONSTRAINT_NAME', '=', 'rc.CONSTRAINT_NAME')
.andOn('c.OWNER', '=', 'rc.OWNER');
});
if (table) {
query.andWhere({ 'c.TABLE_NAME': table });
}
if (column) {
const rawColumn: RawColumn = await query
.andWhere({ 'c.column_name': column })
.first();
return {
name: rawColumn.COLUMN_NAME,
table: rawColumn.TABLE_NAME,
type: rawColumn.DATA_TYPE,
default_value: rawColumn.DATA_DEFAULT,
max_length: rawColumn.DATA_LENGTH,
precision: rawColumn.DATA_PRECISION,
scale: rawColumn.DATA_SCALE,
is_nullable: rawColumn.NULLABLE === 'YES',
is_primary_key: rawColumn.CONSTRAINT_TYPE === 'P',
foreign_key_column: rawColumn.REFERENCED_COLUMN_NAME,
foreign_key_table: rawColumn.REFERENCED_TABLE_NAME,
comment: rawColumn.COLUMN_COMMENT,
} as Column;
}
const records: RawColumn[] = await query;
return records.map(
(rawColumn): Column => {
return {
name: rawColumn.COLUMN_NAME,
table: rawColumn.TABLE_NAME,
type: rawColumn.DATA_TYPE,
default_value: rawColumn.DATA_DEFAULT,
max_length: rawColumn.DATA_DEFAULT,
precision: rawColumn.DATA_PRECISION,
scale: rawColumn.DATA_SCALE,
is_nullable: rawColumn.NULLABLE === 'YES',
is_primary_key: rawColumn.CONSTRAINT_TYPE === 'P',
has_auto_increment: rawColumn.DATA_DEFAULT,
foreign_key_column: rawColumn.REFERENCED_COLUMN_NAME,
foreign_key_table: rawColumn.REFERENCED_TABLE_NAME,
comment: rawColumn.COLUMN_COMMENT,
};
}
) as Column[];
}
/**
* Check if a table exists in the current schema/database
*/
async hasColumn(table: string, column: string): Promise<boolean> {
const { count } = this.knex
.count<{ count: 0 | 1 }>({ count: '*' })
.from('DBA_TAB_COLUMNS')
.where({
table_schema: this.knex.client.database(),
table_name: table,
column_name: column,
})
.first();
return !!count;
}
/**
* Get the primary key column for the given table
*/
async primary(table: string): Promise<string> {
const { column_name } = await this.knex
.select('all_constraints.column_name')
.from('all_constraints')
.where({
'all_constraints.CONSTRAINT_TYPE': 'P',
'all_constraints.TABLE_NAME': table,
})
.first();
return column_name;
}
}

View File

@@ -0,0 +1,372 @@
import Knex from 'knex';
import { Schema } from '../types/schema-inspector';
import { Table } from '../types/table';
import { Column } from '../types/column';
type RawTable = {
table_name: string;
table_schema: 'public' | string;
table_comment: string | null;
};
type RawColumn = {
column_name: string;
table_name: string;
table_schema: string;
data_type: string;
column_default: any | null;
character_maximum_length: number | null;
is_nullable: 'YES' | 'NO';
is_primary: null | 'YES';
numeric_precision: null | number;
numeric_scale: null | number;
serial: null | string;
column_comment: string | null;
referenced_table_schema: null | string;
referenced_table_name: null | string;
referenced_column_name: null | string;
};
export default class Postgres implements Schema {
knex: Knex;
schema: string;
explodedSchema: string[];
constructor(knex: Knex) {
this.knex = knex;
const config = knex.client.config;
if (!config.searchPath) {
this.schema = 'public';
this.explodedSchema = [this.schema];
} else if (typeof config.searchPath === 'string') {
this.schema = config.searchPath;
this.explodedSchema = [config.searchPath];
} else {
this.schema = config.searchPath[0];
this.explodedSchema = config.searchPath;
}
}
// Postgres specific
// ===============================================================================================
/**
* Set the schema to be used in other methods
*/
withSchema(schema: string) {
this.schema = schema;
this.explodedSchema = [this.schema];
return this;
}
/**
* Converts Postgres default value to JS
* Eg `'example'::character varying` => `example`
*/
parseDefaultValue(type: string) {
if (type.startsWith('nextval(')) return null; // auto-increment
const parts = type.split('::');
let value = parts[0];
if (value.startsWith("'") && value.endsWith("'")) {
value = value.slice(1, -1);
}
if (parts[1] && parts[1].includes('json')) return JSON.parse(value);
if (parts[1] && (parts[1].includes('char') || parts[1].includes('text')))
return String(value);
if (Number.isNaN(Number(value))) return value;
return Number(value);
}
// Tables
// ===============================================================================================
/**
* List all existing tables in the current schema/database
*/
async tables() {
const records = await this.knex
.select<{ tablename: string }[]>('tablename')
.from('pg_catalog.pg_tables')
.whereIn('schemaname', this.explodedSchema);
return records.map(({ tablename }) => tablename);
}
/**
* Get the table info for a given table. If table parameter is undefined, it will return all tables
* in the current schema/database
*/
tableInfo(): Promise<Table[]>;
tableInfo(table: string): Promise<Table>;
async tableInfo(table?: string) {
const query = this.knex
.select(
'table_name',
'table_schema',
this.knex
.select(this.knex.raw('obj_description(oid)'))
.from('pg_class')
.where({ relkind: 'r' })
.andWhere({ relname: 'table_name ' })
.as('table_comment')
)
.from('information_schema.tables')
.whereIn('table_schema', this.explodedSchema)
.andWhere({ table_catalog: this.knex.client.database() })
.andWhere({ table_type: 'BASE TABLE' })
.orderBy('table_name', 'asc');
if (table) {
const rawTable: RawTable = await query
.andWhere({ table_name: table })
.limit(1)
.first();
return {
name: rawTable.table_name,
schema: rawTable.table_schema,
comment: rawTable.table_comment,
} as Table;
}
const records = await query;
return records.map(
(rawTable: RawTable): Table => {
return {
name: rawTable.table_name,
schema: rawTable.table_schema,
comment: rawTable.table_comment,
};
}
);
}
/**
* Check if a table exists in the current schema/database
*/
async hasTable(table: string) {
const subquery = this.knex
.select()
.from('information_schema.tables')
.whereIn('table_schema', this.explodedSchema)
.andWhere({ table_name: table });
const record = await this.knex
.select<{ exists: boolean }>(this.knex.raw('exists (?)', [subquery]))
.first();
return record?.exists || false;
}
// Columns
// ===============================================================================================
/**
* Get all the available columns in the current schema/database. Can be filtered to a specific table
*/
async columns(table?: string) {
const query = this.knex
.select<{ table_name: string; column_name: string }[]>(
'table_name',
'column_name'
)
.from('information_schema.columns')
.whereIn('table_schema', this.explodedSchema);
if (table) {
query.andWhere({ table_name: table });
}
const records = await query;
return records.map(({ table_name, column_name }) => ({
table: table_name,
column: column_name,
}));
}
/**
* Get the column info for all columns, columns in a given table, or a specific column.
*/
columnInfo(): Promise<Column[]>;
columnInfo(table: string): Promise<Column[]>;
columnInfo(table: string, column: string): Promise<Column>;
async columnInfo<T>(table?: string, column?: string) {
const { knex } = this;
const query = knex
.select(
'c.column_name',
'c.table_name',
'c.data_type',
'c.column_default',
'c.character_maximum_length',
'c.is_nullable',
'c.numeric_precision',
'c.numeric_scale',
'c.table_schema',
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.indisprimary'))
.as('is_primary'),
knex
.select(
knex.raw(
'pg_catalog.col_description(pg_catalog.pg_class.oid, c.ordinal_position:: int)'
)
)
.from('pg_catalog.pg_class')
.whereRaw(
`pg_catalog.pg_class.oid = (select('"' || c.table_name || '"'):: regclass:: oid)`
)
.andWhere({ 'pg_catalog.pg_class.relname': 'c.table_name' })
.as('column_comment'),
knex.raw(
'pg_get_serial_sequence(c.table_name, c.column_name) as serial'
),
'ffk.referenced_table_schema',
'ffk.referenced_table_name',
'ffk.referenced_column_name'
)
.from(knex.raw('information_schema.columns c'))
.joinRaw(
`
LEFT JOIN (
SELECT
k1.table_schema,
k1.table_name,
k1.column_name,
k2.table_schema AS referenced_table_schema,
k2.table_name AS referenced_table_name,
k2.column_name AS referenced_column_name
FROM
information_schema.key_column_usage k1
JOIN information_schema.referential_constraints fk using (
constraint_schema, constraint_name
)
JOIN information_schema.key_column_usage k2 ON k2.constraint_schema = fk.unique_constraint_schema
AND k2.constraint_name = fk.unique_constraint_name
AND k2.ordinal_position = k1.position_in_unique_constraint
) ffk ON ffk.table_name = c.table_name
AND ffk.column_name = c.column_name
`
)
.whereIn('c.table_schema', this.explodedSchema);
if (table) {
query.andWhere({ 'c.table_name': table });
}
if (column) {
const rawColumn = await query
.andWhere({ 'c.column_name': column })
.first();
return {
name: rawColumn.column_name,
table: rawColumn.table_name,
type: rawColumn.data_type,
default_value: rawColumn.column_default
? this.parseDefaultValue(rawColumn.column_default)
: null,
max_length: rawColumn.character_maximum_length,
precision: rawColumn.numeric_precision,
scale: rawColumn.numeric_scale,
is_nullable: rawColumn.is_nullable === 'YES',
is_primary_key: rawColumn.is_primary === 'YES',
has_auto_increment: rawColumn.serial !== null,
foreign_key_column: rawColumn.referenced_column_name,
foreign_key_table: rawColumn.referenced_table_name,
comment: rawColumn.column_comment,
schema: rawColumn.table_schema,
foreign_key_schema: rawColumn.referenced_table_schema,
} as T extends string ? Column : Column[];
}
const records: RawColumn[] = await query;
return records.map(
(rawColumn): Column => {
return {
name: rawColumn.column_name,
table: rawColumn.table_name,
type: rawColumn.data_type,
default_value: rawColumn.column_default
? this.parseDefaultValue(rawColumn.column_default)
: null,
max_length: rawColumn.character_maximum_length,
precision: rawColumn.numeric_precision,
scale: rawColumn.numeric_scale,
is_nullable: rawColumn.is_nullable === 'YES',
is_primary_key: rawColumn.is_primary === 'YES',
has_auto_increment: rawColumn.serial !== null,
foreign_key_column: rawColumn.referenced_column_name,
foreign_key_table: rawColumn.referenced_table_name,
comment: rawColumn.column_comment,
schema: rawColumn.table_schema,
foreign_key_schema: rawColumn.referenced_table_schema,
};
}
) as T extends string ? Column : Column[];
}
/**
* Check if the given table contains the given column
*/
async hasColumn(table: string, column: string) {
const subquery = this.knex
.select()
.from('information_schema.columns')
.whereIn('table_schema', this.explodedSchema)
.andWhere({
table_name: table,
column_name: column,
});
const record = await this.knex
.select<{ exists: boolean }>(this.knex.raw('exists (?)', [subquery]))
.first();
return record?.exists || false;
}
/**
* Get the primary key column for the given table
*/
async primary(table: string): Promise<string> {
const result = await this.knex
.select('information_schema.key_column_usage.column_name')
.from('information_schema.key_column_usage')
.leftJoin(
'information_schema.table_constraints',
'information_schema.table_constraints.constraint_name',
'information_schema.key_column_usage.constraint_name'
)
.whereIn(
'information_schema.table_constraints.table_schema',
this.explodedSchema
)
.andWhere({
'information_schema.table_constraints.constraint_type': 'PRIMARY KEY',
'information_schema.table_constraints.table_name': table,
})
.first();
return result ? result.column_name : null;
}
}

View File

@@ -0,0 +1,193 @@
import Knex from 'knex';
import flatten from 'lodash.flatten';
import { Schema } from '../types/schema-inspector';
import { Table } from '../types/table';
import { Column } from '../types/column';
import extractMaxLength from '../utils/extract-max-length';
type RawColumn = {
cid: number;
name: string;
type: string;
notnull: 0 | 1;
dflt_value: any;
pk: 0 | 1;
};
export default class SQLite implements Schema {
knex: Knex;
constructor(knex: Knex) {
this.knex = knex;
}
// Tables
// ===============================================================================================
/**
* List all existing tables in the current schema/database
*/
async tables(): Promise<string[]> {
const records = await this.knex
.select('name')
.from('sqlite_master')
.whereRaw(`type = 'table' AND name NOT LIKE 'sqlite_%'`);
return records.map(({ name }) => name) as string[];
}
/**
* Get the table info for a given table. If table parameter is undefined, it will return all tables
* in the current schema/database
*/
tableInfo(): Promise<Table[]>;
tableInfo(table: string): Promise<Table>;
async tableInfo(table?: string) {
const query = this.knex
.select('name', 'sql')
.from('sqlite_master')
.where({ type: 'table' })
.andWhereRaw(`name NOT LIKE 'sqlite_%'`);
if (table) {
query.andWhere({ name: table });
}
let records = await query;
records = records.map((table) => ({
name: table.name,
sql: table.sql,
}));
if (table) {
return records[0];
}
return records;
}
/**
* Check if a table exists in the current schema/database
*/
async hasTable(table: string): Promise<boolean> {
const results = await this.knex
.select(1)
.from('sqlite_master')
.where({ type: 'table', name: table });
return results.length > 0;
}
// Columns
// ===============================================================================================
/**
* Get all the available columns in the current schema/database. Can be filtered to a specific table
*/
async columns(table?: string): Promise<{ table: string; column: string }[]> {
if (table) {
const columns = await this.knex.raw<RawColumn[]>(
`PRAGMA table_info(??)`,
table
);
return columns.map((column) => ({ table, column: column.name }));
}
const tables = await this.tables();
const columnsPerTable = await Promise.all(
tables.map(async (table) => await this.columns(table))
);
return flatten(columnsPerTable);
}
/**
* Get the column info for all columns, columns in a given table, or a specific column.
*/
columnInfo(): Promise<Column[]>;
columnInfo(table: string): Promise<Column[]>;
columnInfo(table: string, column: string): Promise<Column>;
async columnInfo(table?: string, column?: string) {
const getColumnsForTable = async (table: string): Promise<Column[]> => {
const tablesWithAutoIncrementPrimaryKeys = (
await this.knex
.select('name')
.from('sqlite_master')
.whereRaw(`sql LIKE "%AUTOINCREMENT%"`)
).map(({ name }) => name);
const columns: RawColumn[] = await this.knex.raw(
`PRAGMA table_info(??)`,
table
);
const foreignKeys = await this.knex.raw<
{ table: string; from: string; to: string }[]
>(`PRAGMA foreign_key_list(??)`, table);
return columns.map(
(raw): Column => {
const foreignKey = foreignKeys.find((fk) => fk.from === raw.name);
return {
name: raw.name,
table: table,
type: raw.type,
default_value: raw.dflt_value,
max_length: extractMaxLength(raw.dflt_value),
/** @NOTE SQLite3 doesn't support precision/scale */
precision: null,
scale: null,
is_nullable: raw.notnull === 0,
is_primary_key: raw.pk === 1,
has_auto_increment:
raw.pk === 1 &&
tablesWithAutoIncrementPrimaryKeys.includes(table),
foreign_key_column: foreignKey?.to || null,
foreign_key_table: foreignKey?.table || null,
};
}
);
};
if (!table) {
const tables = await this.tables();
const columnsPerTable = await Promise.all(
tables.map(async (table) => await getColumnsForTable(table))
);
return flatten(columnsPerTable);
}
if (table && !column) {
return await getColumnsForTable(table);
}
const columns = await getColumnsForTable(table);
return columns.find((columnInfo) => columnInfo.name === column);
}
/**
* Check if a table exists in the current schema/database
*/
async hasColumn(table: string, column: string): Promise<boolean> {
let isColumn = false;
const results = await this.knex.raw(
`SELECT COUNT(*) AS ct FROM pragma_table_info('${table}') WHERE name='${column}'`
);
const resultsVal = results[0]['ct'];
if (resultsVal !== 0) {
isColumn = true;
}
return isColumn;
}
/**
* Get the primary key column for the given table
*/
async primary(table: string): Promise<string> {
const columns = await this.knex.raw<RawColumn[]>(
`PRAGMA table_info(??)`,
table
);
const pkColumn = columns.find((col) => col.pk !== 0);
return pkColumn!.name;
}
}

View File

@@ -0,0 +1,30 @@
import Knex from 'knex';
import { SchemaConstructor } from './types/schema-inspector';
export default function Schema(knex: Knex) {
let constructor: SchemaConstructor;
switch (knex.client.constructor.name) {
case 'Client_MySQL':
constructor = require('./dialects/mysql').default;
break;
case 'Client_PG':
constructor = require('./dialects/postgres').default;
break;
case 'Client_SQLite3':
constructor = require('./dialects/sqlite').default;
break;
case 'Client_Oracledb':
case 'Client_Oracle':
constructor = require('./dialects/oracledb').default;
break;
case 'Client_MSSQL':
constructor = require('./dialects/mssql').default;
break;
default:
throw Error('Unsupported driver used: ' + knex.client.constructor.name);
}
return new constructor(knex);
}

View File

@@ -0,0 +1,22 @@
export interface Column {
name: string;
table: string;
type: string;
default_value: any | null;
max_length: number | null;
precision: number | null;
scale: number | null;
is_nullable: boolean;
is_primary_key: boolean;
has_auto_increment: boolean;
foreign_key_column: string | null;
foreign_key_table: string | null;
// Not supported in SQLite or MSSQL
comment?: string | null;
// Postgres Only
schema?: string;
foreign_key_schema?: string | null;
}

View File

@@ -0,0 +1,30 @@
import Knex from 'knex';
import { Table } from './table';
import { Column } from './column';
export interface Schema {
knex: Knex;
tables(): Promise<string[]>;
tableInfo(): Promise<Table[]>;
tableInfo(table: string): Promise<Table>;
hasTable(table: string): Promise<boolean>;
columns(table?: string): Promise<{ table: string; column: string }[]>;
columnInfo(): Promise<Column[]>;
columnInfo(table?: string): Promise<Column[]>;
columnInfo(table: string, column: string): Promise<Column>;
hasColumn(table: string, column: string): Promise<boolean>;
primary(table: string): Promise<string | null>;
// Not in MySQL
withSchema?(schema: string): void;
}
export interface SchemaConstructor {
new(knex: Knex): Schema;
}

View File

@@ -0,0 +1,20 @@
export interface Table {
name: string;
// Not supported in SQLite + comment in mssql
comment?: string | null;
schema?: string;
// MySQL Only
collation?: string;
engine?: string;
// Postgres Only
owner?: string;
// SQLite Only
sql?: string;
//MSSQL only
catalog?: string;
}

View File

@@ -0,0 +1,14 @@
/**
* Extracts the length value out of a given datatype
* For example: `varchar(32)` => 32
*/
export default function extractMaxLength(type: string): null | number {
const regex = /\(([^)]+)\)/;
const matches = regex.exec(type);
if (matches && matches.length > 0 && matches[1]) {
return Number(matches[1]);
}
return null;
}

View File

@@ -0,0 +1,436 @@
import Knex from 'knex';
import { expect } from 'chai';
import schemaInspector from '../src';
import { SchemaInspector } from '../src/types/schema-inspector';
describe('mysql', () => {
let database: Knex;
let inspector: SchemaInspector;
before(() => {
database = Knex({
client: 'mysql',
connection: {
host: '127.0.0.1',
port: 5100,
user: 'root',
password: 'secret',
database: 'test_db',
charset: 'utf8',
},
});
inspector = schemaInspector(database);
});
after(async () => {
await database.destroy();
});
describe('.tables', () => {
it('returns tables', async () => {
expect(await inspector.tables()).to.deep.equal([
'page_visits',
'teams',
'users',
]);
});
});
describe('.tableInfo', () => {
it('returns information for all tables', async () => {
expect(await inspector.tableInfo()).to.deep.equal([
{
name: 'page_visits',
schema: 'test_db',
comment: '',
collation: 'latin1_swedish_ci',
engine: 'InnoDB',
},
{
name: 'teams',
schema: 'test_db',
comment: '',
collation: 'latin1_swedish_ci',
engine: 'InnoDB',
},
{
name: 'users',
schema: 'test_db',
comment: '',
collation: 'latin1_swedish_ci',
engine: 'InnoDB',
},
]);
});
it('returns information for specific table', async () => {
expect(await inspector.tableInfo('teams')).to.deep.equal({
collation: 'latin1_swedish_ci',
comment: '',
engine: 'InnoDB',
name: 'teams',
schema: 'test_db',
});
});
});
describe('.hasTable', () => {
it('returns if table exists or not', async () => {
expect(await inspector.hasTable('teams')).to.equal(true);
expect(await inspector.hasTable('foobar')).to.equal(false);
});
});
describe('.columns', () => {
it('returns information for all tables', async () => {
expect(await inspector.columns()).to.deep.equal([
{ table: 'page_visits', column: 'request_path' },
{ table: 'page_visits', column: 'user_agent' },
{ table: 'page_visits', column: 'created_at' },
{ table: 'teams', column: 'id' },
{ table: 'teams', column: 'name' },
{ table: 'teams', column: 'description' },
{ table: 'teams', column: 'credits' },
{ table: 'teams', column: 'created_at' },
{ table: 'teams', column: 'activated_at' },
{ table: 'users', column: 'id' },
{ table: 'users', column: 'team_id' },
{ table: 'users', column: 'email' },
{ table: 'users', column: 'password' },
]);
});
it('returns information for specific table', async () => {
expect(await inspector.columns('teams')).to.deep.equal([
{ column: 'id', table: 'teams' },
{ column: 'name', table: 'teams' },
{ column: 'description', table: 'teams' },
{ column: 'credits', table: 'teams' },
{ column: 'created_at', table: 'teams' },
{ column: 'activated_at', table: 'teams' },
]);
});
});
describe('.columnInfo', () => {
it('returns information for all columns in all tables', async () => {
expect(await inspector.columnInfo()).to.deep.equal([
{
name: 'team_id',
table: 'users',
type: 'int',
default_value: null,
max_length: null,
precision: 10,
scale: 0,
is_nullable: false,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: 'id',
foreign_key_table: 'teams',
comment: '',
},
{
name: 'id',
table: 'teams',
type: 'int',
default_value: null,
max_length: null,
precision: 10,
scale: 0,
is_nullable: false,
is_primary_key: true,
has_auto_increment: true,
foreign_key_column: null,
foreign_key_table: null,
comment: '',
},
{
name: 'id',
table: 'users',
type: 'int',
default_value: null,
max_length: null,
precision: 10,
scale: 0,
is_nullable: false,
is_primary_key: true,
has_auto_increment: true,
foreign_key_column: null,
foreign_key_table: null,
comment: '',
},
{
name: 'request_path',
table: 'page_visits',
type: 'varchar',
default_value: null,
max_length: 100,
precision: null,
scale: null,
is_nullable: true,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: null,
foreign_key_table: null,
comment: '',
},
{
name: 'user_agent',
table: 'page_visits',
type: 'varchar',
default_value: null,
max_length: 200,
precision: null,
scale: null,
is_nullable: true,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: null,
foreign_key_table: null,
comment: '',
},
{
name: 'created_at',
table: 'page_visits',
type: 'datetime',
default_value: null,
max_length: null,
precision: null,
scale: null,
is_nullable: true,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: null,
foreign_key_table: null,
comment: '',
},
{
name: 'name',
table: 'teams',
type: 'varchar',
default_value: null,
max_length: 100,
precision: null,
scale: null,
is_nullable: true,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: null,
foreign_key_table: null,
comment: '',
},
{
name: 'description',
table: 'teams',
type: 'text',
default_value: null,
max_length: 65535,
precision: null,
scale: null,
is_nullable: true,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: null,
foreign_key_table: null,
comment: '',
},
{
name: 'credits',
table: 'teams',
type: 'int',
default_value: null,
max_length: null,
precision: 10,
scale: 0,
is_nullable: true,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: null,
foreign_key_table: null,
comment: 'Remaining usage credits',
},
{
name: 'created_at',
table: 'teams',
type: 'datetime',
default_value: null,
max_length: null,
precision: null,
scale: null,
is_nullable: true,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: null,
foreign_key_table: null,
comment: '',
},
{
name: 'activated_at',
table: 'teams',
type: 'date',
default_value: null,
max_length: null,
precision: null,
scale: null,
is_nullable: true,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: null,
foreign_key_table: null,
comment: '',
},
{
name: 'email',
table: 'users',
type: 'varchar',
default_value: null,
max_length: 100,
precision: null,
scale: null,
is_nullable: true,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: null,
foreign_key_table: null,
comment: '',
},
{
name: 'password',
table: 'users',
type: 'varchar',
default_value: null,
max_length: 60,
precision: null,
scale: null,
is_nullable: true,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: null,
foreign_key_table: null,
comment: '',
},
]);
});
it('returns information for all columns in specific table', async () => {
expect(await inspector.columnInfo('teams')).to.deep.equal([
{
name: 'id',
table: 'teams',
type: 'int',
default_value: null,
max_length: null,
precision: 10,
scale: 0,
is_nullable: false,
is_primary_key: true,
has_auto_increment: true,
foreign_key_column: null,
foreign_key_table: null,
comment: '',
},
{
name: 'name',
table: 'teams',
type: 'varchar',
default_value: null,
max_length: 100,
precision: null,
scale: null,
is_nullable: true,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: null,
foreign_key_table: null,
comment: '',
},
{
name: 'description',
table: 'teams',
type: 'text',
default_value: null,
max_length: 65535,
precision: null,
scale: null,
is_nullable: true,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: null,
foreign_key_table: null,
comment: '',
},
{
name: 'credits',
table: 'teams',
type: 'int',
default_value: null,
max_length: null,
precision: 10,
scale: 0,
is_nullable: true,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: null,
foreign_key_table: null,
comment: 'Remaining usage credits',
},
{
name: 'created_at',
table: 'teams',
type: 'datetime',
default_value: null,
max_length: null,
precision: null,
scale: null,
is_nullable: true,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: null,
foreign_key_table: null,
comment: '',
},
{
name: 'activated_at',
table: 'teams',
type: 'date',
default_value: null,
max_length: null,
precision: null,
scale: null,
is_nullable: true,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: null,
foreign_key_table: null,
comment: '',
},
]);
});
it('returns information for a specific column in a specific table', async () => {
expect(await inspector.columnInfo('teams', 'name')).to.deep.equal({
name: 'name',
table: 'teams',
type: 'varchar',
default_value: null,
max_length: 100,
precision: null,
scale: null,
is_nullable: true,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: null,
foreign_key_table: null,
comment: '',
});
});
});
describe('.primary', () => {
it('returns primary key for a table', async () => {
expect(await inspector.primary('teams')).to.equal('id');
expect(await inspector.primary('page_visits')).to.equal(null);
});
});
});

View File

@@ -0,0 +1,520 @@
import Knex from 'knex';
import { expect } from 'chai';
import schemaInspector from '../src';
import { SchemaInspector } from '../src/types/schema-inspector';
describe('postgres-no-search-path', () => {
let database: Knex;
let inspector: SchemaInspector;
before(() => {
database = Knex({
client: 'pg',
connection: {
host: '127.0.0.1',
port: 5101,
user: 'postgres',
password: 'secret',
database: 'test_db',
charset: 'utf8',
},
});
inspector = schemaInspector(database);
});
after(async () => {
await database.destroy();
});
describe('.tables', () => {
it('returns tables', async () => {
expect(await inspector.tables()).to.deep.equal([
'teams',
'users',
'page_visits',
]);
});
});
describe('.tableInfo', () => {
it('returns information for all tables', async () => {
expect(await inspector.tableInfo()).to.deep.equal([
{ name: 'page_visits', schema: 'public', comment: null },
{ name: 'teams', schema: 'public', comment: null },
{ name: 'users', schema: 'public', comment: null },
]);
});
it('returns information for specific table', async () => {
expect(await inspector.tableInfo('teams')).to.deep.equal({
comment: null,
name: 'teams',
schema: 'public',
});
});
});
describe('.hasTable', () => {
it('returns if table exists or not', async () => {
expect(await inspector.hasTable('teams')).to.equal(true);
expect(await inspector.hasTable('foobar')).to.equal(false);
});
});
describe('.columns', () => {
it('returns information for all tables', async () => {
database.transaction(async (trx) => {
expect(await schemaInspector(trx).columns()).to.deep.equal([
{ table: 'page_visits', column: 'user_agent' },
{ table: 'teams', column: 'name' },
{ table: 'teams', column: 'created_at' },
{ table: 'users', column: 'team_id' },
{ table: 'users', column: 'id' },
{ table: 'users', column: 'password' },
{ table: 'teams', column: 'activated_at' },
{ table: 'users', column: 'email' },
{ table: 'teams', column: 'credits' },
{ table: 'page_visits', column: 'created_at' },
{ table: 'teams', column: 'id' },
{ table: 'teams', column: 'description' },
{ table: 'page_visits', column: 'request_path' },
]);
});
expect(await inspector.columns()).to.deep.equal([
{ table: 'page_visits', column: 'user_agent' },
{ table: 'teams', column: 'name' },
{ table: 'teams', column: 'created_at' },
{ table: 'users', column: 'team_id' },
{ table: 'users', column: 'id' },
{ table: 'users', column: 'password' },
{ table: 'teams', column: 'activated_at' },
{ table: 'users', column: 'email' },
{ table: 'teams', column: 'credits' },
{ table: 'page_visits', column: 'created_at' },
{ table: 'teams', column: 'id' },
{ table: 'teams', column: 'description' },
{ table: 'page_visits', column: 'request_path' },
]);
});
it('returns information for specific table', async () => {
expect(await inspector.columns('teams')).to.deep.equal([
{ table: 'teams', column: 'id' },
{ table: 'teams', column: 'name' },
{ table: 'teams', column: 'description' },
{ table: 'teams', column: 'credits' },
{ table: 'teams', column: 'created_at' },
{ table: 'teams', column: 'activated_at' },
]);
});
});
describe('.columnInfo', () => {
it('returns information for all columns in all tables', async () => {
expect(await inspector.columnInfo()).to.deep.equal([
{
name: 'team_id',
table: 'users',
type: 'integer',
default_value: null,
max_length: null,
precision: 32,
scale: 0,
is_nullable: false,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: 'id',
foreign_key_table: 'teams',
comment: null,
schema: 'public',
foreign_key_schema: 'public',
},
{
name: 'description',
table: 'teams',
type: 'text',
default_value: null,
max_length: null,
precision: null,
scale: null,
is_nullable: true,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: null,
foreign_key_table: null,
comment: null,
schema: 'public',
foreign_key_schema: null,
},
{
name: 'id',
table: 'users',
type: 'integer',
default_value: null,
max_length: null,
precision: 32,
scale: 0,
is_nullable: false,
is_primary_key: true,
has_auto_increment: true,
foreign_key_column: null,
foreign_key_table: null,
comment: null,
schema: 'public',
foreign_key_schema: null,
},
{
name: 'request_path',
table: 'page_visits',
type: 'character varying',
default_value: null,
max_length: 100,
precision: null,
scale: null,
is_nullable: true,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: null,
foreign_key_table: null,
comment: null,
schema: 'public',
foreign_key_schema: null,
},
{
name: 'name',
table: 'teams',
type: 'character varying',
default_value: null,
max_length: 100,
precision: null,
scale: null,
is_nullable: true,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: null,
foreign_key_table: null,
comment: null,
schema: 'public',
foreign_key_schema: null,
},
{
name: 'created_at',
table: 'page_visits',
type: 'timestamp without time zone',
default_value: null,
max_length: null,
precision: null,
scale: null,
is_nullable: true,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: null,
foreign_key_table: null,
comment: null,
schema: 'public',
foreign_key_schema: null,
},
{
name: 'email',
table: 'users',
type: 'character varying',
default_value: null,
max_length: 100,
precision: null,
scale: null,
is_nullable: true,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: null,
foreign_key_table: null,
comment: null,
schema: 'public',
foreign_key_schema: null,
},
{
name: 'password',
table: 'users',
type: 'character varying',
default_value: null,
max_length: 60,
precision: null,
scale: null,
is_nullable: true,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: null,
foreign_key_table: null,
comment: null,
schema: 'public',
foreign_key_schema: null,
},
{
name: 'created_at',
table: 'teams',
type: 'timestamp without time zone',
default_value: null,
max_length: null,
precision: null,
scale: null,
is_nullable: true,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: null,
foreign_key_table: null,
comment: null,
schema: 'public',
foreign_key_schema: null,
},
{
name: 'activated_at',
table: 'teams',
type: 'date',
default_value: null,
max_length: null,
precision: null,
scale: null,
is_nullable: true,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: null,
foreign_key_table: null,
comment: null,
schema: 'public',
foreign_key_schema: null,
},
{
name: 'id',
table: 'teams',
type: 'integer',
default_value: null,
max_length: null,
precision: 32,
scale: 0,
is_nullable: false,
is_primary_key: true,
has_auto_increment: true,
foreign_key_column: null,
foreign_key_table: null,
comment: null,
schema: 'public',
foreign_key_schema: null,
},
{
name: 'user_agent',
table: 'page_visits',
type: 'character varying',
default_value: null,
max_length: 200,
precision: null,
scale: null,
is_nullable: true,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: null,
foreign_key_table: null,
comment: null,
schema: 'public',
foreign_key_schema: null,
},
{
name: 'credits',
table: 'teams',
type: 'integer',
default_value: null,
max_length: null,
precision: 32,
scale: 0,
is_nullable: true,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: null,
foreign_key_table: null,
comment: null,
schema: 'public',
foreign_key_schema: null,
},
]);
});
it('returns information for all columns in specific table', async () => {
expect(await inspector.columnInfo('teams')).to.deep.equal([
{
name: 'id',
table: 'teams',
type: 'integer',
default_value: null,
max_length: null,
precision: 32,
scale: 0,
is_nullable: false,
is_primary_key: true,
has_auto_increment: true,
foreign_key_column: null,
foreign_key_table: null,
comment: null,
schema: 'public',
foreign_key_schema: null,
},
{
name: 'name',
table: 'teams',
type: 'character varying',
default_value: null,
max_length: 100,
precision: null,
scale: null,
is_nullable: true,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: null,
foreign_key_table: null,
comment: null,
schema: 'public',
foreign_key_schema: null,
},
{
name: 'description',
table: 'teams',
type: 'text',
default_value: null,
max_length: null,
precision: null,
scale: null,
is_nullable: true,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: null,
foreign_key_table: null,
comment: null,
schema: 'public',
foreign_key_schema: null,
},
{
name: 'credits',
table: 'teams',
type: 'integer',
default_value: null,
max_length: null,
precision: 32,
scale: 0,
is_nullable: true,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: null,
foreign_key_table: null,
comment: null,
schema: 'public',
foreign_key_schema: null,
},
{
name: 'created_at',
table: 'teams',
type: 'timestamp without time zone',
default_value: null,
max_length: null,
precision: null,
scale: null,
is_nullable: true,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: null,
foreign_key_table: null,
comment: null,
schema: 'public',
foreign_key_schema: null,
},
{
name: 'activated_at',
table: 'teams',
type: 'date',
default_value: null,
max_length: null,
precision: null,
scale: null,
is_nullable: true,
is_primary_key: false,
has_auto_increment: false,
foreign_key_column: null,
foreign_key_table: null,
comment: null,
schema: 'public',
foreign_key_schema: null,
},
]);
});
it('returns information for a specific column in a specific table', async () => {
expect(await inspector.columnInfo('teams', 'name')).to.deep.equal({
schema: 'public',
name: 'name',
table: 'teams',
type: 'character varying',
default_value: null,
max_length: 100,
precision: null,
scale: null,
is_nullable: true,
is_primary_key: false,
has_auto_increment: false,
foreign_key_schema: null,
foreign_key_column: null,
foreign_key_table: null,
comment: null,
});
});
});
describe('.primary', () => {
it('returns primary key for a table', async () => {
expect(await inspector.primary('teams')).to.equal('id');
expect(await inspector.primary('page_visits')).to.equal(null);
});
});
describe('.transaction', () => {
it('works with transactions transaction', async () => {
database.transaction(async (trx) => {
expect(await schemaInspector(trx).primary('teams')).to.equal('id');
});
});
});
});
describe('postgres-with-search-path', () => {
let database: Knex;
let inspector: SchemaInspector;
before(() => {
database = Knex({
searchPath: ['public', 'test'],
client: 'pg',
connection: {
host: '127.0.0.1',
port: 5101,
user: 'postgres',
password: 'secret',
database: 'test_db',
charset: 'utf8',
},
});
inspector = schemaInspector(database);
});
after(async () => {
await database.destroy();
});
describe('.primary', () => {
it('returns primary key for a table', async () => {
expect(await inspector.primary('test')).to.equal('id');
});
});
describe('.transaction', () => {
it('works with transactions transaction', async () => {
database.transaction(async (trx) => {
expect(await schemaInspector(trx).primary('test')).to.equal('id');
});
});
});
});

View File

@@ -0,0 +1,25 @@
create table teams (
id int not null auto_increment primary key,
name varchar(100),
description text,
credits integer(11) comment "Remaining usage credits",
created_at datetime,
activated_at date
);
create table users (
id int not null auto_increment primary key,
team_id int not null,
email varchar(100),
password varchar(60),
constraint fk_team_id
foreign key (team_id)
references teams (id)
);
-- One table without a primary key
create table page_visits (
request_path varchar(100),
user_agent varchar(200),
created_at datetime
);

View File

@@ -0,0 +1,33 @@
create table teams (
id serial primary key,
name varchar(100),
description text,
credits integer,
created_at timestamp,
activated_at date
);
comment on column teams.credits is 'Remaining usage credits';
create table users (
id serial primary key,
team_id int not null,
email varchar(100),
password varchar(60),
constraint fk_team_id
foreign key (team_id)
references teams (id)
);
-- One table without a primary key
create table page_visits (
request_path varchar(100),
user_agent varchar(200),
created_at timestamp
);
-- One table in a schema
create schema test;
create table test.test (
id serial primary key,
number int not null
);

View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"lib": ["es2019", "es2018", "es2017", "es7", "es6", "dom"],
"declaration": true,
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"exclude": ["node_modules", "dist"]
}