chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
**/.DS_Store
|
||||
|
||||
!src
|
||||
!imports-checker
|
||||
!tests
|
||||
!vitest.config.ts
|
||||
!README.md
|
||||
!CONTRIBUTING.md
|
||||
!schema.ts
|
||||
|
||||
!.eslint
|
||||
!.gitignore
|
||||
!package.json
|
||||
!tsconfig.json
|
||||
!tsconfig.cli-types.json
|
||||
!tsconfig.build.json
|
||||
!pnpm-lock.yaml
|
||||
!.github
|
||||
!build.ts
|
||||
!build.dev.ts
|
||||
|
||||
tests/test.ts
|
||||
|
||||
!patches
|
||||
@@ -0,0 +1,79 @@
|
||||
## Drizzle Kit
|
||||
|
||||
Drizzle Kit is a CLI migrator tool for Drizzle ORM. It is probably the one and only tool that lets you completely automatically generate SQL migrations and covers ~95% of the common cases like deletions and renames by prompting user input.
|
||||
<https://github.com/drizzle-team/drizzle-kit-mirror> - is a mirror repository for issues.
|
||||
|
||||
## Documentation
|
||||
|
||||
Check the full documentation on [the website](https://orm.drizzle.team/kit-docs/overview).
|
||||
|
||||
### How it works
|
||||
|
||||
Drizzle Kit traverses a schema module and generates a snapshot to compare with the previous version, if there is one.
|
||||
Based on the difference, it will generate all needed SQL migrations. If there are any cases that can't be resolved automatically, such as renames, it will prompt the user for input.
|
||||
|
||||
For example, for this schema module:
|
||||
|
||||
```typescript
|
||||
// src/db/schema.ts
|
||||
|
||||
import { integer, pgTable, serial, text, varchar } from "drizzle-orm/pg-core";
|
||||
|
||||
const users = pgTable("users", {
|
||||
id: serial("id").primaryKey(),
|
||||
fullName: varchar("full_name", { length: 256 }),
|
||||
}, (table) => ({
|
||||
nameIdx: index("name_idx", table.fullName),
|
||||
})
|
||||
);
|
||||
|
||||
export const authOtp = pgTable("auth_otp", {
|
||||
id: serial("id").primaryKey(),
|
||||
phone: varchar("phone", { length: 256 }),
|
||||
userId: integer("user_id").references(() => users.id),
|
||||
});
|
||||
```
|
||||
|
||||
It will generate:
|
||||
|
||||
```SQL
|
||||
CREATE TABLE IF NOT EXISTS auth_otp (
|
||||
"id" SERIAL PRIMARY KEY,
|
||||
"phone" character varying(256),
|
||||
"user_id" INT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
"id" SERIAL PRIMARY KEY,
|
||||
"full_name" character varying(256)
|
||||
);
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE auth_otp ADD CONSTRAINT auth_otp_user_id_fkey FOREIGN KEY ("user_id") REFERENCES users(id);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS users_full_name_index ON users (full_name);
|
||||
```
|
||||
|
||||
### Installation & configuration
|
||||
|
||||
```shell
|
||||
npm install -D drizzle-kit
|
||||
```
|
||||
|
||||
Running with CLI options:
|
||||
|
||||
```jsonc
|
||||
// package.json
|
||||
{
|
||||
"scripts": {
|
||||
"generate": "drizzle-kit generate --out migrations-folder --schema src/db/schema.ts"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```shell
|
||||
npm run generate
|
||||
```
|
||||
@@ -0,0 +1,51 @@
|
||||
import * as esbuild from 'esbuild';
|
||||
import { cpSync } from 'node:fs';
|
||||
|
||||
const driversPackages = [
|
||||
// postgres drivers
|
||||
'pg',
|
||||
'postgres',
|
||||
'@vercel/postgres',
|
||||
'@neondatabase/serverless',
|
||||
// mysql drivers
|
||||
'mysql2',
|
||||
'@planetscale/database',
|
||||
// sqlite drivers
|
||||
'@libsql/client',
|
||||
'better-sqlite3',
|
||||
];
|
||||
|
||||
esbuild.buildSync({
|
||||
entryPoints: ['./src/utils.ts'],
|
||||
bundle: true,
|
||||
outfile: 'dist/utils.js',
|
||||
format: 'cjs',
|
||||
target: 'node16',
|
||||
platform: 'node',
|
||||
external: ['drizzle-orm', 'esbuild', ...driversPackages],
|
||||
banner: {
|
||||
js: `#!/usr/bin/env -S node --loader @esbuild-kit/esm-loader --no-warnings`,
|
||||
},
|
||||
});
|
||||
|
||||
esbuild.buildSync({
|
||||
entryPoints: ['./src/cli/index.ts'],
|
||||
bundle: true,
|
||||
outfile: 'dist/index.cjs',
|
||||
format: 'cjs',
|
||||
target: 'node16',
|
||||
platform: 'node',
|
||||
external: [
|
||||
'commander',
|
||||
'json-diff',
|
||||
'glob',
|
||||
'esbuild',
|
||||
'drizzle-orm',
|
||||
...driversPackages,
|
||||
],
|
||||
banner: {
|
||||
js: `#!/usr/bin/env -S node --loader ./dist/loader.mjs --no-warnings`,
|
||||
},
|
||||
});
|
||||
|
||||
cpSync('./src/loader.mjs', 'dist/loader.mjs');
|
||||
@@ -0,0 +1,153 @@
|
||||
/// <reference types="bun-types" />
|
||||
import * as esbuild from 'esbuild';
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import * as tsup from 'tsup';
|
||||
import pkg from './package.json';
|
||||
|
||||
const driversPackages = [
|
||||
// postgres drivers
|
||||
'pg',
|
||||
'postgres',
|
||||
'@vercel/postgres',
|
||||
'@neondatabase/serverless',
|
||||
'@electric-sql/pglite',
|
||||
// mysql drivers
|
||||
'mysql2',
|
||||
'@planetscale/database',
|
||||
// sqlite drivers
|
||||
'@libsql/client',
|
||||
'better-sqlite3',
|
||||
'bun:sqlite',
|
||||
];
|
||||
|
||||
esbuild.buildSync({
|
||||
entryPoints: ['./src/utils.ts'],
|
||||
bundle: true,
|
||||
outfile: 'dist/utils.js',
|
||||
format: 'cjs',
|
||||
target: 'node16',
|
||||
platform: 'node',
|
||||
external: [
|
||||
'commander',
|
||||
'json-diff',
|
||||
'glob',
|
||||
'esbuild',
|
||||
'drizzle-orm',
|
||||
...driversPackages,
|
||||
],
|
||||
banner: {
|
||||
js: `#!/usr/bin/env node`,
|
||||
},
|
||||
});
|
||||
|
||||
esbuild.buildSync({
|
||||
entryPoints: ['./src/utils.ts'],
|
||||
bundle: true,
|
||||
outfile: 'dist/utils.mjs',
|
||||
format: 'esm',
|
||||
target: 'node16',
|
||||
platform: 'node',
|
||||
external: [
|
||||
'commander',
|
||||
'json-diff',
|
||||
'glob',
|
||||
'esbuild',
|
||||
'drizzle-orm',
|
||||
...driversPackages,
|
||||
],
|
||||
banner: {
|
||||
js: `#!/usr/bin/env node`,
|
||||
},
|
||||
});
|
||||
|
||||
esbuild.buildSync({
|
||||
entryPoints: ['./src/cli/index.ts'],
|
||||
bundle: true,
|
||||
outfile: 'dist/bin.cjs',
|
||||
format: 'cjs',
|
||||
target: 'node16',
|
||||
platform: 'node',
|
||||
define: {
|
||||
'process.env.DRIZZLE_KIT_VERSION': `"${pkg.version}"`,
|
||||
},
|
||||
external: [
|
||||
'esbuild',
|
||||
'drizzle-orm',
|
||||
...driversPackages,
|
||||
],
|
||||
banner: {
|
||||
js: `#!/usr/bin/env node`,
|
||||
},
|
||||
});
|
||||
|
||||
const main = async () => {
|
||||
await tsup.build({
|
||||
entryPoints: ['./src/index.ts'],
|
||||
outDir: './dist',
|
||||
external: [
|
||||
'esbuild',
|
||||
'drizzle-orm',
|
||||
...driversPackages,
|
||||
],
|
||||
splitting: false,
|
||||
dts: true,
|
||||
format: ['cjs', 'esm'],
|
||||
outExtension: (ctx) => {
|
||||
if (ctx.format === 'cjs') {
|
||||
return {
|
||||
dts: '.d.ts',
|
||||
js: '.js',
|
||||
};
|
||||
}
|
||||
return {
|
||||
dts: '.d.mts',
|
||||
js: '.mjs',
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
await tsup.build({
|
||||
entryPoints: ['./src/api.ts'],
|
||||
outDir: './dist',
|
||||
external: [
|
||||
'esbuild',
|
||||
'drizzle-orm',
|
||||
...driversPackages,
|
||||
],
|
||||
splitting: false,
|
||||
dts: true,
|
||||
format: ['cjs', 'esm'],
|
||||
banner: (ctx) => {
|
||||
/**
|
||||
* fix dynamic require in ESM ("glob" -> "fs.realpath" requires 'fs' module)
|
||||
* @link https://github.com/drizzle-team/drizzle-orm/issues/2853
|
||||
*/
|
||||
if (ctx.format === 'esm') {
|
||||
return {
|
||||
js: "import { createRequire } from 'module'; const require = createRequire(import.meta.url);",
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
outExtension: (ctx) => {
|
||||
if (ctx.format === 'cjs') {
|
||||
return {
|
||||
dts: '.d.ts',
|
||||
js: '.js',
|
||||
};
|
||||
}
|
||||
return {
|
||||
dts: '.d.mts',
|
||||
js: '.mjs',
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const apiCjs = readFileSync('./dist/api.js', 'utf8').replace(/await import\(/g, 'require(');
|
||||
writeFileSync('./dist/api.js', apiCjs);
|
||||
};
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import type { Node } from 'ohm-js';
|
||||
import JSImports from './grammar/grammar.ohm-bundle';
|
||||
|
||||
export type CollectionItem = {
|
||||
type: 'data' | 'types';
|
||||
source: string;
|
||||
};
|
||||
|
||||
function recursiveRun(...args: Node[]): boolean {
|
||||
for (const arg of args) {
|
||||
if (
|
||||
arg.ctorName === 'Rest'
|
||||
|| arg.ctorName === 'comment'
|
||||
|| arg.ctorName === 'stringLiteral'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
arg.ctorName === 'ImportExpr_From'
|
||||
|| arg.ctorName === 'ImportExpr_NoFrom'
|
||||
) {
|
||||
arg['analyze']();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.isTerminal()) continue;
|
||||
|
||||
for (const c of arg.children) {
|
||||
if (!recursiveRun(c)) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
function init(collection: CollectionItem[]) {
|
||||
const semantics = JSImports.createSemantics();
|
||||
|
||||
semantics.addOperation('analyze', {
|
||||
JSImports(arg0, arg1) {
|
||||
recursiveRun(arg0, arg1);
|
||||
},
|
||||
|
||||
ImportExpr_From(kImport, importInner, kFrom, importSource) {
|
||||
const ruleName = importInner.children[0]!.ctorName;
|
||||
const importType = ruleName === 'ImportInner_Type' || ruleName === 'ImportInner_Types'
|
||||
? 'types'
|
||||
: 'data';
|
||||
|
||||
collection.push({
|
||||
source: importSource.children[1]!.sourceString!,
|
||||
type: importType,
|
||||
});
|
||||
},
|
||||
|
||||
ImportExpr_NoFrom(kImport, importSource) {
|
||||
collection.push({
|
||||
source: importSource.children[1]!.sourceString!,
|
||||
type: 'data',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return semantics;
|
||||
}
|
||||
|
||||
export function analyze(path: string) {
|
||||
const file = readFileSync(path).toString();
|
||||
const match = JSImports.match(file, 'JSImports');
|
||||
|
||||
if (match.failed()) throw new Error(`Failed to parse file: ${path}`);
|
||||
const collection: CollectionItem[] = [];
|
||||
|
||||
init(collection)(match)['analyze']();
|
||||
return collection;
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
import fs from 'fs';
|
||||
import m from 'micromatch';
|
||||
import { dirname, join as joinPath, relative, resolve as resolvePath } from 'path';
|
||||
import { analyze } from './analyze';
|
||||
|
||||
type External = {
|
||||
file: string;
|
||||
import: string;
|
||||
type: 'data' | 'types';
|
||||
};
|
||||
|
||||
export type Issue = {
|
||||
file: string;
|
||||
imports: IssueImport[];
|
||||
accessChains: ChainLink[][];
|
||||
};
|
||||
|
||||
export type IssueImport = {
|
||||
name: string;
|
||||
type: 'data' | 'types';
|
||||
};
|
||||
|
||||
export type ChainLink = {
|
||||
file: string;
|
||||
import: string;
|
||||
};
|
||||
|
||||
type ListMode = 'whitelist' | 'blacklist';
|
||||
|
||||
class ImportAnalyzer {
|
||||
private localImportRegex = /^(\.?\.?\/|\.\.?$)/;
|
||||
private importedFileFormatRegex = /^.*\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|json)$/i;
|
||||
|
||||
private visited: Set<string> = new Set<string>();
|
||||
|
||||
private externals: External[] = [];
|
||||
private accessChains: Record<string, ChainLink[][]> = {};
|
||||
|
||||
constructor(
|
||||
private basePath: string,
|
||||
private entry: string,
|
||||
private listMode: ListMode,
|
||||
private readonly wantedList: string[],
|
||||
private localPaths: string[],
|
||||
private logger?: boolean,
|
||||
private ignoreTypes?: boolean,
|
||||
) {}
|
||||
|
||||
private isDirectory = (path: string) => {
|
||||
try {
|
||||
return fs.lstatSync(path).isDirectory();
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
private isFile = (path: string) => {
|
||||
try {
|
||||
return fs.lstatSync(path).isFile();
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
private localizePath = (path: string) => relative(resolvePath(this.basePath), resolvePath(path));
|
||||
|
||||
private isCustomLocal = (importTarget: string) =>
|
||||
!!this.localPaths.find(
|
||||
(l) =>
|
||||
importTarget === l
|
||||
|| importTarget.startsWith(l.endsWith('/') ? l : `${l}/`),
|
||||
);
|
||||
private isLocal = (importTarget: string) =>
|
||||
this.localImportRegex.test(importTarget)
|
||||
|| this.isCustomLocal(importTarget);
|
||||
private isTsFormat = (path: string) => this.importedFileFormatRegex.test(path);
|
||||
|
||||
private resolveCustomLocalPath = (
|
||||
absoluteBase: string,
|
||||
base: string,
|
||||
target: string,
|
||||
): string => {
|
||||
return joinPath(absoluteBase, target);
|
||||
};
|
||||
|
||||
private resolveTargetFile = (path: string): string => {
|
||||
if (this.isFile(path)) return path;
|
||||
|
||||
const formats = [
|
||||
'.ts',
|
||||
'.mts',
|
||||
'.cts',
|
||||
'.tsx',
|
||||
'.js',
|
||||
'.mjs',
|
||||
'.cjs',
|
||||
'.jsx',
|
||||
];
|
||||
|
||||
for (const format of formats) {
|
||||
const indexPath = joinPath(path, `/index${format}`);
|
||||
if (this.isFile(indexPath)) return indexPath;
|
||||
|
||||
const formatFilePath = `${path}${format}`;
|
||||
if (this.isFile(formatFilePath)) return formatFilePath;
|
||||
}
|
||||
|
||||
return path;
|
||||
};
|
||||
|
||||
private resolveTargetPath = (
|
||||
absoluteBase: string,
|
||||
base: string,
|
||||
target: string,
|
||||
): string => {
|
||||
if (this.isCustomLocal(target)) {
|
||||
return this.resolveTargetFile(
|
||||
this.resolveCustomLocalPath(absoluteBase, base, target),
|
||||
);
|
||||
}
|
||||
|
||||
const dir = this.isDirectory(base) ? base : dirname(base);
|
||||
const joined = joinPath(dir, target);
|
||||
|
||||
return this.resolveTargetFile(joined);
|
||||
};
|
||||
|
||||
private _analyzeImports = (
|
||||
target: string = this.entry,
|
||||
basePath: string = this.basePath,
|
||||
accessChain: ChainLink[] = [],
|
||||
) => {
|
||||
if (this.visited.has(target)) return;
|
||||
|
||||
const locals: string[] = [];
|
||||
|
||||
try {
|
||||
if (this.logger) console.log(`${this.localizePath(target)}`);
|
||||
|
||||
const imports = analyze(target);
|
||||
|
||||
for (const { source: i, type } of imports) {
|
||||
if (this.ignoreTypes && type === 'types') continue;
|
||||
|
||||
if (this.isLocal(i)) {
|
||||
locals.push(i);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
this.externals.push({
|
||||
file: this.localizePath(target),
|
||||
import: i,
|
||||
type: type,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
throw e;
|
||||
} finally {
|
||||
this.visited.add(target);
|
||||
}
|
||||
|
||||
for (const local of locals) {
|
||||
const transformedTarget = this.resolveTargetPath(basePath, target, local);
|
||||
|
||||
const localChain = [
|
||||
...accessChain,
|
||||
{
|
||||
file: this.localizePath(target),
|
||||
import: local,
|
||||
},
|
||||
];
|
||||
|
||||
const localized = this.localizePath(transformedTarget);
|
||||
|
||||
if (this.accessChains[localized]) {
|
||||
this.accessChains[localized].push(localChain);
|
||||
} else this.accessChains[localized] = [localChain];
|
||||
|
||||
if (this.isTsFormat(transformedTarget)) {
|
||||
this._analyzeImports(transformedTarget, basePath, localChain);
|
||||
} else {
|
||||
throw new Error(`unrecognized: ${localized}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public analyzeImports = () => {
|
||||
const entryLocalized = this.localizePath(this.entry);
|
||||
if (!this.accessChains[entryLocalized]) {
|
||||
this.accessChains[entryLocalized] = [[]];
|
||||
}
|
||||
|
||||
this._analyzeImports();
|
||||
|
||||
const rawIssues = this.listMode === 'whitelist'
|
||||
? this.externals.filter((e) => !m([e.import], this.wantedList).length)
|
||||
: this.externals.filter((e) => m([e.import], this.wantedList).length);
|
||||
|
||||
const issueMap: Record<string, Issue> = {};
|
||||
for (const { file, import: i, type } of rawIssues) {
|
||||
if (issueMap[file]) {
|
||||
issueMap[file].imports.push({
|
||||
name: i,
|
||||
type,
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
issueMap[file] = {
|
||||
file,
|
||||
imports: [
|
||||
{
|
||||
name: i,
|
||||
type,
|
||||
},
|
||||
],
|
||||
accessChains: this.accessChains[file]!,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
issues: Object.entries(issueMap).map(([file, data]) => {
|
||||
for (const chain of data.accessChains) {
|
||||
chain.push({
|
||||
file,
|
||||
import: '',
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
}),
|
||||
accessChains: this.accessChains,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export type CustomLocalPathResolver = (
|
||||
basePath: string,
|
||||
path: string,
|
||||
target: string,
|
||||
) => string;
|
||||
|
||||
export type AnalyzeImportsConfig =
|
||||
& {
|
||||
basePath: string;
|
||||
entry: string;
|
||||
logger?: boolean;
|
||||
ignoreTypes?: boolean;
|
||||
localPaths?: string[];
|
||||
}
|
||||
& (
|
||||
| {
|
||||
blackList: string[];
|
||||
}
|
||||
| {
|
||||
whiteList: string[];
|
||||
}
|
||||
);
|
||||
|
||||
type AnyAnalyzeImportsConfig = {
|
||||
basePath: string;
|
||||
entry: string;
|
||||
blackList?: string[];
|
||||
whiteList?: string[];
|
||||
logger?: boolean;
|
||||
ignoreTypes?: boolean;
|
||||
localPaths?: string[];
|
||||
};
|
||||
|
||||
export function analyzeImports(cfg: AnalyzeImportsConfig) {
|
||||
const {
|
||||
basePath,
|
||||
blackList,
|
||||
whiteList,
|
||||
entry,
|
||||
localPaths: localImports,
|
||||
ignoreTypes,
|
||||
logger,
|
||||
} = cfg as AnyAnalyzeImportsConfig;
|
||||
const mode = whiteList ? 'whitelist' : 'blacklist';
|
||||
const wantedList = blackList ?? whiteList!;
|
||||
|
||||
const analyzer = new ImportAnalyzer(
|
||||
joinPath(basePath),
|
||||
joinPath(entry),
|
||||
mode,
|
||||
wantedList,
|
||||
localImports ?? [],
|
||||
logger,
|
||||
ignoreTypes,
|
||||
);
|
||||
|
||||
return analyzer.analyzeImports();
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
JSImports {
|
||||
JSImports = (Expr ";"?)*
|
||||
|
||||
Expr =
|
||||
| comment
|
||||
| stringLiteral
|
||||
| ImportExpr
|
||||
| Rest
|
||||
|
||||
ImportExpr =
|
||||
| "import" ImportInner "from" importSource -- From
|
||||
| "import" importSource -- NoFrom
|
||||
|
||||
Rest = (~(ImportExpr | comment | stringLiteral) any)+
|
||||
|
||||
ImportInner =
|
||||
| ("type" "{" NonemptyListOf<ImportExtendedSelectionTypeless, ","> ","? "}") -- Type
|
||||
| ("{" NonemptyListOf<ImportExtendedSelectionTypes, ","> ","? "}") -- Types
|
||||
| ("{" NonemptyListOf<ImportExtendedSelection, ","> ","? "}") -- Extended
|
||||
| (identifier ("," "type"? "{" NonemptyListOf<ImportExtendedSelection, ","> ","? "}")?) -- Mixed
|
||||
| ("*" ("as" identifier)?) -- All
|
||||
| (identifier ("as" identifier)?) -- Default
|
||||
|
||||
|
||||
ImportExtendedSelection = TypeImport | Import
|
||||
ImportExtendedSelectionTypes = TypeImport
|
||||
ImportExtendedSelectionTypeless = Import
|
||||
|
||||
Import = identifier ("as" identifier)?
|
||||
TypeImport = "type" Import ("as" identifier)?
|
||||
|
||||
identifier = letter alnum*
|
||||
quote = "\"" | "'" | "`"
|
||||
notQuote = ~quote any
|
||||
importSource =
|
||||
| "\"" notQuote+ "\""
|
||||
| "'" notQuote+ "'"
|
||||
| "`" notQuote+ "`"
|
||||
|
||||
lineTerminator = "\n" | "\r" | "\u2028" | "\u2029"
|
||||
lineTerminatorSequence = "\n" | "\r" ~"\n" | "\u2028" | "\u2029" | "\r\n"
|
||||
|
||||
comment = multiLineComment | singleLineComment
|
||||
|
||||
multiLineComment = "/*" (~"*/" any)* "*/"
|
||||
singleLineComment = "//" (~lineTerminator any)*
|
||||
|
||||
stringLiteral =
|
||||
| "\"" doubleStringCharacter* "\""
|
||||
| "'" singleStringCharacter* "'"
|
||||
| "`" templateStringCharacter* "`"
|
||||
doubleStringCharacter =
|
||||
| ~("\"" | "\\" | lineTerminator) any -- NonEscaped
|
||||
| "\\" escapeSequence -- Escaped
|
||||
| lineContinuation -- LineContinuation
|
||||
singleStringCharacter =
|
||||
| ~("'" | "\\" | lineTerminator) any -- NonEscaped
|
||||
| "\\" escapeSequence -- Escaped
|
||||
| lineContinuation -- LineContinuation
|
||||
templateStringCharacter =
|
||||
| ~ ("`" | "\\") any -- NonEscaped
|
||||
| "\\" escapeSequence -- Escaped
|
||||
lineContinuation = "\\" lineTerminatorSequence
|
||||
escapeSequence = unicodeEscapeSequence | hexEscapeSequence | octalEscapeSequence | characterEscapeSequence
|
||||
characterEscapeSequence = singleEscapeCharacter | nonEscapeCharacter
|
||||
singleEscapeCharacter = "'" | "\"" | "\\" | "b" | "f" | "n" | "r" | "t" | "v"
|
||||
nonEscapeCharacter = ~(escapeCharacter | lineTerminator) any
|
||||
escapeCharacter = singleEscapeCharacter | decimalDigit | "x" | "u"
|
||||
octalEscapeSequence =
|
||||
| zeroToThree octalDigit octalDigit -- Whole
|
||||
| fourToSeven octalDigit -- EightTimesfourToSeven
|
||||
| zeroToThree octalDigit ~decimalDigit -- EightTimesZeroToThree
|
||||
| octalDigit ~decimalDigit -- Octal
|
||||
hexEscapeSequence = "x" hexDigit hexDigit
|
||||
unicodeEscapeSequence = "u" hexDigit hexDigit hexDigit hexDigit
|
||||
|
||||
zeroToThree = "0".."3"
|
||||
fourToSeven = "4".."7"
|
||||
decimalDigit = "0".."9"
|
||||
nonZeroDigit = "1".."9"
|
||||
octalDigit = "0".."7"
|
||||
|
||||
regularExpressionLiteral = "/" regularExpressionBody "/" regularExpressionFlags
|
||||
regularExpressionBody = regularExpressionFirstChar regularExpressionChar*
|
||||
regularExpressionFirstChar =
|
||||
| ~("*" | "\\" | "/" | "[") regularExpressionNonTerminator
|
||||
| regularExpressionBackslashSequence
|
||||
| regularExpressionClass
|
||||
regularExpressionChar = ~("\\" | "/" | "[") regularExpressionNonTerminator
|
||||
| regularExpressionBackslashSequence
|
||||
| regularExpressionClass
|
||||
regularExpressionBackslashSequence = "\\" regularExpressionNonTerminator
|
||||
regularExpressionNonTerminator = ~(lineTerminator) any
|
||||
regularExpressionClass = "[" regularExpressionClassChar* "]"
|
||||
regularExpressionClassChar =
|
||||
| ~("]" | "\\") regularExpressionNonTerminator
|
||||
| regularExpressionBackslashSequence
|
||||
regularExpressionFlags = identifierPart*
|
||||
|
||||
multiLineCommentNoNL = "/*" (~("*/" | lineTerminator) any)* "*/"
|
||||
|
||||
identifierStart =
|
||||
| letter | "$" | "_"
|
||||
| "\\" unicodeEscapeSequence -- escaped
|
||||
identifierPart =
|
||||
| identifierStart | unicodeCombiningMark
|
||||
| unicodeDigit | unicodeConnectorPunctuation
|
||||
| "\u200C" | "\u200D"
|
||||
letter += unicodeCategoryNl
|
||||
unicodeCategoryNl
|
||||
= "\u2160".."\u2182" | "\u3007" | "\u3021".."\u3029"
|
||||
unicodeDigit (a digit)
|
||||
= "\u0030".."\u0039" | "\u0660".."\u0669" | "\u06F0".."\u06F9" | "\u0966".."\u096F" | "\u09E6".."\u09EF" | "\u0A66".."\u0A6F" | "\u0AE6".."\u0AEF" | "\u0B66".."\u0B6F" | "\u0BE7".."\u0BEF" | "\u0C66".."\u0C6F" | "\u0CE6".."\u0CEF" | "\u0D66".."\u0D6F" | "\u0E50".."\u0E59" | "\u0ED0".."\u0ED9" | "\u0F20".."\u0F29" | "\uFF10".."\uFF19"
|
||||
|
||||
unicodeCombiningMark (a Unicode combining mark)
|
||||
= "\u0300".."\u0345" | "\u0360".."\u0361" | "\u0483".."\u0486" | "\u0591".."\u05A1" | "\u05A3".."\u05B9" | "\u05BB".."\u05BD" | "\u05BF".."\u05BF" | "\u05C1".."\u05C2" | "\u05C4".."\u05C4" | "\u064B".."\u0652" | "\u0670".."\u0670" | "\u06D6".."\u06DC" | "\u06DF".."\u06E4" | "\u06E7".."\u06E8" | "\u06EA".."\u06ED" | "\u0901".."\u0902" | "\u093C".."\u093C" | "\u0941".."\u0948" | "\u094D".."\u094D" | "\u0951".."\u0954" | "\u0962".."\u0963" | "\u0981".."\u0981" | "\u09BC".."\u09BC" | "\u09C1".."\u09C4" | "\u09CD".."\u09CD" | "\u09E2".."\u09E3" | "\u0A02".."\u0A02" | "\u0A3C".."\u0A3C" | "\u0A41".."\u0A42" | "\u0A47".."\u0A48" | "\u0A4B".."\u0A4D" | "\u0A70".."\u0A71" | "\u0A81".."\u0A82" | "\u0ABC".."\u0ABC" | "\u0AC1".."\u0AC5" | "\u0AC7".."\u0AC8" | "\u0ACD".."\u0ACD" | "\u0B01".."\u0B01" | "\u0B3C".."\u0B3C" | "\u0B3F".."\u0B3F" | "\u0B41".."\u0B43" | "\u0B4D".."\u0B4D" | "\u0B56".."\u0B56" | "\u0B82".."\u0B82" | "\u0BC0".."\u0BC0" | "\u0BCD".."\u0BCD" | "\u0C3E".."\u0C40" | "\u0C46".."\u0C48" | "\u0C4A".."\u0C4D" | "\u0C55".."\u0C56" | "\u0CBF".."\u0CBF" | "\u0CC6".."\u0CC6" | "\u0CCC".."\u0CCD" | "\u0D41".."\u0D43" | "\u0D4D".."\u0D4D" | "\u0E31".."\u0E31" | "\u0E34".."\u0E3A" | "\u0E47".."\u0E4E" | "\u0EB1".."\u0EB1" | "\u0EB4".."\u0EB9" | "\u0EBB".."\u0EBC" | "\u0EC8".."\u0ECD" | "\u0F18".."\u0F19" | "\u0F35".."\u0F35" | "\u0F37".."\u0F37" | "\u0F39".."\u0F39" | "\u0F71".."\u0F7E" | "\u0F80".."\u0F84" | "\u0F86".."\u0F87" | "\u0F90".."\u0F95" | "\u0F97".."\u0F97" | "\u0F99".."\u0FAD" | "\u0FB1".."\u0FB7" | "\u0FB9".."\u0FB9" | "\u20D0".."\u20DC" | "\u20E1".."\u20E1" | "\u302A".."\u302F" | "\u3099".."\u309A" | "\uFB1E".."\uFB1E" | "\uFE20".."\uFE23"
|
||||
|
||||
unicodeConnectorPunctuation = "\u005F" | "\u203F".."\u2040" | "\u30FB" | "\uFE33".."\uFE34" | "\uFE4D".."\uFE4F" | "\uFF3F" | "\uFF65"
|
||||
unicodeSpaceSeparator = "\u2000".."\u200B" | "\u3000"
|
||||
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// AUTOGENERATED FILE
|
||||
// This file was generated from grammar.ohm by `ohm generateBundles`.
|
||||
|
||||
import { BaseActionDict, Grammar, IterationNode, Node, NonterminalNode, Semantics, TerminalNode } from 'ohm-js';
|
||||
|
||||
export interface JSImportsActionDict<T> extends BaseActionDict<T> {
|
||||
JSImports?: (this: NonterminalNode, arg0: IterationNode, arg1: IterationNode) => T;
|
||||
Expr?: (this: NonterminalNode, arg0: NonterminalNode) => T;
|
||||
ImportExpr_From?: (
|
||||
this: NonterminalNode,
|
||||
arg0: TerminalNode,
|
||||
arg1: NonterminalNode,
|
||||
arg2: TerminalNode,
|
||||
arg3: NonterminalNode,
|
||||
) => T;
|
||||
ImportExpr_NoFrom?: (this: NonterminalNode, arg0: TerminalNode, arg1: NonterminalNode) => T;
|
||||
ImportExpr?: (this: NonterminalNode, arg0: NonterminalNode) => T;
|
||||
Rest?: (this: NonterminalNode, arg0: IterationNode) => T;
|
||||
ImportInner_Type?: (
|
||||
this: NonterminalNode,
|
||||
arg0: TerminalNode,
|
||||
arg1: TerminalNode,
|
||||
arg2: NonterminalNode,
|
||||
arg3: IterationNode,
|
||||
arg4: TerminalNode,
|
||||
) => T;
|
||||
ImportInner_Types?: (
|
||||
this: NonterminalNode,
|
||||
arg0: TerminalNode,
|
||||
arg1: NonterminalNode,
|
||||
arg2: IterationNode,
|
||||
arg3: TerminalNode,
|
||||
) => T;
|
||||
ImportInner_Extended?: (
|
||||
this: NonterminalNode,
|
||||
arg0: TerminalNode,
|
||||
arg1: NonterminalNode,
|
||||
arg2: IterationNode,
|
||||
arg3: TerminalNode,
|
||||
) => T;
|
||||
ImportInner_Mixed?: (
|
||||
this: NonterminalNode,
|
||||
arg0: NonterminalNode,
|
||||
arg1: IterationNode,
|
||||
arg2: IterationNode,
|
||||
arg3: IterationNode,
|
||||
arg4: IterationNode,
|
||||
arg5: IterationNode,
|
||||
arg6: IterationNode,
|
||||
) => T;
|
||||
ImportInner_All?: (this: NonterminalNode, arg0: TerminalNode, arg1: IterationNode, arg2: IterationNode) => T;
|
||||
ImportInner_Default?: (this: NonterminalNode, arg0: NonterminalNode, arg1: IterationNode, arg2: IterationNode) => T;
|
||||
ImportInner?: (this: NonterminalNode, arg0: NonterminalNode) => T;
|
||||
ImportExtendedSelection?: (this: NonterminalNode, arg0: NonterminalNode) => T;
|
||||
ImportExtendedSelectionTypes?: (this: NonterminalNode, arg0: NonterminalNode) => T;
|
||||
ImportExtendedSelectionTypeless?: (this: NonterminalNode, arg0: NonterminalNode) => T;
|
||||
Import?: (this: NonterminalNode, arg0: NonterminalNode, arg1: IterationNode, arg2: IterationNode) => T;
|
||||
TypeImport?: (
|
||||
this: NonterminalNode,
|
||||
arg0: TerminalNode,
|
||||
arg1: NonterminalNode,
|
||||
arg2: IterationNode,
|
||||
arg3: IterationNode,
|
||||
) => T;
|
||||
identifier?: (this: NonterminalNode, arg0: NonterminalNode, arg1: IterationNode) => T;
|
||||
quote?: (this: NonterminalNode, arg0: TerminalNode) => T;
|
||||
notQuote?: (this: NonterminalNode, arg0: NonterminalNode) => T;
|
||||
importSource?: (this: NonterminalNode, arg0: TerminalNode, arg1: IterationNode, arg2: TerminalNode) => T;
|
||||
lineTerminator?: (this: NonterminalNode, arg0: TerminalNode) => T;
|
||||
lineTerminatorSequence?: (this: NonterminalNode, arg0: TerminalNode) => T;
|
||||
comment?: (this: NonterminalNode, arg0: NonterminalNode) => T;
|
||||
multiLineComment?: (this: NonterminalNode, arg0: TerminalNode, arg1: IterationNode, arg2: TerminalNode) => T;
|
||||
singleLineComment?: (this: NonterminalNode, arg0: TerminalNode, arg1: IterationNode) => T;
|
||||
stringLiteral?: (this: NonterminalNode, arg0: TerminalNode, arg1: IterationNode, arg2: TerminalNode) => T;
|
||||
doubleStringCharacter_NonEscaped?: (this: NonterminalNode, arg0: NonterminalNode) => T;
|
||||
doubleStringCharacter_Escaped?: (this: NonterminalNode, arg0: TerminalNode, arg1: NonterminalNode) => T;
|
||||
doubleStringCharacter_LineContinuation?: (this: NonterminalNode, arg0: NonterminalNode) => T;
|
||||
doubleStringCharacter?: (this: NonterminalNode, arg0: NonterminalNode) => T;
|
||||
singleStringCharacter_NonEscaped?: (this: NonterminalNode, arg0: NonterminalNode) => T;
|
||||
singleStringCharacter_Escaped?: (this: NonterminalNode, arg0: TerminalNode, arg1: NonterminalNode) => T;
|
||||
singleStringCharacter_LineContinuation?: (this: NonterminalNode, arg0: NonterminalNode) => T;
|
||||
singleStringCharacter?: (this: NonterminalNode, arg0: NonterminalNode) => T;
|
||||
templateStringCharacter_NonEscaped?: (this: NonterminalNode, arg0: NonterminalNode) => T;
|
||||
templateStringCharacter_Escaped?: (this: NonterminalNode, arg0: TerminalNode, arg1: NonterminalNode) => T;
|
||||
templateStringCharacter?: (this: NonterminalNode, arg0: NonterminalNode) => T;
|
||||
lineContinuation?: (this: NonterminalNode, arg0: TerminalNode, arg1: NonterminalNode) => T;
|
||||
escapeSequence?: (this: NonterminalNode, arg0: NonterminalNode) => T;
|
||||
characterEscapeSequence?: (this: NonterminalNode, arg0: NonterminalNode) => T;
|
||||
singleEscapeCharacter?: (this: NonterminalNode, arg0: TerminalNode) => T;
|
||||
nonEscapeCharacter?: (this: NonterminalNode, arg0: NonterminalNode) => T;
|
||||
escapeCharacter?: (this: NonterminalNode, arg0: NonterminalNode | TerminalNode) => T;
|
||||
octalEscapeSequence_Whole?: (
|
||||
this: NonterminalNode,
|
||||
arg0: NonterminalNode,
|
||||
arg1: NonterminalNode,
|
||||
arg2: NonterminalNode,
|
||||
) => T;
|
||||
octalEscapeSequence_EightTimesfourToSeven?: (
|
||||
this: NonterminalNode,
|
||||
arg0: NonterminalNode,
|
||||
arg1: NonterminalNode,
|
||||
) => T;
|
||||
octalEscapeSequence_EightTimesZeroToThree?: (
|
||||
this: NonterminalNode,
|
||||
arg0: NonterminalNode,
|
||||
arg1: NonterminalNode,
|
||||
) => T;
|
||||
octalEscapeSequence_Octal?: (this: NonterminalNode, arg0: NonterminalNode) => T;
|
||||
octalEscapeSequence?: (this: NonterminalNode, arg0: NonterminalNode) => T;
|
||||
hexEscapeSequence?: (this: NonterminalNode, arg0: TerminalNode, arg1: NonterminalNode, arg2: NonterminalNode) => T;
|
||||
unicodeEscapeSequence?: (
|
||||
this: NonterminalNode,
|
||||
arg0: TerminalNode,
|
||||
arg1: NonterminalNode,
|
||||
arg2: NonterminalNode,
|
||||
arg3: NonterminalNode,
|
||||
arg4: NonterminalNode,
|
||||
) => T;
|
||||
zeroToThree?: (this: NonterminalNode, arg0: TerminalNode) => T;
|
||||
fourToSeven?: (this: NonterminalNode, arg0: TerminalNode) => T;
|
||||
decimalDigit?: (this: NonterminalNode, arg0: TerminalNode) => T;
|
||||
nonZeroDigit?: (this: NonterminalNode, arg0: TerminalNode) => T;
|
||||
octalDigit?: (this: NonterminalNode, arg0: TerminalNode) => T;
|
||||
regularExpressionLiteral?: (
|
||||
this: NonterminalNode,
|
||||
arg0: TerminalNode,
|
||||
arg1: NonterminalNode,
|
||||
arg2: TerminalNode,
|
||||
arg3: NonterminalNode,
|
||||
) => T;
|
||||
regularExpressionBody?: (this: NonterminalNode, arg0: NonterminalNode, arg1: IterationNode) => T;
|
||||
regularExpressionFirstChar?: (this: NonterminalNode, arg0: NonterminalNode) => T;
|
||||
regularExpressionChar?: (this: NonterminalNode, arg0: NonterminalNode) => T;
|
||||
regularExpressionBackslashSequence?: (this: NonterminalNode, arg0: TerminalNode, arg1: NonterminalNode) => T;
|
||||
regularExpressionNonTerminator?: (this: NonterminalNode, arg0: NonterminalNode) => T;
|
||||
regularExpressionClass?: (this: NonterminalNode, arg0: TerminalNode, arg1: IterationNode, arg2: TerminalNode) => T;
|
||||
regularExpressionClassChar?: (this: NonterminalNode, arg0: NonterminalNode) => T;
|
||||
regularExpressionFlags?: (this: NonterminalNode, arg0: IterationNode) => T;
|
||||
multiLineCommentNoNL?: (this: NonterminalNode, arg0: TerminalNode, arg1: IterationNode, arg2: TerminalNode) => T;
|
||||
identifierStart_escaped?: (this: NonterminalNode, arg0: TerminalNode, arg1: NonterminalNode) => T;
|
||||
identifierStart?: (this: NonterminalNode, arg0: NonterminalNode | TerminalNode) => T;
|
||||
identifierPart?: (this: NonterminalNode, arg0: NonterminalNode | TerminalNode) => T;
|
||||
letter?: (this: NonterminalNode, arg0: NonterminalNode) => T;
|
||||
unicodeCategoryNl?: (this: NonterminalNode, arg0: TerminalNode) => T;
|
||||
unicodeDigit?: (this: NonterminalNode, arg0: TerminalNode) => T;
|
||||
unicodeCombiningMark?: (this: NonterminalNode, arg0: TerminalNode) => T;
|
||||
unicodeConnectorPunctuation?: (this: NonterminalNode, arg0: TerminalNode) => T;
|
||||
unicodeSpaceSeparator?: (this: NonterminalNode, arg0: TerminalNode) => T;
|
||||
}
|
||||
|
||||
export interface JSImportsSemantics extends Semantics {
|
||||
addOperation<T>(name: string, actionDict: JSImportsActionDict<T>): this;
|
||||
extendOperation<T>(name: string, actionDict: JSImportsActionDict<T>): this;
|
||||
addAttribute<T>(name: string, actionDict: JSImportsActionDict<T>): this;
|
||||
extendAttribute<T>(name: string, actionDict: JSImportsActionDict<T>): this;
|
||||
}
|
||||
|
||||
export interface JSImportsGrammar extends Grammar {
|
||||
createSemantics(): JSImportsSemantics;
|
||||
extendSemantics(superSemantics: JSImportsSemantics): JSImportsSemantics;
|
||||
}
|
||||
|
||||
declare const grammar: JSImportsGrammar;
|
||||
export default grammar;
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,48 @@
|
||||
import chalk from 'chalk';
|
||||
import { analyzeImports, ChainLink } from './checker';
|
||||
|
||||
const issues = analyzeImports({
|
||||
basePath: './drizzle-kit',
|
||||
localPaths: ['src'],
|
||||
whiteList: [
|
||||
'@drizzle-team/brocli',
|
||||
'json-diff',
|
||||
'path',
|
||||
'fs',
|
||||
'fs/*',
|
||||
'url',
|
||||
'zod',
|
||||
'node:*',
|
||||
'hono',
|
||||
'glob',
|
||||
'hono/*',
|
||||
'hono/**/*',
|
||||
'@hono/*',
|
||||
'crypto',
|
||||
'hanji',
|
||||
],
|
||||
entry: './drizzle-kit/src/cli/index.ts',
|
||||
logger: true,
|
||||
ignoreTypes: true,
|
||||
}).issues;
|
||||
|
||||
const chainToString = (chains: ChainLink[]) => {
|
||||
if (chains.length === 0) throw new Error();
|
||||
|
||||
let out = chains[0]!.file + '\n';
|
||||
let indentation = 0;
|
||||
for (let chain of chains) {
|
||||
out += ' '.repeat(indentation)
|
||||
+ '└'
|
||||
+ chain.import
|
||||
+ ` ${chalk.gray(chain.file)}\n`;
|
||||
indentation += 1;
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
console.log();
|
||||
for (const issue of issues) {
|
||||
console.log(chalk.red(issue.imports.map((it) => it.name).join('\n')));
|
||||
console.log(issue.accessChains.map((it) => chainToString(it)).join('\n'));
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
{
|
||||
"name": "drizzle-kit",
|
||||
"version": "0.31.10",
|
||||
"homepage": "https://orm.drizzle.team",
|
||||
"keywords": [
|
||||
"drizzle",
|
||||
"orm",
|
||||
"pg",
|
||||
"mysql",
|
||||
"singlestore",
|
||||
"postgresql",
|
||||
"postgres",
|
||||
"sqlite",
|
||||
"database",
|
||||
"sql",
|
||||
"typescript",
|
||||
"ts",
|
||||
"drizzle-kit",
|
||||
"migrations",
|
||||
"schema"
|
||||
],
|
||||
"publishConfig": {
|
||||
"provenance": true
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/drizzle-team/drizzle-orm.git"
|
||||
},
|
||||
"author": "Drizzle Team",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"drizzle-kit": "./bin.cjs"
|
||||
},
|
||||
"scripts": {
|
||||
"api": "tsx ./dev/api.ts",
|
||||
"migrate:old": "drizzle-kit generate:mysql",
|
||||
"cli": "tsx ./src/cli/index.ts",
|
||||
"test": "pnpm tsc && TEST_CONFIG_PATH_PREFIX=./tests/cli/ vitest",
|
||||
"build": "rm -rf ./dist && tsx build.ts && cp package.json dist/ && attw --pack dist",
|
||||
"build:dev": "rm -rf ./dist && tsx build.dev.ts && tsc -p tsconfig.cli-types.json && chmod +x ./dist/index.cjs",
|
||||
"pack": "cp package.json README.md dist/ && (cd dist && npm pack --pack-destination ..) && rm -f package.tgz && mv *.tgz package.tgz",
|
||||
"tsc": "tsc -p tsconfig.build.json --noEmit",
|
||||
"publish": "npm publish package.tgz"
|
||||
},
|
||||
"dependencies": {
|
||||
"@drizzle-team/brocli": "^0.10.2",
|
||||
"@esbuild-kit/esm-loader": "^2.5.5",
|
||||
"esbuild": "^0.25.4",
|
||||
"tsx": "^4.21.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@arethetypeswrong/cli": "^0.15.3",
|
||||
"@aws-sdk/client-rds-data": "^3.556.0",
|
||||
"@cloudflare/workers-types": "^4.20230518.0",
|
||||
"@electric-sql/pglite": "^0.2.12",
|
||||
"@hono/node-server": "^1.9.0",
|
||||
"@hono/zod-validator": "^0.2.1",
|
||||
"@libsql/client": "^0.10.0",
|
||||
"@neondatabase/serverless": "^0.9.1",
|
||||
"@originjs/vite-plugin-commonjs": "^1.0.3",
|
||||
"@planetscale/database": "^1.16.0",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/dockerode": "^3.3.28",
|
||||
"@types/glob": "^8.1.0",
|
||||
"@types/json-diff": "^1.0.3",
|
||||
"@types/micromatch": "^4.0.9",
|
||||
"@types/minimatch": "^5.1.2",
|
||||
"@types/node": "^18.11.15",
|
||||
"@types/pg": "^8.10.7",
|
||||
"@types/pluralize": "^0.0.33",
|
||||
"@types/semver": "^7.5.5",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||
"@typescript-eslint/parser": "^7.2.0",
|
||||
"@vercel/postgres": "^0.8.0",
|
||||
"ava": "^5.1.0",
|
||||
"better-sqlite3": "^11.9.1",
|
||||
"bun-types": "^0.6.6",
|
||||
"camelcase": "^7.0.1",
|
||||
"chalk": "^5.2.0",
|
||||
"commander": "^12.1.0",
|
||||
"dockerode": "^4.0.6",
|
||||
"dotenv": "^16.0.3",
|
||||
"drizzle-kit": "0.25.0-b1faa33",
|
||||
"drizzle-orm": "workspace:./drizzle-orm/dist",
|
||||
"env-paths": "^3.0.0",
|
||||
"esbuild-node-externals": "^1.9.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"gel": "^2.0.0",
|
||||
"get-port": "^6.1.2",
|
||||
"glob": "^8.1.0",
|
||||
"hanji": "^0.0.8",
|
||||
"hono": "^4.7.9",
|
||||
"json-diff": "1.0.6",
|
||||
"micromatch": "^4.0.8",
|
||||
"minimatch": "^7.4.3",
|
||||
"mysql2": "3.14.1",
|
||||
"node-fetch": "^3.3.2",
|
||||
"ohm-js": "^17.1.0",
|
||||
"pg": "^8.11.5",
|
||||
"pluralize": "^8.0.0",
|
||||
"postgres": "^3.4.4",
|
||||
"prettier": "^3.5.3",
|
||||
"semver": "^7.7.2",
|
||||
"superjson": "^2.2.1",
|
||||
"tsup": "^8.3.5",
|
||||
"typescript": "^5.6.3",
|
||||
"uuid": "^9.0.1",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"vitest": "^3.1.3",
|
||||
"ws": "^8.18.2",
|
||||
"zod": "^3.20.2",
|
||||
"zx": "^8.3.2"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./index.d.mts",
|
||||
"default": "./index.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./index.d.ts",
|
||||
"default": "./index.js"
|
||||
},
|
||||
"types": "./index.d.mts",
|
||||
"default": "./index.mjs"
|
||||
},
|
||||
"./api": {
|
||||
"import": {
|
||||
"types": "./api.d.mts",
|
||||
"default": "./api.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./api.d.ts",
|
||||
"default": "./api.js"
|
||||
},
|
||||
"types": "./api.d.mts",
|
||||
"default": "./api.mjs"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
diff --git a/lib/difflib.js b/lib/difflib.js
|
||||
index 80d250e7e18bdc972df3621ee5c05ffff0e3659f..94916f33dbae0d3eea6f74e2c619c4c6f52cc125 100644
|
||||
--- a/lib/difflib.js
|
||||
+++ b/lib/difflib.js
|
||||
@@ -17,7 +17,7 @@ Function restore(delta, which):
|
||||
|
||||
Function unifiedDiff(a, b):
|
||||
For two lists of strings, return a delta in unified diff format.
|
||||
-
|
||||
+.
|
||||
Class SequenceMatcher:
|
||||
A flexible class for comparing pairs of sequences of any type.
|
||||
|
||||
@@ -75,7 +75,7 @@ Class Differ:
|
||||
|
||||
SequenceMatcher = (function() {
|
||||
|
||||
- SequenceMatcher.name = 'SequenceMatcher';
|
||||
+ // SequenceMatcher.name = 'SequenceMatcher';
|
||||
|
||||
/*
|
||||
SequenceMatcher is a flexible class for comparing pairs of sequences of
|
||||
@@ -737,7 +737,7 @@ Class Differ:
|
||||
|
||||
Differ = (function() {
|
||||
|
||||
- Differ.name = 'Differ';
|
||||
+ // Differ.name = 'Differ';
|
||||
|
||||
/*
|
||||
Differ is a class for comparing sequences of lines of text, and
|
||||
@@ -0,0 +1,58 @@
|
||||
declare global {
|
||||
interface String {
|
||||
trimChar(char: string): string;
|
||||
squashSpaces(): string;
|
||||
capitalise(): string;
|
||||
camelCase(): string;
|
||||
snake_case(): string;
|
||||
|
||||
concatIf(it: string, condition: boolean): string;
|
||||
}
|
||||
|
||||
interface Array<T> {
|
||||
random(): T;
|
||||
}
|
||||
}
|
||||
|
||||
import camelcase from 'camelcase';
|
||||
|
||||
String.prototype.trimChar = function(char: string) {
|
||||
let start = 0;
|
||||
let end = this.length;
|
||||
|
||||
while (start < end && this[start] === char) ++start;
|
||||
while (end > start && this[end - 1] === char) --end;
|
||||
|
||||
// this.toString() due to ava deep equal issue with String { "value" }
|
||||
return start > 0 || end < this.length
|
||||
? this.substring(start, end)
|
||||
: this.toString();
|
||||
};
|
||||
|
||||
String.prototype.squashSpaces = function() {
|
||||
return this.replace(/ +/g, ' ').trim();
|
||||
};
|
||||
|
||||
String.prototype.camelCase = function() {
|
||||
return camelcase(String(this));
|
||||
};
|
||||
|
||||
String.prototype.capitalise = function() {
|
||||
return this && this.length > 0
|
||||
? `${this[0].toUpperCase()}${this.slice(1)}`
|
||||
: String(this);
|
||||
};
|
||||
|
||||
String.prototype.concatIf = function(it: string, condition: boolean) {
|
||||
return condition ? `${this}${it}` : String(this);
|
||||
};
|
||||
|
||||
String.prototype.snake_case = function() {
|
||||
return this && this.length > 0 ? `${this.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)}` : String(this);
|
||||
};
|
||||
|
||||
Array.prototype.random = function() {
|
||||
return this[~~(Math.random() * this.length)];
|
||||
};
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,668 @@
|
||||
/// <reference types="@cloudflare/workers-types" />
|
||||
import type { PGlite } from '@electric-sql/pglite';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { is } from 'drizzle-orm';
|
||||
import { LibSQLDatabase } from 'drizzle-orm/libsql';
|
||||
import { AnyMySqlTable, getTableConfig as mysqlTableConfig, MySqlTable } from 'drizzle-orm/mysql-core';
|
||||
import type { MySql2Database } from 'drizzle-orm/mysql2';
|
||||
import { AnyPgTable, getTableConfig as pgTableConfig, PgDatabase, PgTable } from 'drizzle-orm/pg-core';
|
||||
import { Relations } from 'drizzle-orm/relations';
|
||||
import { SingleStoreDriverDatabase } from 'drizzle-orm/singlestore';
|
||||
import {
|
||||
AnySingleStoreTable,
|
||||
getTableConfig as singlestoreTableConfig,
|
||||
SingleStoreTable,
|
||||
} from 'drizzle-orm/singlestore-core';
|
||||
import { AnySQLiteTable, SQLiteTable } from 'drizzle-orm/sqlite-core';
|
||||
import {
|
||||
columnsResolver,
|
||||
enumsResolver,
|
||||
indPolicyResolver,
|
||||
mySqlViewsResolver,
|
||||
policyResolver,
|
||||
roleResolver,
|
||||
schemasResolver,
|
||||
sequencesResolver,
|
||||
sqliteViewsResolver,
|
||||
tablesResolver,
|
||||
viewsResolver,
|
||||
} from './cli/commands/migrate';
|
||||
import { pgPushIntrospect } from './cli/commands/pgIntrospect';
|
||||
import { pgSuggestions } from './cli/commands/pgPushUtils';
|
||||
import { updateUpToV6 as upPgV6, updateUpToV7 as upPgV7 } from './cli/commands/pgUp';
|
||||
import { sqlitePushIntrospect } from './cli/commands/sqliteIntrospect';
|
||||
import { logSuggestionsAndReturn } from './cli/commands/sqlitePushUtils';
|
||||
import type { CasingType } from './cli/validations/common';
|
||||
import type { MysqlCredentials } from './cli/validations/mysql';
|
||||
import type { PostgresCredentials } from './cli/validations/postgres';
|
||||
import type { SingleStoreCredentials } from './cli/validations/singlestore';
|
||||
import type { SqliteCredentials } from './cli/validations/sqlite';
|
||||
import { getTablesFilterByExtensions } from './extensions/getTablesFilterByExtensions';
|
||||
import { originUUID } from './global';
|
||||
import type { Config } from './index';
|
||||
import { MySqlSchema as MySQLSchemaKit, mysqlSchema, squashMysqlScheme } from './serializer/mysqlSchema';
|
||||
import { generateMySqlSnapshot } from './serializer/mysqlSerializer';
|
||||
import { prepareFromExports } from './serializer/pgImports';
|
||||
import { PgSchema as PgSchemaKit, pgSchema, squashPgScheme } from './serializer/pgSchema';
|
||||
import { generatePgSnapshot } from './serializer/pgSerializer';
|
||||
import {
|
||||
SingleStoreSchema as SingleStoreSchemaKit,
|
||||
singlestoreSchema,
|
||||
squashSingleStoreScheme,
|
||||
} from './serializer/singlestoreSchema';
|
||||
import { generateSingleStoreSnapshot } from './serializer/singlestoreSerializer';
|
||||
import { SQLiteSchema as SQLiteSchemaKit, sqliteSchema, squashSqliteScheme } from './serializer/sqliteSchema';
|
||||
import { generateSqliteSnapshot } from './serializer/sqliteSerializer';
|
||||
import type { Setup } from './serializer/studio';
|
||||
import type { DB, SQLiteDB } from './utils';
|
||||
import { certs } from './utils/certs';
|
||||
export type DrizzleSnapshotJSON = PgSchemaKit;
|
||||
export type DrizzleSQLiteSnapshotJSON = SQLiteSchemaKit;
|
||||
export type DrizzleMySQLSnapshotJSON = MySQLSchemaKit;
|
||||
export type DrizzleSingleStoreSnapshotJSON = SingleStoreSchemaKit;
|
||||
|
||||
export const generateDrizzleJson = (
|
||||
imports: Record<string, unknown>,
|
||||
prevId?: string,
|
||||
schemaFilters?: string[],
|
||||
casing?: CasingType,
|
||||
): PgSchemaKit => {
|
||||
const prepared = prepareFromExports(imports);
|
||||
|
||||
const id = randomUUID();
|
||||
|
||||
const snapshot = generatePgSnapshot(
|
||||
prepared.tables,
|
||||
prepared.enums,
|
||||
prepared.schemas,
|
||||
prepared.sequences,
|
||||
prepared.roles,
|
||||
prepared.policies,
|
||||
prepared.views,
|
||||
prepared.matViews,
|
||||
casing,
|
||||
schemaFilters,
|
||||
);
|
||||
|
||||
return {
|
||||
...snapshot,
|
||||
id,
|
||||
prevId: prevId ?? originUUID,
|
||||
};
|
||||
};
|
||||
|
||||
export const generateMigration = async (
|
||||
prev: DrizzleSnapshotJSON,
|
||||
cur: DrizzleSnapshotJSON,
|
||||
) => {
|
||||
const { applyPgSnapshotsDiff } = await import('./snapshotsDiffer');
|
||||
|
||||
const validatedPrev = pgSchema.parse(prev);
|
||||
const validatedCur = pgSchema.parse(cur);
|
||||
|
||||
const squashedPrev = squashPgScheme(validatedPrev);
|
||||
const squashedCur = squashPgScheme(validatedCur);
|
||||
|
||||
const { sqlStatements, _meta } = await applyPgSnapshotsDiff(
|
||||
squashedPrev,
|
||||
squashedCur,
|
||||
schemasResolver,
|
||||
enumsResolver,
|
||||
sequencesResolver,
|
||||
policyResolver,
|
||||
indPolicyResolver,
|
||||
roleResolver,
|
||||
tablesResolver,
|
||||
columnsResolver,
|
||||
viewsResolver,
|
||||
validatedPrev,
|
||||
validatedCur,
|
||||
);
|
||||
|
||||
return sqlStatements;
|
||||
};
|
||||
|
||||
export const pushSchema = async (
|
||||
imports: Record<string, unknown>,
|
||||
drizzleInstance: PgDatabase<any>,
|
||||
schemaFilters?: string[],
|
||||
tablesFilter?: string[],
|
||||
extensionsFilters?: Config['extensionsFilters'],
|
||||
) => {
|
||||
const { applyPgSnapshotsDiff } = await import('./snapshotsDiffer');
|
||||
const { sql } = await import('drizzle-orm');
|
||||
const filters = (tablesFilter ?? []).concat(
|
||||
getTablesFilterByExtensions({ extensionsFilters, dialect: 'postgresql' }),
|
||||
);
|
||||
|
||||
const db: DB = {
|
||||
query: async (query: string, params?: any[]) => {
|
||||
const res = await drizzleInstance.execute(sql.raw(query));
|
||||
return res.rows;
|
||||
},
|
||||
};
|
||||
|
||||
const cur = generateDrizzleJson(imports);
|
||||
const { schema: prev } = await pgPushIntrospect(
|
||||
db,
|
||||
filters,
|
||||
schemaFilters ?? ['public'],
|
||||
undefined,
|
||||
);
|
||||
|
||||
const validatedPrev = pgSchema.parse(prev);
|
||||
const validatedCur = pgSchema.parse(cur);
|
||||
|
||||
const squashedPrev = squashPgScheme(validatedPrev, 'push');
|
||||
const squashedCur = squashPgScheme(validatedCur, 'push');
|
||||
|
||||
const { statements } = await applyPgSnapshotsDiff(
|
||||
squashedPrev,
|
||||
squashedCur,
|
||||
schemasResolver,
|
||||
enumsResolver,
|
||||
sequencesResolver,
|
||||
policyResolver,
|
||||
indPolicyResolver,
|
||||
roleResolver,
|
||||
tablesResolver,
|
||||
columnsResolver,
|
||||
viewsResolver,
|
||||
validatedPrev,
|
||||
validatedCur,
|
||||
'push',
|
||||
);
|
||||
|
||||
const { shouldAskForApprove, statementsToExecute, infoToPrint } = await pgSuggestions(db, statements);
|
||||
|
||||
return {
|
||||
hasDataLoss: shouldAskForApprove,
|
||||
warnings: infoToPrint,
|
||||
statementsToExecute,
|
||||
apply: async () => {
|
||||
for (const dStmnt of statementsToExecute) {
|
||||
await db.query(dStmnt);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const startStudioPostgresServer = async (
|
||||
imports: Record<string, unknown>,
|
||||
credentials: PostgresCredentials | {
|
||||
driver: 'pglite';
|
||||
client: PGlite;
|
||||
},
|
||||
options?: {
|
||||
host?: string;
|
||||
port?: number;
|
||||
casing?: CasingType;
|
||||
},
|
||||
) => {
|
||||
const { drizzleForPostgres } = await import('./serializer/studio');
|
||||
|
||||
const pgSchema: Record<string, Record<string, AnyPgTable>> = {};
|
||||
const relations: Record<string, Relations> = {};
|
||||
|
||||
Object.entries(imports).forEach(([k, t]) => {
|
||||
if (is(t, PgTable)) {
|
||||
const schema = pgTableConfig(t).schema || 'public';
|
||||
pgSchema[schema] = pgSchema[schema] || {};
|
||||
pgSchema[schema][k] = t;
|
||||
}
|
||||
|
||||
if (is(t, Relations)) {
|
||||
relations[k] = t;
|
||||
}
|
||||
});
|
||||
|
||||
const setup = await drizzleForPostgres(credentials, pgSchema, relations, [], options?.casing);
|
||||
await startServerFromSetup(setup, options);
|
||||
};
|
||||
|
||||
// SQLite
|
||||
|
||||
export const generateSQLiteDrizzleJson = async (
|
||||
imports: Record<string, unknown>,
|
||||
prevId?: string,
|
||||
casing?: CasingType,
|
||||
): Promise<SQLiteSchemaKit> => {
|
||||
const { prepareFromExports } = await import('./serializer/sqliteImports');
|
||||
|
||||
const prepared = prepareFromExports(imports);
|
||||
|
||||
const id = randomUUID();
|
||||
|
||||
const snapshot = generateSqliteSnapshot(prepared.tables, prepared.views, casing);
|
||||
|
||||
return {
|
||||
...snapshot,
|
||||
id,
|
||||
prevId: prevId ?? originUUID,
|
||||
};
|
||||
};
|
||||
|
||||
export const generateSQLiteMigration = async (
|
||||
prev: DrizzleSQLiteSnapshotJSON,
|
||||
cur: DrizzleSQLiteSnapshotJSON,
|
||||
) => {
|
||||
const { applySqliteSnapshotsDiff } = await import('./snapshotsDiffer');
|
||||
|
||||
const validatedPrev = sqliteSchema.parse(prev);
|
||||
const validatedCur = sqliteSchema.parse(cur);
|
||||
|
||||
const squashedPrev = squashSqliteScheme(validatedPrev);
|
||||
const squashedCur = squashSqliteScheme(validatedCur);
|
||||
|
||||
const { sqlStatements } = await applySqliteSnapshotsDiff(
|
||||
squashedPrev,
|
||||
squashedCur,
|
||||
tablesResolver,
|
||||
columnsResolver,
|
||||
sqliteViewsResolver,
|
||||
validatedPrev,
|
||||
validatedCur,
|
||||
);
|
||||
|
||||
return sqlStatements;
|
||||
};
|
||||
|
||||
export const pushSQLiteSchema = async (
|
||||
imports: Record<string, unknown>,
|
||||
drizzleInstance: LibSQLDatabase<any>,
|
||||
) => {
|
||||
const { applySqliteSnapshotsDiff } = await import('./snapshotsDiffer');
|
||||
const { sql } = await import('drizzle-orm');
|
||||
|
||||
const db: SQLiteDB = {
|
||||
query: async (query: string, params?: any[]) => {
|
||||
const res = drizzleInstance.all<any>(sql.raw(query));
|
||||
return res;
|
||||
},
|
||||
run: async (query: string) => {
|
||||
return Promise.resolve(drizzleInstance.run(sql.raw(query))).then(
|
||||
() => {},
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const cur = await generateSQLiteDrizzleJson(imports);
|
||||
const { schema: prev } = await sqlitePushIntrospect(db, []);
|
||||
|
||||
const validatedPrev = sqliteSchema.parse(prev);
|
||||
const validatedCur = sqliteSchema.parse(cur);
|
||||
|
||||
const squashedPrev = squashSqliteScheme(validatedPrev, 'push');
|
||||
const squashedCur = squashSqliteScheme(validatedCur, 'push');
|
||||
|
||||
const { statements, _meta } = await applySqliteSnapshotsDiff(
|
||||
squashedPrev,
|
||||
squashedCur,
|
||||
tablesResolver,
|
||||
columnsResolver,
|
||||
sqliteViewsResolver,
|
||||
validatedPrev,
|
||||
validatedCur,
|
||||
'push',
|
||||
);
|
||||
|
||||
const { shouldAskForApprove, statementsToExecute, infoToPrint } = await logSuggestionsAndReturn(
|
||||
db,
|
||||
statements,
|
||||
squashedPrev,
|
||||
squashedCur,
|
||||
_meta!,
|
||||
);
|
||||
|
||||
return {
|
||||
hasDataLoss: shouldAskForApprove,
|
||||
warnings: infoToPrint,
|
||||
statementsToExecute,
|
||||
apply: async () => {
|
||||
for (const dStmnt of statementsToExecute) {
|
||||
await db.query(dStmnt);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const startStudioSQLiteServer = async (
|
||||
imports: Record<string, unknown>,
|
||||
credentials: SqliteCredentials | {
|
||||
driver: 'd1';
|
||||
binding: D1Database;
|
||||
},
|
||||
options?: {
|
||||
host?: string;
|
||||
port?: number;
|
||||
casing?: CasingType;
|
||||
},
|
||||
) => {
|
||||
const { drizzleForSQLite } = await import('./serializer/studio');
|
||||
|
||||
const sqliteSchema: Record<string, Record<string, AnySQLiteTable>> = {};
|
||||
const relations: Record<string, Relations> = {};
|
||||
|
||||
Object.entries(imports).forEach(([k, t]) => {
|
||||
if (is(t, SQLiteTable)) {
|
||||
const schema = 'public'; // sqlite does not have schemas
|
||||
sqliteSchema[schema] = sqliteSchema[schema] || {};
|
||||
sqliteSchema[schema][k] = t;
|
||||
}
|
||||
|
||||
if (is(t, Relations)) {
|
||||
relations[k] = t;
|
||||
}
|
||||
});
|
||||
|
||||
const setup = await drizzleForSQLite(credentials, sqliteSchema, relations, [], options?.casing);
|
||||
await startServerFromSetup(setup, options);
|
||||
};
|
||||
|
||||
// MySQL
|
||||
|
||||
export const generateMySQLDrizzleJson = async (
|
||||
imports: Record<string, unknown>,
|
||||
prevId?: string,
|
||||
casing?: CasingType,
|
||||
): Promise<MySQLSchemaKit> => {
|
||||
const { prepareFromExports } = await import('./serializer/mysqlImports');
|
||||
|
||||
const prepared = prepareFromExports(imports);
|
||||
|
||||
const id = randomUUID();
|
||||
|
||||
const snapshot = generateMySqlSnapshot(prepared.tables, prepared.views, casing);
|
||||
|
||||
return {
|
||||
...snapshot,
|
||||
id,
|
||||
prevId: prevId ?? originUUID,
|
||||
};
|
||||
};
|
||||
|
||||
export const generateMySQLMigration = async (
|
||||
prev: DrizzleMySQLSnapshotJSON,
|
||||
cur: DrizzleMySQLSnapshotJSON,
|
||||
) => {
|
||||
const { applyMysqlSnapshotsDiff } = await import('./snapshotsDiffer');
|
||||
|
||||
const validatedPrev = mysqlSchema.parse(prev);
|
||||
const validatedCur = mysqlSchema.parse(cur);
|
||||
|
||||
const squashedPrev = squashMysqlScheme(validatedPrev);
|
||||
const squashedCur = squashMysqlScheme(validatedCur);
|
||||
|
||||
const { sqlStatements } = await applyMysqlSnapshotsDiff(
|
||||
squashedPrev,
|
||||
squashedCur,
|
||||
tablesResolver,
|
||||
columnsResolver,
|
||||
mySqlViewsResolver,
|
||||
validatedPrev,
|
||||
validatedCur,
|
||||
);
|
||||
|
||||
return sqlStatements;
|
||||
};
|
||||
|
||||
export const pushMySQLSchema = async (
|
||||
imports: Record<string, unknown>,
|
||||
drizzleInstance: MySql2Database<any>,
|
||||
databaseName: string,
|
||||
) => {
|
||||
const { applyMysqlSnapshotsDiff } = await import('./snapshotsDiffer');
|
||||
const { logSuggestionsAndReturn } = await import(
|
||||
'./cli/commands/mysqlPushUtils'
|
||||
);
|
||||
const { mysqlPushIntrospect } = await import(
|
||||
'./cli/commands/mysqlIntrospect'
|
||||
);
|
||||
const { sql } = await import('drizzle-orm');
|
||||
|
||||
const db: DB = {
|
||||
query: async (query: string, params?: any[]) => {
|
||||
const res = await drizzleInstance.execute(sql.raw(query));
|
||||
return res[0] as unknown as any[];
|
||||
},
|
||||
};
|
||||
const cur = await generateMySQLDrizzleJson(imports);
|
||||
const { schema: prev } = await mysqlPushIntrospect(db, databaseName, []);
|
||||
|
||||
const validatedPrev = mysqlSchema.parse(prev);
|
||||
const validatedCur = mysqlSchema.parse(cur);
|
||||
|
||||
const squashedPrev = squashMysqlScheme(validatedPrev);
|
||||
const squashedCur = squashMysqlScheme(validatedCur);
|
||||
|
||||
const { statements } = await applyMysqlSnapshotsDiff(
|
||||
squashedPrev,
|
||||
squashedCur,
|
||||
tablesResolver,
|
||||
columnsResolver,
|
||||
mySqlViewsResolver,
|
||||
validatedPrev,
|
||||
validatedCur,
|
||||
'push',
|
||||
);
|
||||
|
||||
const { shouldAskForApprove, statementsToExecute, infoToPrint } = await logSuggestionsAndReturn(
|
||||
db,
|
||||
statements,
|
||||
validatedCur,
|
||||
);
|
||||
|
||||
return {
|
||||
hasDataLoss: shouldAskForApprove,
|
||||
warnings: infoToPrint,
|
||||
statementsToExecute,
|
||||
apply: async () => {
|
||||
for (const dStmnt of statementsToExecute) {
|
||||
await db.query(dStmnt);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const startStudioMySQLServer = async (
|
||||
imports: Record<string, unknown>,
|
||||
credentials: MysqlCredentials,
|
||||
options?: {
|
||||
host?: string;
|
||||
port?: number;
|
||||
casing?: CasingType;
|
||||
},
|
||||
) => {
|
||||
const { drizzleForMySQL } = await import('./serializer/studio');
|
||||
|
||||
const mysqlSchema: Record<string, Record<string, AnyMySqlTable>> = {};
|
||||
const relations: Record<string, Relations> = {};
|
||||
|
||||
Object.entries(imports).forEach(([k, t]) => {
|
||||
if (is(t, MySqlTable)) {
|
||||
const schema = mysqlTableConfig(t).schema || 'public';
|
||||
mysqlSchema[schema] = mysqlSchema[schema] || {};
|
||||
mysqlSchema[schema][k] = t;
|
||||
}
|
||||
|
||||
if (is(t, Relations)) {
|
||||
relations[k] = t;
|
||||
}
|
||||
});
|
||||
|
||||
const setup = await drizzleForMySQL(credentials, mysqlSchema, relations, [], options?.casing);
|
||||
await startServerFromSetup(setup, options);
|
||||
};
|
||||
|
||||
// SingleStore
|
||||
|
||||
export const generateSingleStoreDrizzleJson = async (
|
||||
imports: Record<string, unknown>,
|
||||
prevId?: string,
|
||||
casing?: CasingType,
|
||||
): Promise<SingleStoreSchemaKit> => {
|
||||
const { prepareFromExports } = await import('./serializer/singlestoreImports');
|
||||
|
||||
const prepared = prepareFromExports(imports);
|
||||
|
||||
const id = randomUUID();
|
||||
|
||||
const snapshot = generateSingleStoreSnapshot(prepared.tables, /* prepared.views, */ casing);
|
||||
|
||||
return {
|
||||
...snapshot,
|
||||
id,
|
||||
prevId: prevId ?? originUUID,
|
||||
};
|
||||
};
|
||||
|
||||
export const generateSingleStoreMigration = async (
|
||||
prev: DrizzleSingleStoreSnapshotJSON,
|
||||
cur: DrizzleSingleStoreSnapshotJSON,
|
||||
) => {
|
||||
const { applySingleStoreSnapshotsDiff } = await import('./snapshotsDiffer');
|
||||
|
||||
const validatedPrev = singlestoreSchema.parse(prev);
|
||||
const validatedCur = singlestoreSchema.parse(cur);
|
||||
|
||||
const squashedPrev = squashSingleStoreScheme(validatedPrev);
|
||||
const squashedCur = squashSingleStoreScheme(validatedCur);
|
||||
|
||||
const { sqlStatements } = await applySingleStoreSnapshotsDiff(
|
||||
squashedPrev,
|
||||
squashedCur,
|
||||
tablesResolver,
|
||||
columnsResolver,
|
||||
/* singleStoreViewsResolver, */
|
||||
validatedPrev,
|
||||
validatedCur,
|
||||
'push',
|
||||
);
|
||||
|
||||
return sqlStatements;
|
||||
};
|
||||
|
||||
export const pushSingleStoreSchema = async (
|
||||
imports: Record<string, unknown>,
|
||||
drizzleInstance: SingleStoreDriverDatabase<any>,
|
||||
databaseName: string,
|
||||
) => {
|
||||
const { applySingleStoreSnapshotsDiff } = await import('./snapshotsDiffer');
|
||||
const { logSuggestionsAndReturn } = await import(
|
||||
'./cli/commands/singlestorePushUtils'
|
||||
);
|
||||
const { singlestorePushIntrospect } = await import(
|
||||
'./cli/commands/singlestoreIntrospect'
|
||||
);
|
||||
const { sql } = await import('drizzle-orm');
|
||||
|
||||
const db: DB = {
|
||||
query: async (query: string) => {
|
||||
const res = await drizzleInstance.execute(sql.raw(query));
|
||||
return res[0] as unknown as any[];
|
||||
},
|
||||
};
|
||||
const cur = await generateSingleStoreDrizzleJson(imports);
|
||||
const { schema: prev } = await singlestorePushIntrospect(db, databaseName, []);
|
||||
|
||||
const validatedPrev = singlestoreSchema.parse(prev);
|
||||
const validatedCur = singlestoreSchema.parse(cur);
|
||||
|
||||
const squashedPrev = squashSingleStoreScheme(validatedPrev);
|
||||
const squashedCur = squashSingleStoreScheme(validatedCur);
|
||||
|
||||
const { statements } = await applySingleStoreSnapshotsDiff(
|
||||
squashedPrev,
|
||||
squashedCur,
|
||||
tablesResolver,
|
||||
columnsResolver,
|
||||
/* singleStoreViewsResolver, */
|
||||
validatedPrev,
|
||||
validatedCur,
|
||||
'push',
|
||||
);
|
||||
|
||||
const { shouldAskForApprove, statementsToExecute, infoToPrint } = await logSuggestionsAndReturn(
|
||||
db,
|
||||
statements,
|
||||
validatedCur,
|
||||
validatedPrev,
|
||||
);
|
||||
|
||||
return {
|
||||
hasDataLoss: shouldAskForApprove,
|
||||
warnings: infoToPrint,
|
||||
statementsToExecute,
|
||||
apply: async () => {
|
||||
for (const dStmnt of statementsToExecute) {
|
||||
await db.query(dStmnt);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const startStudioSingleStoreServer = async (
|
||||
imports: Record<string, unknown>,
|
||||
credentials: SingleStoreCredentials,
|
||||
options?: {
|
||||
host?: string;
|
||||
port?: number;
|
||||
casing?: CasingType;
|
||||
},
|
||||
) => {
|
||||
const { drizzleForSingleStore } = await import('./serializer/studio');
|
||||
|
||||
const singleStoreSchema: Record<string, Record<string, AnySingleStoreTable>> = {};
|
||||
const relations: Record<string, Relations> = {};
|
||||
|
||||
Object.entries(imports).forEach(([k, t]) => {
|
||||
if (is(t, SingleStoreTable)) {
|
||||
const schema = singlestoreTableConfig(t).schema || 'public';
|
||||
singleStoreSchema[schema] = singleStoreSchema[schema] || {};
|
||||
singleStoreSchema[schema][k] = t;
|
||||
}
|
||||
|
||||
if (is(t, Relations)) {
|
||||
relations[k] = t;
|
||||
}
|
||||
});
|
||||
|
||||
const setup = await drizzleForSingleStore(credentials, singleStoreSchema, relations, [], options?.casing);
|
||||
await startServerFromSetup(setup, options);
|
||||
};
|
||||
|
||||
const startServerFromSetup = async (setup: Setup, options?: {
|
||||
host?: string;
|
||||
port?: number;
|
||||
}) => {
|
||||
const { prepareServer } = await import('./serializer/studio');
|
||||
|
||||
const server = await prepareServer(setup);
|
||||
|
||||
const host = options?.host || '127.0.0.1';
|
||||
const port = options?.port || 4983;
|
||||
const { key, cert } = (await certs()) || {};
|
||||
server.start({
|
||||
host,
|
||||
port,
|
||||
key,
|
||||
cert,
|
||||
cb: (err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
} else {
|
||||
console.log(`Studio is running at ${key ? 'https' : 'http'}://${host}:${port}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const upPgSnapshot = (snapshot: Record<string, unknown>) => {
|
||||
if (snapshot.version === '5') {
|
||||
return upPgV7(upPgV6(snapshot));
|
||||
}
|
||||
if (snapshot.version === '6') {
|
||||
return upPgV7(snapshot);
|
||||
}
|
||||
return snapshot;
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
const _ = '';
|
||||
export default _;
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Dialect } from '../../schemaValidator';
|
||||
import { prepareOutFolder, validateWithReport } from '../../utils';
|
||||
|
||||
export const checkHandler = (out: string, dialect: Dialect) => {
|
||||
const { snapshots } = prepareOutFolder(out, dialect);
|
||||
const report = validateWithReport(snapshots, dialect);
|
||||
|
||||
if (report.nonLatest.length > 0) {
|
||||
console.log(
|
||||
report.nonLatest
|
||||
.map((it) => {
|
||||
return `${it} is not of the latest version, please run "drizzle-kit up"`;
|
||||
})
|
||||
.join('\n'),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (report.malformed.length) {
|
||||
const message = report.malformed
|
||||
.map((it) => {
|
||||
return `${it} data is malformed`;
|
||||
})
|
||||
.join('\n');
|
||||
console.log(message);
|
||||
}
|
||||
|
||||
const collisionEntries = Object.entries(report.idsMap).filter(
|
||||
(it) => it[1].snapshots.length > 1,
|
||||
);
|
||||
|
||||
const message = collisionEntries
|
||||
.map((it) => {
|
||||
const data = it[1];
|
||||
return `[${
|
||||
data.snapshots.join(
|
||||
', ',
|
||||
)
|
||||
}] are pointing to a parent snapshot: ${data.parent}/snapshot.json which is a collision.`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
if (message) {
|
||||
console.log(message);
|
||||
}
|
||||
|
||||
const abort = report.malformed.length!! || collisionEntries.length > 0;
|
||||
|
||||
if (abort) {
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import chalk from 'chalk';
|
||||
import { readFileSync, rmSync, writeFileSync } from 'fs';
|
||||
import fs from 'fs';
|
||||
import { render } from 'hanji';
|
||||
import { join } from 'path';
|
||||
import { Journal } from '../../utils';
|
||||
import { DropMigrationView } from '../views';
|
||||
import { embeddedMigrations } from './migrate';
|
||||
|
||||
export const dropMigration = async ({
|
||||
out,
|
||||
bundle,
|
||||
}: {
|
||||
out: string;
|
||||
bundle: boolean;
|
||||
}) => {
|
||||
const metaFilePath = join(out, 'meta', '_journal.json');
|
||||
const journal = JSON.parse(readFileSync(metaFilePath, 'utf-8')) as Journal;
|
||||
|
||||
if (journal.entries.length === 0) {
|
||||
console.log(
|
||||
`[${chalk.blue('i')}] no migration entries found in ${metaFilePath}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await render(new DropMigrationView(journal.entries));
|
||||
if (result.status === 'aborted') return;
|
||||
|
||||
delete journal.entries[journal.entries.indexOf(result.data!)];
|
||||
|
||||
const resultJournal: Journal = {
|
||||
...journal,
|
||||
entries: journal.entries.filter(Boolean),
|
||||
};
|
||||
const sqlFilePath = join(out, `${result.data.tag}.sql`);
|
||||
const snapshotFilePath = join(
|
||||
out,
|
||||
'meta',
|
||||
`${result.data.tag.split('_')[0]}_snapshot.json`,
|
||||
);
|
||||
rmSync(sqlFilePath);
|
||||
rmSync(snapshotFilePath);
|
||||
writeFileSync(metaFilePath, JSON.stringify(resultJournal, null, 2));
|
||||
|
||||
if (bundle) {
|
||||
fs.writeFileSync(
|
||||
join(out, `migrations.js`),
|
||||
embeddedMigrations(resultJournal),
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[${chalk.green('✓')}] ${
|
||||
chalk.bold(
|
||||
result.data.tag,
|
||||
)
|
||||
} migration successfully dropped`,
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,900 @@
|
||||
import chalk from 'chalk';
|
||||
import { writeFileSync } from 'fs';
|
||||
import { render, renderWithTask } from 'hanji';
|
||||
import { Minimatch } from 'minimatch';
|
||||
import { join } from 'path';
|
||||
import { plural, singular } from 'pluralize';
|
||||
import { GelSchema } from 'src/serializer/gelSchema';
|
||||
import { drySingleStore, SingleStoreSchema, squashSingleStoreScheme } from 'src/serializer/singlestoreSchema';
|
||||
import { assertUnreachable, originUUID } from '../../global';
|
||||
import { schemaToTypeScript as gelSchemaToTypeScript } from '../../introspect-gel';
|
||||
import { schemaToTypeScript as mysqlSchemaToTypeScript } from '../../introspect-mysql';
|
||||
import { paramNameFor, schemaToTypeScript as postgresSchemaToTypeScript } from '../../introspect-pg';
|
||||
import { schemaToTypeScript as singlestoreSchemaToTypeScript } from '../../introspect-singlestore';
|
||||
import { schemaToTypeScript as sqliteSchemaToTypeScript } from '../../introspect-sqlite';
|
||||
import { fromDatabase as fromGelDatabase } from '../../serializer/gelSerializer';
|
||||
import { dryMySql, MySqlSchema, squashMysqlScheme } from '../../serializer/mysqlSchema';
|
||||
import { fromDatabase as fromMysqlDatabase } from '../../serializer/mysqlSerializer';
|
||||
import { dryPg, type PgSchema, squashPgScheme } from '../../serializer/pgSchema';
|
||||
import { fromDatabase as fromPostgresDatabase } from '../../serializer/pgSerializer';
|
||||
import { fromDatabase as fromSingleStoreDatabase } from '../../serializer/singlestoreSerializer';
|
||||
import { drySQLite, type SQLiteSchema, squashSqliteScheme } from '../../serializer/sqliteSchema';
|
||||
import { fromDatabase as fromSqliteDatabase } from '../../serializer/sqliteSerializer';
|
||||
import {
|
||||
applyLibSQLSnapshotsDiff,
|
||||
applyMysqlSnapshotsDiff,
|
||||
applyPgSnapshotsDiff,
|
||||
applySingleStoreSnapshotsDiff,
|
||||
applySqliteSnapshotsDiff,
|
||||
} from '../../snapshotsDiffer';
|
||||
import { prepareOutFolder } from '../../utils';
|
||||
import { Entities } from '../validations/cli';
|
||||
import type { Casing, Prefix } from '../validations/common';
|
||||
import { GelCredentials } from '../validations/gel';
|
||||
import { LibSQLCredentials } from '../validations/libsql';
|
||||
import type { MysqlCredentials } from '../validations/mysql';
|
||||
import type { PostgresCredentials } from '../validations/postgres';
|
||||
import { SingleStoreCredentials } from '../validations/singlestore';
|
||||
import type { SqliteCredentials } from '../validations/sqlite';
|
||||
import { IntrospectProgress } from '../views';
|
||||
import {
|
||||
columnsResolver,
|
||||
enumsResolver,
|
||||
indPolicyResolver,
|
||||
mySqlViewsResolver,
|
||||
policyResolver,
|
||||
roleResolver,
|
||||
schemasResolver,
|
||||
sequencesResolver,
|
||||
sqliteViewsResolver,
|
||||
tablesResolver,
|
||||
viewsResolver,
|
||||
writeResult,
|
||||
} from './migrate';
|
||||
|
||||
export const introspectPostgres = async (
|
||||
casing: Casing,
|
||||
out: string,
|
||||
breakpoints: boolean,
|
||||
credentials: PostgresCredentials,
|
||||
tablesFilter: string[],
|
||||
schemasFilter: string[],
|
||||
prefix: Prefix,
|
||||
entities: Entities,
|
||||
) => {
|
||||
const { preparePostgresDB } = await import('../connections');
|
||||
const db = await preparePostgresDB(credentials);
|
||||
|
||||
const matchers = tablesFilter.map((it) => {
|
||||
return new Minimatch(it);
|
||||
});
|
||||
|
||||
const filter = (tableName: string) => {
|
||||
if (matchers.length === 0) return true;
|
||||
|
||||
let flags: boolean[] = [];
|
||||
|
||||
for (let matcher of matchers) {
|
||||
if (matcher.negate) {
|
||||
if (!matcher.match(tableName)) {
|
||||
flags.push(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (matcher.match(tableName)) {
|
||||
flags.push(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (flags.length > 0) {
|
||||
return flags.every(Boolean);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const progress = new IntrospectProgress(true);
|
||||
|
||||
const res = await renderWithTask(
|
||||
progress,
|
||||
fromPostgresDatabase(
|
||||
db,
|
||||
filter,
|
||||
schemasFilter,
|
||||
entities,
|
||||
(stage, count, status) => {
|
||||
progress.update(stage, count, status);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const schema = { id: originUUID, prevId: '', ...res } as PgSchema;
|
||||
const ts = postgresSchemaToTypeScript(schema, casing);
|
||||
const relationsTs = relationsToTypeScript(schema, casing);
|
||||
const { internal, ...schemaWithoutInternals } = schema;
|
||||
|
||||
const schemaFile = join(out, 'schema.ts');
|
||||
writeFileSync(schemaFile, ts.file);
|
||||
const relationsFile = join(out, 'relations.ts');
|
||||
writeFileSync(relationsFile, relationsTs.file);
|
||||
console.log();
|
||||
|
||||
const { snapshots, journal } = prepareOutFolder(out, 'postgresql');
|
||||
|
||||
if (snapshots.length === 0) {
|
||||
const { sqlStatements, _meta } = await applyPgSnapshotsDiff(
|
||||
squashPgScheme(dryPg),
|
||||
squashPgScheme(schema),
|
||||
schemasResolver,
|
||||
enumsResolver,
|
||||
sequencesResolver,
|
||||
policyResolver,
|
||||
indPolicyResolver,
|
||||
roleResolver,
|
||||
tablesResolver,
|
||||
columnsResolver,
|
||||
viewsResolver,
|
||||
dryPg,
|
||||
schema,
|
||||
);
|
||||
|
||||
writeResult({
|
||||
cur: schema,
|
||||
sqlStatements,
|
||||
journal,
|
||||
_meta,
|
||||
outFolder: out,
|
||||
breakpoints,
|
||||
type: 'introspect',
|
||||
prefixMode: prefix,
|
||||
});
|
||||
} else {
|
||||
render(
|
||||
`[${
|
||||
chalk.blue(
|
||||
'i',
|
||||
)
|
||||
}] No SQL generated, you already have migrations in project`,
|
||||
);
|
||||
}
|
||||
|
||||
render(
|
||||
`[${
|
||||
chalk.green(
|
||||
'✓',
|
||||
)
|
||||
}] Your schema file is ready ➜ ${chalk.bold.underline.blue(schemaFile)} 🚀`,
|
||||
);
|
||||
render(
|
||||
`[${
|
||||
chalk.green(
|
||||
'✓',
|
||||
)
|
||||
}] Your relations file is ready ➜ ${
|
||||
chalk.bold.underline.blue(
|
||||
relationsFile,
|
||||
)
|
||||
} 🚀`,
|
||||
);
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
export const introspectGel = async (
|
||||
casing: Casing,
|
||||
out: string,
|
||||
breakpoints: boolean,
|
||||
credentials: GelCredentials | undefined,
|
||||
tablesFilter: string[],
|
||||
schemasFilter: string[],
|
||||
prefix: Prefix,
|
||||
entities: Entities,
|
||||
) => {
|
||||
const { prepareGelDB } = await import('../connections');
|
||||
const db = await prepareGelDB(credentials);
|
||||
|
||||
const matchers = tablesFilter.map((it) => {
|
||||
return new Minimatch(it);
|
||||
});
|
||||
|
||||
const filter = (tableName: string) => {
|
||||
if (matchers.length === 0) return true;
|
||||
|
||||
let flags: boolean[] = [];
|
||||
|
||||
for (let matcher of matchers) {
|
||||
if (matcher.negate) {
|
||||
if (!matcher.match(tableName)) {
|
||||
flags.push(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (matcher.match(tableName)) {
|
||||
flags.push(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (flags.length > 0) {
|
||||
return flags.every(Boolean);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const progress = new IntrospectProgress(true);
|
||||
|
||||
const res = await renderWithTask(
|
||||
progress,
|
||||
fromGelDatabase(
|
||||
db,
|
||||
filter,
|
||||
schemasFilter,
|
||||
entities,
|
||||
(stage, count, status) => {
|
||||
progress.update(stage, count, status);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const schema = { id: originUUID, prevId: '', ...res } as GelSchema;
|
||||
const ts = gelSchemaToTypeScript(schema, casing);
|
||||
const relationsTs = relationsToTypeScript(schema, casing);
|
||||
const { internal, ...schemaWithoutInternals } = schema;
|
||||
|
||||
const schemaFile = join(out, 'schema.ts');
|
||||
writeFileSync(schemaFile, ts.file);
|
||||
const relationsFile = join(out, 'relations.ts');
|
||||
writeFileSync(relationsFile, relationsTs.file);
|
||||
console.log();
|
||||
|
||||
// const { snapshots, journal } = prepareOutFolder(out, 'gel');
|
||||
|
||||
// if (snapshots.length === 0) {
|
||||
// const { sqlStatements, _meta } = await applyGelSnapshotsDiff(
|
||||
// squashGelScheme(dryGel),
|
||||
// squashGelScheme(schema),
|
||||
// schemasResolver,
|
||||
// enumsResolver,
|
||||
// sequencesResolver,
|
||||
// policyResolver,
|
||||
// indPolicyResolver,
|
||||
// roleResolver,
|
||||
// tablesResolver,
|
||||
// columnsResolver,
|
||||
// viewsResolver,
|
||||
// dryPg,
|
||||
// schema,
|
||||
// );
|
||||
|
||||
// writeResult({
|
||||
// cur: schema,
|
||||
// sqlStatements,
|
||||
// journal,
|
||||
// _meta,
|
||||
// outFolder: out,
|
||||
// breakpoints,
|
||||
// type: 'introspect',
|
||||
// prefixMode: prefix,
|
||||
// });
|
||||
// } else {
|
||||
// render(
|
||||
// `[${
|
||||
// chalk.blue(
|
||||
// 'i',
|
||||
// )
|
||||
// }] No SQL generated, you already have migrations in project`,
|
||||
// );
|
||||
// }
|
||||
|
||||
render(
|
||||
`[${
|
||||
chalk.green(
|
||||
'✓',
|
||||
)
|
||||
}] Your schema file is ready ➜ ${chalk.bold.underline.blue(schemaFile)} 🚀`,
|
||||
);
|
||||
render(
|
||||
`[${
|
||||
chalk.green(
|
||||
'✓',
|
||||
)
|
||||
}] Your relations file is ready ➜ ${
|
||||
chalk.bold.underline.blue(
|
||||
relationsFile,
|
||||
)
|
||||
} 🚀`,
|
||||
);
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
export const introspectMysql = async (
|
||||
casing: Casing,
|
||||
out: string,
|
||||
breakpoints: boolean,
|
||||
credentials: MysqlCredentials,
|
||||
tablesFilter: string[],
|
||||
prefix: Prefix,
|
||||
) => {
|
||||
const { connectToMySQL } = await import('../connections');
|
||||
const { db, database } = await connectToMySQL(credentials);
|
||||
|
||||
const matchers = tablesFilter.map((it) => {
|
||||
return new Minimatch(it);
|
||||
});
|
||||
|
||||
const filter = (tableName: string) => {
|
||||
if (matchers.length === 0) return true;
|
||||
|
||||
let flags: boolean[] = [];
|
||||
|
||||
for (let matcher of matchers) {
|
||||
if (matcher.negate) {
|
||||
if (!matcher.match(tableName)) {
|
||||
flags.push(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (matcher.match(tableName)) {
|
||||
flags.push(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (flags.length > 0) {
|
||||
return flags.every(Boolean);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const progress = new IntrospectProgress();
|
||||
const res = await renderWithTask(
|
||||
progress,
|
||||
fromMysqlDatabase(db, database, filter, (stage, count, status) => {
|
||||
progress.update(stage, count, status);
|
||||
}),
|
||||
);
|
||||
|
||||
const schema = { id: originUUID, prevId: '', ...res } as MySqlSchema;
|
||||
const ts = mysqlSchemaToTypeScript(schema, casing);
|
||||
const relationsTs = relationsToTypeScript(schema, casing);
|
||||
const { internal, ...schemaWithoutInternals } = schema;
|
||||
|
||||
const schemaFile = join(out, 'schema.ts');
|
||||
writeFileSync(schemaFile, ts.file);
|
||||
const relationsFile = join(out, 'relations.ts');
|
||||
writeFileSync(relationsFile, relationsTs.file);
|
||||
console.log();
|
||||
|
||||
const { snapshots, journal } = prepareOutFolder(out, 'mysql');
|
||||
|
||||
if (snapshots.length === 0) {
|
||||
const { sqlStatements, _meta } = await applyMysqlSnapshotsDiff(
|
||||
squashMysqlScheme(dryMySql),
|
||||
squashMysqlScheme(schema),
|
||||
tablesResolver,
|
||||
columnsResolver,
|
||||
mySqlViewsResolver,
|
||||
dryMySql,
|
||||
schema,
|
||||
);
|
||||
|
||||
writeResult({
|
||||
cur: schema,
|
||||
sqlStatements,
|
||||
journal,
|
||||
_meta,
|
||||
outFolder: out,
|
||||
breakpoints,
|
||||
type: 'introspect',
|
||||
prefixMode: prefix,
|
||||
});
|
||||
} else {
|
||||
render(
|
||||
`[${
|
||||
chalk.blue(
|
||||
'i',
|
||||
)
|
||||
}] No SQL generated, you already have migrations in project`,
|
||||
);
|
||||
}
|
||||
|
||||
render(
|
||||
`[${
|
||||
chalk.green(
|
||||
'✓',
|
||||
)
|
||||
}] Your schema file is ready ➜ ${chalk.bold.underline.blue(schemaFile)} 🚀`,
|
||||
);
|
||||
render(
|
||||
`[${
|
||||
chalk.green(
|
||||
'✓',
|
||||
)
|
||||
}] Your relations file is ready ➜ ${
|
||||
chalk.bold.underline.blue(
|
||||
relationsFile,
|
||||
)
|
||||
} 🚀`,
|
||||
);
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
export const introspectSingleStore = async (
|
||||
casing: Casing,
|
||||
out: string,
|
||||
breakpoints: boolean,
|
||||
credentials: SingleStoreCredentials,
|
||||
tablesFilter: string[],
|
||||
prefix: Prefix,
|
||||
) => {
|
||||
const { connectToSingleStore } = await import('../connections');
|
||||
const { db, database } = await connectToSingleStore(credentials);
|
||||
|
||||
const matchers = tablesFilter.map((it) => {
|
||||
return new Minimatch(it);
|
||||
});
|
||||
|
||||
const filter = (tableName: string) => {
|
||||
if (matchers.length === 0) return true;
|
||||
|
||||
let flags: boolean[] = [];
|
||||
|
||||
for (let matcher of matchers) {
|
||||
if (matcher.negate) {
|
||||
if (!matcher.match(tableName)) {
|
||||
flags.push(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (matcher.match(tableName)) {
|
||||
flags.push(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (flags.length > 0) {
|
||||
return flags.every(Boolean);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const progress = new IntrospectProgress();
|
||||
const res = await renderWithTask(
|
||||
progress,
|
||||
fromSingleStoreDatabase(db, database, filter, (stage, count, status) => {
|
||||
progress.update(stage, count, status);
|
||||
}),
|
||||
);
|
||||
|
||||
const schema = { id: originUUID, prevId: '', ...res } as SingleStoreSchema;
|
||||
const ts = singlestoreSchemaToTypeScript(schema, casing);
|
||||
const { internal, ...schemaWithoutInternals } = schema;
|
||||
|
||||
const schemaFile = join(out, 'schema.ts');
|
||||
writeFileSync(schemaFile, ts.file);
|
||||
console.log();
|
||||
|
||||
const { snapshots, journal } = prepareOutFolder(out, 'postgresql');
|
||||
|
||||
if (snapshots.length === 0) {
|
||||
const { sqlStatements, _meta } = await applySingleStoreSnapshotsDiff(
|
||||
squashSingleStoreScheme(drySingleStore),
|
||||
squashSingleStoreScheme(schema),
|
||||
tablesResolver,
|
||||
columnsResolver,
|
||||
/* singleStoreViewsResolver, */
|
||||
drySingleStore,
|
||||
schema,
|
||||
);
|
||||
|
||||
writeResult({
|
||||
cur: schema,
|
||||
sqlStatements,
|
||||
journal,
|
||||
_meta,
|
||||
outFolder: out,
|
||||
breakpoints,
|
||||
type: 'introspect',
|
||||
prefixMode: prefix,
|
||||
});
|
||||
} else {
|
||||
render(
|
||||
`[${
|
||||
chalk.blue(
|
||||
'i',
|
||||
)
|
||||
}] No SQL generated, you already have migrations in project`,
|
||||
);
|
||||
}
|
||||
|
||||
render(
|
||||
`[${
|
||||
chalk.green(
|
||||
'✓',
|
||||
)
|
||||
}] You schema file is ready ➜ ${chalk.bold.underline.blue(schemaFile)} 🚀`,
|
||||
);
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
export const introspectSqlite = async (
|
||||
casing: Casing,
|
||||
out: string,
|
||||
breakpoints: boolean,
|
||||
credentials: SqliteCredentials,
|
||||
tablesFilter: string[],
|
||||
prefix: Prefix,
|
||||
) => {
|
||||
const { connectToSQLite } = await import('../connections');
|
||||
const db = await connectToSQLite(credentials);
|
||||
|
||||
const matchers = tablesFilter.map((it) => {
|
||||
return new Minimatch(it);
|
||||
});
|
||||
|
||||
const filter = (tableName: string) => {
|
||||
if (matchers.length === 0) return true;
|
||||
|
||||
let flags: boolean[] = [];
|
||||
|
||||
for (let matcher of matchers) {
|
||||
if (matcher.negate) {
|
||||
if (!matcher.match(tableName)) {
|
||||
flags.push(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (matcher.match(tableName)) {
|
||||
flags.push(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (flags.length > 0) {
|
||||
return flags.every(Boolean);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const progress = new IntrospectProgress();
|
||||
const res = await renderWithTask(
|
||||
progress,
|
||||
fromSqliteDatabase(db, filter, (stage, count, status) => {
|
||||
progress.update(stage, count, status);
|
||||
}),
|
||||
);
|
||||
|
||||
const schema = { id: originUUID, prevId: '', ...res } as SQLiteSchema;
|
||||
const ts = sqliteSchemaToTypeScript(schema, casing);
|
||||
const relationsTs = relationsToTypeScript(schema, casing);
|
||||
|
||||
// check orm and orm-pg api version
|
||||
|
||||
const schemaFile = join(out, 'schema.ts');
|
||||
writeFileSync(schemaFile, ts.file);
|
||||
const relationsFile = join(out, 'relations.ts');
|
||||
writeFileSync(relationsFile, relationsTs.file);
|
||||
console.log();
|
||||
|
||||
const { snapshots, journal } = prepareOutFolder(out, 'sqlite');
|
||||
|
||||
if (snapshots.length === 0) {
|
||||
const { sqlStatements, _meta } = await applySqliteSnapshotsDiff(
|
||||
squashSqliteScheme(drySQLite),
|
||||
squashSqliteScheme(schema),
|
||||
tablesResolver,
|
||||
columnsResolver,
|
||||
sqliteViewsResolver,
|
||||
drySQLite,
|
||||
schema,
|
||||
);
|
||||
|
||||
writeResult({
|
||||
cur: schema,
|
||||
sqlStatements,
|
||||
journal,
|
||||
_meta,
|
||||
outFolder: out,
|
||||
breakpoints,
|
||||
type: 'introspect',
|
||||
prefixMode: prefix,
|
||||
});
|
||||
} else {
|
||||
render(
|
||||
`[${
|
||||
chalk.blue(
|
||||
'i',
|
||||
)
|
||||
}] No SQL generated, you already have migrations in project`,
|
||||
);
|
||||
}
|
||||
|
||||
render(
|
||||
`[${
|
||||
chalk.green(
|
||||
'✓',
|
||||
)
|
||||
}] You schema file is ready ➜ ${chalk.bold.underline.blue(schemaFile)} 🚀`,
|
||||
);
|
||||
render(
|
||||
`[${
|
||||
chalk.green(
|
||||
'✓',
|
||||
)
|
||||
}] You relations file is ready ➜ ${
|
||||
chalk.bold.underline.blue(
|
||||
relationsFile,
|
||||
)
|
||||
} 🚀`,
|
||||
);
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
export const introspectLibSQL = async (
|
||||
casing: Casing,
|
||||
out: string,
|
||||
breakpoints: boolean,
|
||||
credentials: LibSQLCredentials,
|
||||
tablesFilter: string[],
|
||||
prefix: Prefix,
|
||||
) => {
|
||||
const { connectToLibSQL } = await import('../connections');
|
||||
const db = await connectToLibSQL(credentials);
|
||||
|
||||
const matchers = tablesFilter.map((it) => {
|
||||
return new Minimatch(it);
|
||||
});
|
||||
|
||||
const filter = (tableName: string) => {
|
||||
if (matchers.length === 0) return true;
|
||||
|
||||
let flags: boolean[] = [];
|
||||
|
||||
for (let matcher of matchers) {
|
||||
if (matcher.negate) {
|
||||
if (!matcher.match(tableName)) {
|
||||
flags.push(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (matcher.match(tableName)) {
|
||||
flags.push(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (flags.length > 0) {
|
||||
return flags.every(Boolean);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const progress = new IntrospectProgress();
|
||||
const res = await renderWithTask(
|
||||
progress,
|
||||
fromSqliteDatabase(db, filter, (stage, count, status) => {
|
||||
progress.update(stage, count, status);
|
||||
}),
|
||||
);
|
||||
|
||||
const schema = { id: originUUID, prevId: '', ...res } as SQLiteSchema;
|
||||
const ts = sqliteSchemaToTypeScript(schema, casing);
|
||||
const relationsTs = relationsToTypeScript(schema, casing);
|
||||
|
||||
// check orm and orm-pg api version
|
||||
|
||||
const schemaFile = join(out, 'schema.ts');
|
||||
writeFileSync(schemaFile, ts.file);
|
||||
const relationsFile = join(out, 'relations.ts');
|
||||
writeFileSync(relationsFile, relationsTs.file);
|
||||
console.log();
|
||||
|
||||
const { snapshots, journal } = prepareOutFolder(out, 'sqlite');
|
||||
|
||||
if (snapshots.length === 0) {
|
||||
const { sqlStatements, _meta } = await applyLibSQLSnapshotsDiff(
|
||||
squashSqliteScheme(drySQLite),
|
||||
squashSqliteScheme(schema),
|
||||
tablesResolver,
|
||||
columnsResolver,
|
||||
sqliteViewsResolver,
|
||||
drySQLite,
|
||||
schema,
|
||||
);
|
||||
|
||||
writeResult({
|
||||
cur: schema,
|
||||
sqlStatements,
|
||||
journal,
|
||||
_meta,
|
||||
outFolder: out,
|
||||
breakpoints,
|
||||
type: 'introspect',
|
||||
prefixMode: prefix,
|
||||
});
|
||||
} else {
|
||||
render(
|
||||
`[${
|
||||
chalk.blue(
|
||||
'i',
|
||||
)
|
||||
}] No SQL generated, you already have migrations in project`,
|
||||
);
|
||||
}
|
||||
|
||||
render(
|
||||
`[${
|
||||
chalk.green(
|
||||
'✓',
|
||||
)
|
||||
}] Your schema file is ready ➜ ${chalk.bold.underline.blue(schemaFile)} 🚀`,
|
||||
);
|
||||
render(
|
||||
`[${
|
||||
chalk.green(
|
||||
'✓',
|
||||
)
|
||||
}] Your relations file is ready ➜ ${
|
||||
chalk.bold.underline.blue(
|
||||
relationsFile,
|
||||
)
|
||||
} 🚀`,
|
||||
);
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
const withCasing = (value: string, casing: Casing) => {
|
||||
if (casing === 'preserve') {
|
||||
return value;
|
||||
}
|
||||
if (casing === 'camel') {
|
||||
return value.camelCase();
|
||||
}
|
||||
|
||||
assertUnreachable(casing);
|
||||
};
|
||||
|
||||
export const relationsToTypeScript = (
|
||||
schema: {
|
||||
tables: Record<
|
||||
string,
|
||||
{
|
||||
schema?: string;
|
||||
foreignKeys: Record<
|
||||
string,
|
||||
{
|
||||
name: string;
|
||||
tableFrom: string;
|
||||
columnsFrom: string[];
|
||||
tableTo: string;
|
||||
schemaTo?: string;
|
||||
columnsTo: string[];
|
||||
onUpdate?: string | undefined;
|
||||
onDelete?: string | undefined;
|
||||
}
|
||||
>;
|
||||
}
|
||||
>;
|
||||
},
|
||||
casing: Casing,
|
||||
) => {
|
||||
const imports: string[] = [];
|
||||
const tableRelations: Record<
|
||||
string,
|
||||
{
|
||||
name: string;
|
||||
type: 'one' | 'many';
|
||||
tableFrom: string;
|
||||
schemaFrom?: string;
|
||||
columnFrom: string;
|
||||
tableTo: string;
|
||||
schemaTo?: string;
|
||||
columnTo: string;
|
||||
relationName?: string;
|
||||
}[]
|
||||
> = {};
|
||||
|
||||
Object.values(schema.tables).forEach((table) => {
|
||||
Object.values(table.foreignKeys).forEach((fk) => {
|
||||
const tableNameFrom = paramNameFor(fk.tableFrom, table.schema);
|
||||
const tableNameTo = paramNameFor(fk.tableTo, fk.schemaTo);
|
||||
const tableFrom = withCasing(tableNameFrom.replace(/:+/g, ''), casing);
|
||||
const tableTo = withCasing(tableNameTo.replace(/:+/g, ''), casing);
|
||||
const columnFrom = withCasing(fk.columnsFrom[0], casing);
|
||||
const columnTo = withCasing(fk.columnsTo[0], casing);
|
||||
|
||||
imports.push(tableTo, tableFrom);
|
||||
|
||||
// const keyFrom = `${schemaFrom}.${tableFrom}`;
|
||||
const keyFrom = tableFrom;
|
||||
|
||||
if (!tableRelations[keyFrom]) {
|
||||
tableRelations[keyFrom] = [];
|
||||
}
|
||||
|
||||
tableRelations[keyFrom].push({
|
||||
name: singular(tableTo),
|
||||
type: 'one',
|
||||
tableFrom,
|
||||
columnFrom,
|
||||
tableTo,
|
||||
columnTo,
|
||||
});
|
||||
|
||||
// const keyTo = `${schemaTo}.${tableTo}`;
|
||||
const keyTo = tableTo;
|
||||
|
||||
if (!tableRelations[keyTo]) {
|
||||
tableRelations[keyTo] = [];
|
||||
}
|
||||
|
||||
tableRelations[keyTo].push({
|
||||
name: plural(tableFrom),
|
||||
type: 'many',
|
||||
tableFrom: tableTo,
|
||||
columnFrom: columnTo,
|
||||
tableTo: tableFrom,
|
||||
columnTo: columnFrom,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const uniqueImports = [...new Set(imports)];
|
||||
|
||||
const importsTs = `import { relations } from "drizzle-orm/relations";\nimport { ${
|
||||
uniqueImports.join(
|
||||
', ',
|
||||
)
|
||||
} } from "./schema";\n\n`;
|
||||
|
||||
const relationStatements = Object.entries(tableRelations).map(
|
||||
([table, relations]) => {
|
||||
const hasOne = relations.some((it) => it.type === 'one');
|
||||
const hasMany = relations.some((it) => it.type === 'many');
|
||||
|
||||
// * change relation names if they are duplicated or if there are multiple relations between two tables
|
||||
const preparedRelations = relations.map(
|
||||
(relation, relationIndex, originArray) => {
|
||||
let name = relation.name;
|
||||
let relationName;
|
||||
const hasMultipleRelations = originArray.some(
|
||||
(it, originIndex) => relationIndex !== originIndex && it.tableTo === relation.tableTo,
|
||||
);
|
||||
if (hasMultipleRelations) {
|
||||
relationName = relation.type === 'one'
|
||||
? `${relation.tableFrom}_${relation.columnFrom}_${relation.tableTo}_${relation.columnTo}`
|
||||
: `${relation.tableTo}_${relation.columnTo}_${relation.tableFrom}_${relation.columnFrom}`;
|
||||
}
|
||||
const hasDuplicatedRelation = originArray.some(
|
||||
(it, originIndex) => relationIndex !== originIndex && it.name === relation.name,
|
||||
);
|
||||
if (hasDuplicatedRelation) {
|
||||
name = `${relation.name}_${relation.type === 'one' ? relation.columnFrom : relation.columnTo}`;
|
||||
}
|
||||
return {
|
||||
...relation,
|
||||
name,
|
||||
relationName,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const fields = preparedRelations.map((relation) => {
|
||||
if (relation.type === 'one') {
|
||||
return `\t${relation.name}: one(${relation.tableTo}, {\n\t\tfields: [${relation.tableFrom}.${relation.columnFrom}],\n\t\treferences: [${relation.tableTo}.${relation.columnTo}]${
|
||||
relation.relationName
|
||||
? `,\n\t\trelationName: "${relation.relationName}"`
|
||||
: ''
|
||||
}\n\t}),`;
|
||||
} else {
|
||||
return `\t${relation.name}: many(${relation.tableTo}${
|
||||
relation.relationName
|
||||
? `, {\n\t\trelationName: "${relation.relationName}"\n\t}`
|
||||
: ''
|
||||
}),`;
|
||||
}
|
||||
});
|
||||
|
||||
return `export const ${table}Relations = relations(${table}, ({${hasOne ? 'one' : ''}${
|
||||
hasOne && hasMany ? ', ' : ''
|
||||
}${hasMany ? 'many' : ''}}) => ({\n${fields.join('\n')}\n}));`;
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
file: importsTs + relationStatements.join('\n\n'),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,355 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { JsonStatement } from 'src/jsonStatements';
|
||||
import { findAddedAndRemoved, SQLiteDB } from 'src/utils';
|
||||
import { SQLiteSchemaInternal, SQLiteSchemaSquashed, SQLiteSquasher } from '../../serializer/sqliteSchema';
|
||||
import {
|
||||
CreateSqliteIndexConvertor,
|
||||
fromJson,
|
||||
LibSQLModifyColumn,
|
||||
SQLiteCreateTableConvertor,
|
||||
SQLiteDropTableConvertor,
|
||||
SqliteRenameTableConvertor,
|
||||
} from '../../sqlgenerator';
|
||||
|
||||
export const getOldTableName = (
|
||||
tableName: string,
|
||||
meta: SQLiteSchemaInternal['_meta'],
|
||||
) => {
|
||||
for (const key of Object.keys(meta.tables)) {
|
||||
const value = meta.tables[key];
|
||||
if (`"${tableName}"` === value) {
|
||||
return key.substring(1, key.length - 1);
|
||||
}
|
||||
}
|
||||
return tableName;
|
||||
};
|
||||
|
||||
export const _moveDataStatements = (
|
||||
tableName: string,
|
||||
json: SQLiteSchemaSquashed,
|
||||
dataLoss: boolean = false,
|
||||
) => {
|
||||
const statements: string[] = [];
|
||||
|
||||
const newTableName = `__new_${tableName}`;
|
||||
|
||||
// create table statement from a new json2 with proper name
|
||||
const tableColumns = Object.values(json.tables[tableName].columns);
|
||||
const referenceData = Object.values(json.tables[tableName].foreignKeys);
|
||||
const compositePKs = Object.values(
|
||||
json.tables[tableName].compositePrimaryKeys,
|
||||
).map((it) => SQLiteSquasher.unsquashPK(it));
|
||||
const checkConstraints = Object.values(json.tables[tableName].checkConstraints);
|
||||
|
||||
const fks = referenceData.map((it) => SQLiteSquasher.unsquashPushFK(it));
|
||||
|
||||
const mappedCheckConstraints: string[] = checkConstraints.map((it) =>
|
||||
it.replaceAll(`"${tableName}".`, `"${newTableName}".`)
|
||||
.replaceAll(`\`${tableName}\`.`, `\`${newTableName}\`.`)
|
||||
.replaceAll(`${tableName}.`, `${newTableName}.`)
|
||||
.replaceAll(`'${tableName}'.`, `\`${newTableName}\`.`)
|
||||
);
|
||||
|
||||
// create new table
|
||||
statements.push(
|
||||
new SQLiteCreateTableConvertor().convert({
|
||||
type: 'sqlite_create_table',
|
||||
tableName: newTableName,
|
||||
columns: tableColumns,
|
||||
referenceData: fks,
|
||||
compositePKs,
|
||||
checkConstraints: mappedCheckConstraints,
|
||||
}),
|
||||
);
|
||||
|
||||
// move data
|
||||
if (!dataLoss) {
|
||||
const columns = Object.keys(json.tables[tableName].columns).map(
|
||||
(c) => `"${c}"`,
|
||||
);
|
||||
|
||||
statements.push(
|
||||
`INSERT INTO \`${newTableName}\`(${
|
||||
columns.join(
|
||||
', ',
|
||||
)
|
||||
}) SELECT ${columns.join(', ')} FROM \`${tableName}\`;`,
|
||||
);
|
||||
}
|
||||
|
||||
statements.push(
|
||||
new SQLiteDropTableConvertor().convert({
|
||||
type: 'drop_table',
|
||||
tableName: tableName,
|
||||
schema: '',
|
||||
}),
|
||||
);
|
||||
|
||||
// rename table
|
||||
statements.push(
|
||||
new SqliteRenameTableConvertor().convert({
|
||||
fromSchema: '',
|
||||
tableNameFrom: newTableName,
|
||||
tableNameTo: tableName,
|
||||
toSchema: '',
|
||||
type: 'rename_table',
|
||||
}),
|
||||
);
|
||||
|
||||
for (const idx of Object.values(json.tables[tableName].indexes)) {
|
||||
statements.push(
|
||||
new CreateSqliteIndexConvertor().convert({
|
||||
type: 'create_index',
|
||||
tableName: tableName,
|
||||
schema: '',
|
||||
data: idx,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return statements;
|
||||
};
|
||||
|
||||
export const libSqlLogSuggestionsAndReturn = async (
|
||||
connection: SQLiteDB,
|
||||
statements: JsonStatement[],
|
||||
json1: SQLiteSchemaSquashed,
|
||||
json2: SQLiteSchemaSquashed,
|
||||
meta: SQLiteSchemaInternal['_meta'],
|
||||
) => {
|
||||
let shouldAskForApprove = false;
|
||||
const statementsToExecute: string[] = [];
|
||||
const infoToPrint: string[] = [];
|
||||
|
||||
const tablesToRemove: string[] = [];
|
||||
const columnsToRemove: string[] = [];
|
||||
const tablesToTruncate: string[] = [];
|
||||
|
||||
for (const statement of statements) {
|
||||
if (statement.type === 'drop_table') {
|
||||
const res = await connection.query<{ count: string }>(
|
||||
`select count(*) as count from \`${statement.tableName}\``,
|
||||
);
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about to delete ${
|
||||
chalk.underline(
|
||||
statement.tableName,
|
||||
)
|
||||
} table with ${count} items`,
|
||||
);
|
||||
tablesToRemove.push(statement.tableName);
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
const fromJsonStatement = fromJson([statement], 'turso', 'push', json2);
|
||||
statementsToExecute.push(
|
||||
...(Array.isArray(fromJsonStatement) ? fromJsonStatement : [fromJsonStatement]),
|
||||
);
|
||||
} else if (statement.type === 'alter_table_drop_column') {
|
||||
const tableName = statement.tableName;
|
||||
|
||||
const res = await connection.query<{ count: string }>(
|
||||
`select count(*) as count from \`${tableName}\``,
|
||||
);
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about to delete ${
|
||||
chalk.underline(
|
||||
statement.columnName,
|
||||
)
|
||||
} column in ${tableName} table with ${count} items`,
|
||||
);
|
||||
columnsToRemove.push(`${tableName}_${statement.columnName}`);
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
|
||||
const fromJsonStatement = fromJson([statement], 'turso', 'push', json2);
|
||||
statementsToExecute.push(
|
||||
...(Array.isArray(fromJsonStatement) ? fromJsonStatement : [fromJsonStatement]),
|
||||
);
|
||||
} else if (
|
||||
statement.type === 'sqlite_alter_table_add_column'
|
||||
&& statement.column.notNull
|
||||
&& !statement.column.default
|
||||
) {
|
||||
const newTableName = statement.tableName;
|
||||
const res = await connection.query<{ count: string }>(
|
||||
`select count(*) as count from \`${newTableName}\``,
|
||||
);
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about to add not-null ${
|
||||
chalk.underline(
|
||||
statement.column.name,
|
||||
)
|
||||
} column without default value, which contains ${count} items`,
|
||||
);
|
||||
|
||||
tablesToTruncate.push(newTableName);
|
||||
statementsToExecute.push(`delete from ${newTableName};`);
|
||||
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
|
||||
const fromJsonStatement = fromJson([statement], 'turso', 'push', json2);
|
||||
statementsToExecute.push(
|
||||
...(Array.isArray(fromJsonStatement) ? fromJsonStatement : [fromJsonStatement]),
|
||||
);
|
||||
} else if (statement.type === 'alter_table_alter_column_set_notnull') {
|
||||
const tableName = statement.tableName;
|
||||
|
||||
if (
|
||||
statement.type === 'alter_table_alter_column_set_notnull'
|
||||
&& typeof statement.columnDefault === 'undefined'
|
||||
) {
|
||||
const res = await connection.query<{ count: string }>(
|
||||
`select count(*) as count from \`${tableName}\``,
|
||||
);
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about to add not-null constraint to ${
|
||||
chalk.underline(
|
||||
statement.columnName,
|
||||
)
|
||||
} column without default value, which contains ${count} items`,
|
||||
);
|
||||
|
||||
tablesToTruncate.push(tableName);
|
||||
statementsToExecute.push(`delete from \`${tableName}\``);
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
}
|
||||
|
||||
const modifyStatements = new LibSQLModifyColumn().convert(statement, json2);
|
||||
|
||||
statementsToExecute.push(
|
||||
...(Array.isArray(modifyStatements) ? modifyStatements : [modifyStatements]),
|
||||
);
|
||||
} else if (statement.type === 'recreate_table') {
|
||||
const tableName = statement.tableName;
|
||||
|
||||
let dataLoss = false;
|
||||
|
||||
const oldTableName = getOldTableName(tableName, meta);
|
||||
|
||||
const prevColumnNames = Object.keys(json1.tables[oldTableName].columns);
|
||||
const currentColumnNames = Object.keys(json2.tables[tableName].columns);
|
||||
const { removedColumns, addedColumns } = findAddedAndRemoved(
|
||||
prevColumnNames,
|
||||
currentColumnNames,
|
||||
);
|
||||
|
||||
if (removedColumns.length) {
|
||||
for (const removedColumn of removedColumns) {
|
||||
const res = await connection.query<{ count: string }>(
|
||||
`select count(\`${tableName}\`.\`${removedColumn}\`) as count from \`${tableName}\``,
|
||||
);
|
||||
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about to delete ${
|
||||
chalk.underline(
|
||||
removedColumn,
|
||||
)
|
||||
} column in ${tableName} table with ${count} items`,
|
||||
);
|
||||
columnsToRemove.push(removedColumn);
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (addedColumns.length) {
|
||||
for (const addedColumn of addedColumns) {
|
||||
const [res] = await connection.query<{ count: string }>(
|
||||
`select count(*) as count from \`${tableName}\``,
|
||||
);
|
||||
|
||||
const columnConf = json2.tables[tableName].columns[addedColumn];
|
||||
|
||||
const count = Number(res.count);
|
||||
if (count > 0 && columnConf.notNull && !columnConf.default) {
|
||||
dataLoss = true;
|
||||
|
||||
infoToPrint.push(
|
||||
`· You're about to add not-null ${
|
||||
chalk.underline(
|
||||
addedColumn,
|
||||
)
|
||||
} column without default value to table, which contains ${count} items`,
|
||||
);
|
||||
shouldAskForApprove = true;
|
||||
tablesToTruncate.push(tableName);
|
||||
|
||||
statementsToExecute.push(`DELETE FROM \`${tableName}\`;`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check if some tables referencing current for pragma
|
||||
const tablesReferencingCurrent: string[] = [];
|
||||
|
||||
for (const table of Object.values(json2.tables)) {
|
||||
const tablesRefs = Object.values(json2.tables[table.name].foreignKeys)
|
||||
.filter((t) => SQLiteSquasher.unsquashPushFK(t).tableTo === tableName)
|
||||
.map((it) => SQLiteSquasher.unsquashPushFK(it).tableFrom);
|
||||
|
||||
tablesReferencingCurrent.push(...tablesRefs);
|
||||
}
|
||||
|
||||
if (!tablesReferencingCurrent.length) {
|
||||
statementsToExecute.push(..._moveDataStatements(tableName, json2, dataLoss));
|
||||
continue;
|
||||
}
|
||||
|
||||
// recreate table
|
||||
statementsToExecute.push(
|
||||
..._moveDataStatements(tableName, json2, dataLoss),
|
||||
);
|
||||
} else if (
|
||||
statement.type === 'alter_table_alter_column_set_generated'
|
||||
|| statement.type === 'alter_table_alter_column_drop_generated'
|
||||
) {
|
||||
const tableName = statement.tableName;
|
||||
|
||||
const res = await connection.query<{ count: string }>(
|
||||
`select count("${statement.columnName}") as count from \`${tableName}\``,
|
||||
);
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about to delete ${
|
||||
chalk.underline(
|
||||
statement.columnName,
|
||||
)
|
||||
} column in ${tableName} table with ${count} items`,
|
||||
);
|
||||
columnsToRemove.push(`${tableName}_${statement.columnName}`);
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
const fromJsonStatement = fromJson([statement], 'turso', 'push', json2);
|
||||
statementsToExecute.push(
|
||||
...(Array.isArray(fromJsonStatement) ? fromJsonStatement : [fromJsonStatement]),
|
||||
);
|
||||
} else {
|
||||
const fromJsonStatement = fromJson([statement], 'turso', 'push', json2);
|
||||
statementsToExecute.push(
|
||||
...(Array.isArray(fromJsonStatement) ? fromJsonStatement : [fromJsonStatement]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
statementsToExecute: [...new Set(statementsToExecute)],
|
||||
shouldAskForApprove,
|
||||
infoToPrint,
|
||||
columnsToRemove: [...new Set(columnsToRemove)],
|
||||
tablesToTruncate: [...new Set(tablesToTruncate)],
|
||||
tablesToRemove: [...new Set(tablesToRemove)],
|
||||
};
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,53 @@
|
||||
import { renderWithTask } from 'hanji';
|
||||
import { Minimatch } from 'minimatch';
|
||||
import { originUUID } from '../../global';
|
||||
import type { MySqlSchema } from '../../serializer/mysqlSchema';
|
||||
import { fromDatabase } from '../../serializer/mysqlSerializer';
|
||||
import type { DB } from '../../utils';
|
||||
import { ProgressView } from '../views';
|
||||
|
||||
export const mysqlPushIntrospect = async (
|
||||
db: DB,
|
||||
databaseName: string,
|
||||
filters: string[],
|
||||
) => {
|
||||
const matchers = filters.map((it) => {
|
||||
return new Minimatch(it);
|
||||
});
|
||||
|
||||
const filter = (tableName: string) => {
|
||||
if (matchers.length === 0) return true;
|
||||
|
||||
let flags: boolean[] = [];
|
||||
|
||||
for (let matcher of matchers) {
|
||||
if (matcher.negate) {
|
||||
if (!matcher.match(tableName)) {
|
||||
flags.push(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (matcher.match(tableName)) {
|
||||
flags.push(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (flags.length > 0) {
|
||||
return flags.every(Boolean);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const progress = new ProgressView(
|
||||
'Pulling schema from database...',
|
||||
'Pulling schema from database...',
|
||||
);
|
||||
const res = await renderWithTask(
|
||||
progress,
|
||||
fromDatabase(db, databaseName, filter),
|
||||
);
|
||||
|
||||
const schema = { id: originUUID, prevId: '', ...res } as MySqlSchema;
|
||||
const { internal, ...schemaWithoutInternals } = schema;
|
||||
return { schema: schemaWithoutInternals };
|
||||
};
|
||||
@@ -0,0 +1,352 @@
|
||||
import chalk from 'chalk';
|
||||
import { render } from 'hanji';
|
||||
import { TypeOf } from 'zod';
|
||||
import { JsonAlterColumnTypeStatement, JsonStatement } from '../../jsonStatements';
|
||||
import { mysqlSchema, MySqlSquasher } from '../../serializer/mysqlSchema';
|
||||
import type { DB } from '../../utils';
|
||||
import { Select } from '../selector-ui';
|
||||
import { withStyle } from '../validations/outputs';
|
||||
|
||||
export const filterStatements = (
|
||||
statements: JsonStatement[],
|
||||
currentSchema: TypeOf<typeof mysqlSchema>,
|
||||
prevSchema: TypeOf<typeof mysqlSchema>,
|
||||
) => {
|
||||
return statements.filter((statement) => {
|
||||
if (statement.type === 'alter_table_alter_column_set_type') {
|
||||
// Don't need to handle it on migrations step and introspection
|
||||
// but for both it should be skipped
|
||||
if (
|
||||
statement.oldDataType.startsWith('tinyint')
|
||||
&& statement.newDataType.startsWith('boolean')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
statement.oldDataType.startsWith('bigint unsigned')
|
||||
&& statement.newDataType.startsWith('serial')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
statement.oldDataType.startsWith('serial')
|
||||
&& statement.newDataType.startsWith('bigint unsigned')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} else if (statement.type === 'alter_table_alter_column_set_default') {
|
||||
if (
|
||||
statement.newDefaultValue === false
|
||||
&& statement.oldDefaultValue === 0
|
||||
&& statement.newDataType === 'boolean'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
statement.newDefaultValue === true
|
||||
&& statement.oldDefaultValue === 1
|
||||
&& statement.newDataType === 'boolean'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} else if (statement.type === 'delete_unique_constraint') {
|
||||
const unsquashed = MySqlSquasher.unsquashUnique(statement.data);
|
||||
// only if constraint was removed from a serial column, than treat it as removed
|
||||
// const serialStatement = statements.find(
|
||||
// (it) => it.type === "alter_table_alter_column_set_type"
|
||||
// ) as JsonAlterColumnTypeStatement;
|
||||
// if (
|
||||
// serialStatement?.oldDataType.startsWith("bigint unsigned") &&
|
||||
// serialStatement?.newDataType.startsWith("serial") &&
|
||||
// serialStatement.columnName ===
|
||||
// MySqlSquasher.unsquashUnique(statement.data).columns[0]
|
||||
// ) {
|
||||
// return false;
|
||||
// }
|
||||
// Check if uniqueindex was only on this column, that is serial
|
||||
|
||||
// if now serial and was not serial and was unique index
|
||||
if (
|
||||
unsquashed.columns.length === 1
|
||||
&& currentSchema.tables[statement.tableName].columns[unsquashed.columns[0]]
|
||||
.type === 'serial'
|
||||
&& prevSchema.tables[statement.tableName].columns[unsquashed.columns[0]]
|
||||
.type === 'serial'
|
||||
&& currentSchema.tables[statement.tableName].columns[unsquashed.columns[0]]
|
||||
.name === unsquashed.columns[0]
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} else if (statement.type === 'alter_table_alter_column_drop_notnull') {
|
||||
// only if constraint was removed from a serial column, than treat it as removed
|
||||
const serialStatement = statements.find(
|
||||
(it) => it.type === 'alter_table_alter_column_set_type',
|
||||
) as JsonAlterColumnTypeStatement;
|
||||
if (
|
||||
serialStatement?.oldDataType.startsWith('bigint unsigned')
|
||||
&& serialStatement?.newDataType.startsWith('serial')
|
||||
&& serialStatement.columnName === statement.columnName
|
||||
&& serialStatement.tableName === statement.tableName
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (statement.newDataType === 'serial' && !statement.columnNotNull) {
|
||||
return false;
|
||||
}
|
||||
if (statement.columnAutoIncrement) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
export const logSuggestionsAndReturn = async (
|
||||
db: DB,
|
||||
statements: JsonStatement[],
|
||||
json2: TypeOf<typeof mysqlSchema>,
|
||||
) => {
|
||||
let shouldAskForApprove = false;
|
||||
const statementsToExecute: string[] = [];
|
||||
const infoToPrint: string[] = [];
|
||||
|
||||
const tablesToRemove: string[] = [];
|
||||
const columnsToRemove: string[] = [];
|
||||
const schemasToRemove: string[] = [];
|
||||
const tablesToTruncate: string[] = [];
|
||||
|
||||
for (const statement of statements) {
|
||||
if (statement.type === 'drop_table') {
|
||||
const res = await db.query(
|
||||
`select count(*) as count from \`${statement.tableName}\``,
|
||||
);
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about to delete ${
|
||||
chalk.underline(
|
||||
statement.tableName,
|
||||
)
|
||||
} table with ${count} items`,
|
||||
);
|
||||
tablesToRemove.push(statement.tableName);
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
} else if (statement.type === 'alter_table_drop_column') {
|
||||
const res = await db.query(
|
||||
`select count(*) as count from \`${statement.tableName}\``,
|
||||
);
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about to delete ${
|
||||
chalk.underline(
|
||||
statement.columnName,
|
||||
)
|
||||
} column in ${statement.tableName} table with ${count} items`,
|
||||
);
|
||||
columnsToRemove.push(`${statement.tableName}_${statement.columnName}`);
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
} else if (statement.type === 'drop_schema') {
|
||||
const res = await db.query(
|
||||
`select count(*) as count from information_schema.tables where table_schema = \`${statement.name}\`;`,
|
||||
);
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about to delete ${
|
||||
chalk.underline(
|
||||
statement.name,
|
||||
)
|
||||
} schema with ${count} tables`,
|
||||
);
|
||||
schemasToRemove.push(statement.name);
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
} else if (statement.type === 'alter_table_alter_column_set_type') {
|
||||
const res = await db.query(
|
||||
`select count(*) as count from \`${statement.tableName}\``,
|
||||
);
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about to change ${
|
||||
chalk.underline(
|
||||
statement.columnName,
|
||||
)
|
||||
} column type from ${
|
||||
chalk.underline(
|
||||
statement.oldDataType,
|
||||
)
|
||||
} to ${chalk.underline(statement.newDataType)} with ${count} items`,
|
||||
);
|
||||
statementsToExecute.push(`truncate table ${statement.tableName};`);
|
||||
tablesToTruncate.push(statement.tableName);
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
} else if (statement.type === 'alter_table_alter_column_drop_default') {
|
||||
if (statement.columnNotNull) {
|
||||
const res = await db.query(
|
||||
`select count(*) as count from \`${statement.tableName}\``,
|
||||
);
|
||||
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about to remove default value from ${
|
||||
chalk.underline(
|
||||
statement.columnName,
|
||||
)
|
||||
} not-null column with ${count} items`,
|
||||
);
|
||||
|
||||
tablesToTruncate.push(statement.tableName);
|
||||
statementsToExecute.push(`truncate table ${statement.tableName};`);
|
||||
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
}
|
||||
// shouldAskForApprove = true;
|
||||
} else if (statement.type === 'alter_table_alter_column_set_notnull') {
|
||||
if (typeof statement.columnDefault === 'undefined') {
|
||||
const res = await db.query(
|
||||
`select count(*) as count from \`${statement.tableName}\``,
|
||||
);
|
||||
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about to set not-null constraint to ${
|
||||
chalk.underline(
|
||||
statement.columnName,
|
||||
)
|
||||
} column without default, which contains ${count} items`,
|
||||
);
|
||||
|
||||
tablesToTruncate.push(statement.tableName);
|
||||
statementsToExecute.push(`truncate table ${statement.tableName};`);
|
||||
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
}
|
||||
} else if (statement.type === 'alter_table_alter_column_drop_pk') {
|
||||
const res = await db.query(
|
||||
`select count(*) as count from \`${statement.tableName}\``,
|
||||
);
|
||||
|
||||
// if drop pk and json2 has autoincrement in table -> exit process with error
|
||||
if (
|
||||
Object.values(json2.tables[statement.tableName].columns).filter(
|
||||
(column) => column.autoincrement,
|
||||
).length > 0
|
||||
) {
|
||||
console.log(
|
||||
`${
|
||||
withStyle.errorWarning(
|
||||
`You have removed the primary key from a ${statement.tableName} table without removing the auto-increment property from this table. As the database error states: 'there can be only one auto column, and it must be defined as a key. Make sure to remove autoincrement from ${statement.tableName} table`,
|
||||
)
|
||||
}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about to change ${
|
||||
chalk.underline(
|
||||
statement.tableName,
|
||||
)
|
||||
} primary key. This statements may fail and you table may left without primary key`,
|
||||
);
|
||||
|
||||
tablesToTruncate.push(statement.tableName);
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
} else if (statement.type === 'delete_composite_pk') {
|
||||
// if drop pk and json2 has autoincrement in table -> exit process with error
|
||||
if (
|
||||
Object.values(json2.tables[statement.tableName].columns).filter(
|
||||
(column) => column.autoincrement,
|
||||
).length > 0
|
||||
) {
|
||||
console.log(
|
||||
`${
|
||||
withStyle.errorWarning(
|
||||
`You have removed the primary key from a ${statement.tableName} table without removing the auto-increment property from this table. As the database error states: 'there can be only one auto column, and it must be defined as a key. Make sure to remove autoincrement from ${statement.tableName} table`,
|
||||
)
|
||||
}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (statement.type === 'alter_table_add_column') {
|
||||
if (
|
||||
statement.column.notNull
|
||||
&& typeof statement.column.default === 'undefined'
|
||||
) {
|
||||
const res = await db.query(
|
||||
`select count(*) as count from \`${statement.tableName}\``,
|
||||
);
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about to add not-null ${
|
||||
chalk.underline(
|
||||
statement.column.name,
|
||||
)
|
||||
} column without default value, which contains ${count} items`,
|
||||
);
|
||||
|
||||
tablesToTruncate.push(statement.tableName);
|
||||
statementsToExecute.push(`truncate table ${statement.tableName};`);
|
||||
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
}
|
||||
} else if (statement.type === 'create_unique_constraint') {
|
||||
const res = await db.query(
|
||||
`select count(*) as count from \`${statement.tableName}\``,
|
||||
);
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
const unsquashedUnique = MySqlSquasher.unsquashUnique(statement.data);
|
||||
console.log(
|
||||
`· You're about to add ${
|
||||
chalk.underline(
|
||||
unsquashedUnique.name,
|
||||
)
|
||||
} unique constraint to the table, which contains ${count} items. If this statement fails, you will receive an error from the database. Do you want to truncate ${
|
||||
chalk.underline(
|
||||
statement.tableName,
|
||||
)
|
||||
} table?\n`,
|
||||
);
|
||||
const { status, data } = await render(
|
||||
new Select([
|
||||
'No, add the constraint without truncating the table',
|
||||
`Yes, truncate the table`,
|
||||
]),
|
||||
);
|
||||
if (data?.index === 1) {
|
||||
tablesToTruncate.push(statement.tableName);
|
||||
statementsToExecute.push(`truncate table ${statement.tableName};`);
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
statementsToExecute,
|
||||
shouldAskForApprove,
|
||||
infoToPrint,
|
||||
columnsToRemove: [...new Set(columnsToRemove)],
|
||||
schemasToRemove: [...new Set(schemasToRemove)],
|
||||
tablesToTruncate: [...new Set(tablesToTruncate)],
|
||||
tablesToRemove: [...new Set(tablesToRemove)],
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
import chalk from 'chalk';
|
||||
import fs, { writeFileSync } from 'fs';
|
||||
import path from 'path';
|
||||
import { Column, MySqlSchema, MySqlSchemaV4, MySqlSchemaV5, mysqlSchemaV5, Table } from '../../serializer/mysqlSchema';
|
||||
import { prepareOutFolder, validateWithReport } from '../../utils';
|
||||
|
||||
export const upMysqlHandler = (out: string) => {};
|
||||
|
||||
export const upMySqlHandlerV4toV5 = (obj: MySqlSchemaV4): MySqlSchemaV5 => {
|
||||
const mappedTables: Record<string, Table> = {};
|
||||
|
||||
for (const [key, table] of Object.entries(obj.tables)) {
|
||||
const mappedColumns: Record<string, Column> = {};
|
||||
for (const [ckey, column] of Object.entries(table.columns)) {
|
||||
let newDefault: any = column.default;
|
||||
let newType: string = column.type;
|
||||
let newAutoIncrement: boolean | undefined = column.autoincrement;
|
||||
|
||||
if (column.type.toLowerCase().startsWith('datetime')) {
|
||||
if (typeof column.default !== 'undefined') {
|
||||
if (column.default.startsWith("'") && column.default.endsWith("'")) {
|
||||
newDefault = `'${
|
||||
column.default
|
||||
.substring(1, column.default.length - 1)
|
||||
.replace('T', ' ')
|
||||
.slice(0, 23)
|
||||
}'`;
|
||||
} else {
|
||||
newDefault = column.default.replace('T', ' ').slice(0, 23);
|
||||
}
|
||||
}
|
||||
|
||||
newType = column.type.toLowerCase().replace('datetime (', 'datetime(');
|
||||
} else if (column.type.toLowerCase() === 'date') {
|
||||
if (typeof column.default !== 'undefined') {
|
||||
if (column.default.startsWith("'") && column.default.endsWith("'")) {
|
||||
newDefault = `'${
|
||||
column.default
|
||||
.substring(1, column.default.length - 1)
|
||||
.split('T')[0]
|
||||
}'`;
|
||||
} else {
|
||||
newDefault = column.default.split('T')[0];
|
||||
}
|
||||
}
|
||||
newType = column.type.toLowerCase().replace('date (', 'date(');
|
||||
} else if (column.type.toLowerCase().startsWith('timestamp')) {
|
||||
if (typeof column.default !== 'undefined') {
|
||||
if (column.default.startsWith("'") && column.default.endsWith("'")) {
|
||||
newDefault = `'${
|
||||
column.default
|
||||
.substring(1, column.default.length - 1)
|
||||
.replace('T', ' ')
|
||||
.slice(0, 23)
|
||||
}'`;
|
||||
} else {
|
||||
newDefault = column.default.replace('T', ' ').slice(0, 23);
|
||||
}
|
||||
}
|
||||
newType = column.type
|
||||
.toLowerCase()
|
||||
.replace('timestamp (', 'timestamp(');
|
||||
} else if (column.type.toLowerCase().startsWith('time')) {
|
||||
newType = column.type.toLowerCase().replace('time (', 'time(');
|
||||
} else if (column.type.toLowerCase().startsWith('decimal')) {
|
||||
newType = column.type.toLowerCase().replace(', ', ',');
|
||||
} else if (column.type.toLowerCase().startsWith('enum')) {
|
||||
newType = column.type.toLowerCase();
|
||||
} else if (column.type.toLowerCase().startsWith('serial')) {
|
||||
newAutoIncrement = true;
|
||||
}
|
||||
mappedColumns[ckey] = {
|
||||
...column,
|
||||
default: newDefault,
|
||||
type: newType,
|
||||
autoincrement: newAutoIncrement,
|
||||
};
|
||||
}
|
||||
|
||||
mappedTables[key] = {
|
||||
...table,
|
||||
columns: mappedColumns,
|
||||
compositePrimaryKeys: {},
|
||||
uniqueConstraints: {},
|
||||
checkConstraint: {},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
version: '5',
|
||||
dialect: obj.dialect,
|
||||
id: obj.id,
|
||||
prevId: obj.prevId,
|
||||
tables: mappedTables,
|
||||
schemas: obj.schemas,
|
||||
_meta: {
|
||||
schemas: {} as Record<string, string>,
|
||||
tables: {} as Record<string, string>,
|
||||
columns: {} as Record<string, string>,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { renderWithTask } from 'hanji';
|
||||
import { Minimatch } from 'minimatch';
|
||||
import { originUUID } from '../../global';
|
||||
import type { PgSchema, PgSchemaInternal } from '../../serializer/pgSchema';
|
||||
import { fromDatabase } from '../../serializer/pgSerializer';
|
||||
import type { DB } from '../../utils';
|
||||
import type { Entities } from '../validations/cli';
|
||||
import { ProgressView } from '../views';
|
||||
|
||||
export const pgPushIntrospect = async (
|
||||
db: DB,
|
||||
filters: string[],
|
||||
schemaFilters: string[],
|
||||
entities: Entities,
|
||||
tsSchema?: PgSchemaInternal,
|
||||
) => {
|
||||
const matchers = filters.map((it) => {
|
||||
return new Minimatch(it);
|
||||
});
|
||||
|
||||
const filter = (tableName: string) => {
|
||||
if (matchers.length === 0) return true;
|
||||
|
||||
let flags: boolean[] = [];
|
||||
|
||||
for (let matcher of matchers) {
|
||||
if (matcher.negate) {
|
||||
if (!matcher.match(tableName)) {
|
||||
flags.push(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (matcher.match(tableName)) {
|
||||
flags.push(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (flags.length > 0) {
|
||||
return flags.every(Boolean);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const progress = new ProgressView(
|
||||
'Pulling schema from database...',
|
||||
'Pulling schema from database...',
|
||||
);
|
||||
const res = await renderWithTask(
|
||||
progress,
|
||||
fromDatabase(db, filter, schemaFilters, entities, undefined, tsSchema),
|
||||
);
|
||||
|
||||
const schema = { id: originUUID, prevId: '', ...res } as PgSchema;
|
||||
const { internal, ...schemaWithoutInternals } = schema;
|
||||
return { schema: schemaWithoutInternals };
|
||||
};
|
||||
@@ -0,0 +1,269 @@
|
||||
import chalk from 'chalk';
|
||||
import { render } from 'hanji';
|
||||
import type { JsonStatement } from '../../jsonStatements';
|
||||
import { PgSquasher } from '../../serializer/pgSchema';
|
||||
import { fromJson } from '../../sqlgenerator';
|
||||
import type { DB } from '../../utils';
|
||||
import { Select } from '../selector-ui';
|
||||
|
||||
// export const filterStatements = (statements: JsonStatement[]) => {
|
||||
// return statements.filter((statement) => {
|
||||
// if (statement.type === "alter_table_alter_column_set_type") {
|
||||
// // Don't need to handle it on migrations step and introspection
|
||||
// // but for both it should be skipped
|
||||
// if (
|
||||
// statement.oldDataType.startsWith("tinyint") &&
|
||||
// statement.newDataType.startsWith("boolean")
|
||||
// ) {
|
||||
// return false;
|
||||
// }
|
||||
// } else if (statement.type === "alter_table_alter_column_set_default") {
|
||||
// if (
|
||||
// statement.newDefaultValue === false &&
|
||||
// statement.oldDefaultValue === 0 &&
|
||||
// statement.newDataType === "boolean"
|
||||
// ) {
|
||||
// return false;
|
||||
// }
|
||||
// if (
|
||||
// statement.newDefaultValue === true &&
|
||||
// statement.oldDefaultValue === 1 &&
|
||||
// statement.newDataType === "boolean"
|
||||
// ) {
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
// return true;
|
||||
// });
|
||||
// };
|
||||
|
||||
function concatSchemaAndTableName(schema: string | undefined, table: string) {
|
||||
return schema ? `"${schema}"."${table}"` : `"${table}"`;
|
||||
}
|
||||
|
||||
function tableNameWithSchemaFrom(
|
||||
schema: string | undefined,
|
||||
tableName: string,
|
||||
renamedSchemas: Record<string, string>,
|
||||
renamedTables: Record<string, string>,
|
||||
) {
|
||||
const newSchemaName = schema ? (renamedSchemas[schema] ? renamedSchemas[schema] : schema) : undefined;
|
||||
|
||||
const newTableName = renamedTables[concatSchemaAndTableName(newSchemaName, tableName)]
|
||||
? renamedTables[concatSchemaAndTableName(newSchemaName, tableName)]
|
||||
: tableName;
|
||||
|
||||
return concatSchemaAndTableName(newSchemaName, newTableName);
|
||||
}
|
||||
|
||||
export const pgSuggestions = async (db: DB, statements: JsonStatement[]) => {
|
||||
let shouldAskForApprove = false;
|
||||
const statementsToExecute: string[] = [];
|
||||
const infoToPrint: string[] = [];
|
||||
|
||||
const tablesToRemove: string[] = [];
|
||||
const columnsToRemove: string[] = [];
|
||||
const schemasToRemove: string[] = [];
|
||||
const tablesToTruncate: string[] = [];
|
||||
const matViewsToRemove: string[] = [];
|
||||
|
||||
let renamedSchemas: Record<string, string> = {};
|
||||
let renamedTables: Record<string, string> = {};
|
||||
|
||||
for (const statement of statements) {
|
||||
if (statement.type === 'rename_schema') {
|
||||
renamedSchemas[statement.to] = statement.from;
|
||||
} else if (statement.type === 'rename_table') {
|
||||
renamedTables[concatSchemaAndTableName(statement.toSchema, statement.tableNameTo)] = statement.tableNameFrom;
|
||||
} else if (statement.type === 'drop_table') {
|
||||
const res = await db.query(
|
||||
`select count(*) as count from ${
|
||||
tableNameWithSchemaFrom(statement.schema, statement.tableName, renamedSchemas, renamedTables)
|
||||
}`,
|
||||
);
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(`· You're about to delete ${chalk.underline(statement.tableName)} table with ${count} items`);
|
||||
// statementsToExecute.push(
|
||||
// `truncate table ${tableNameWithSchemaFrom(statement)} cascade;`
|
||||
// );
|
||||
tablesToRemove.push(statement.tableName);
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
} else if (statement.type === 'drop_view' && statement.materialized) {
|
||||
const res = await db.query(`select count(*) as count from "${statement.schema ?? 'public'}"."${statement.name}"`);
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about to delete "${chalk.underline(statement.name)}" materialized view with ${count} items`,
|
||||
);
|
||||
|
||||
matViewsToRemove.push(statement.name);
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
} else if (statement.type === 'alter_table_drop_column') {
|
||||
const res = await db.query(
|
||||
`select count(*) as count from ${
|
||||
tableNameWithSchemaFrom(statement.schema, statement.tableName, renamedSchemas, renamedTables)
|
||||
}`,
|
||||
);
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about to delete ${
|
||||
chalk.underline(statement.columnName)
|
||||
} column in ${statement.tableName} table with ${count} items`,
|
||||
);
|
||||
columnsToRemove.push(`${statement.tableName}_${statement.columnName}`);
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
} else if (statement.type === 'drop_schema') {
|
||||
const res = await db.query(
|
||||
`select count(*) as count from information_schema.tables where table_schema = '${statement.name}';`,
|
||||
);
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(`· You're about to delete ${chalk.underline(statement.name)} schema with ${count} tables`);
|
||||
schemasToRemove.push(statement.name);
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
} else if (statement.type === 'alter_table_alter_column_set_type') {
|
||||
const res = await db.query(
|
||||
`select count(*) as count from ${
|
||||
tableNameWithSchemaFrom(statement.schema, statement.tableName, renamedSchemas, renamedTables)
|
||||
}`,
|
||||
);
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about to change ${chalk.underline(statement.columnName)} column type from ${
|
||||
chalk.underline(statement.oldDataType)
|
||||
} to ${
|
||||
chalk.underline(
|
||||
statement.newDataType,
|
||||
)
|
||||
} with ${count} items`,
|
||||
);
|
||||
statementsToExecute.push(
|
||||
`truncate table ${
|
||||
tableNameWithSchemaFrom(statement.schema, statement.tableName, renamedSchemas, renamedTables)
|
||||
} cascade;`,
|
||||
);
|
||||
tablesToTruncate.push(statement.tableName);
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
} else if (statement.type === 'alter_table_alter_column_drop_pk') {
|
||||
const res = await db.query(
|
||||
`select count(*) as count from ${
|
||||
tableNameWithSchemaFrom(statement.schema, statement.tableName, renamedSchemas, renamedTables)
|
||||
}`,
|
||||
);
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about to change ${
|
||||
chalk.underline(statement.tableName)
|
||||
} primary key. This statements may fail and you table may left without primary key`,
|
||||
);
|
||||
|
||||
tablesToTruncate.push(statement.tableName);
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
|
||||
const tableNameWithSchema = tableNameWithSchemaFrom(
|
||||
statement.schema,
|
||||
statement.tableName,
|
||||
renamedSchemas,
|
||||
renamedTables,
|
||||
);
|
||||
|
||||
const pkNameResponse = await db.query(
|
||||
`SELECT constraint_name FROM information_schema.table_constraints
|
||||
WHERE table_schema = '${
|
||||
typeof statement.schema === 'undefined' || statement.schema === '' ? 'public' : statement.schema
|
||||
}'
|
||||
AND table_name = '${statement.tableName}'
|
||||
AND constraint_type = 'PRIMARY KEY';`,
|
||||
);
|
||||
|
||||
statementsToExecute.push(
|
||||
`ALTER TABLE ${tableNameWithSchema} DROP CONSTRAINT "${pkNameResponse[0].constraint_name}"`,
|
||||
);
|
||||
// we will generate statement for drop pk here and not after all if-else statements
|
||||
continue;
|
||||
} else if (statement.type === 'alter_table_add_column') {
|
||||
if (statement.column.notNull && typeof statement.column.default === 'undefined') {
|
||||
const res = await db.query(
|
||||
`select count(*) as count from ${
|
||||
tableNameWithSchemaFrom(statement.schema, statement.tableName, renamedSchemas, renamedTables)
|
||||
}`,
|
||||
);
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about to add not-null ${
|
||||
chalk.underline(statement.column.name)
|
||||
} column without default value, which contains ${count} items`,
|
||||
);
|
||||
|
||||
tablesToTruncate.push(statement.tableName);
|
||||
statementsToExecute.push(
|
||||
`truncate table ${
|
||||
tableNameWithSchemaFrom(statement.schema, statement.tableName, renamedSchemas, renamedTables)
|
||||
} cascade;`,
|
||||
);
|
||||
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
}
|
||||
} else if (statement.type === 'create_unique_constraint') {
|
||||
const res = await db.query(
|
||||
`select count(*) as count from ${
|
||||
tableNameWithSchemaFrom(statement.schema, statement.tableName, renamedSchemas, renamedTables)
|
||||
}`,
|
||||
);
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
const unsquashedUnique = PgSquasher.unsquashUnique(statement.data);
|
||||
console.log(
|
||||
`· You're about to add ${
|
||||
chalk.underline(
|
||||
unsquashedUnique.name,
|
||||
)
|
||||
} unique constraint to the table, which contains ${count} items. If this statement fails, you will receive an error from the database. Do you want to truncate ${
|
||||
chalk.underline(
|
||||
statement.tableName,
|
||||
)
|
||||
} table?\n`,
|
||||
);
|
||||
const { status, data } = await render(
|
||||
new Select(['No, add the constraint without truncating the table', `Yes, truncate the table`]),
|
||||
);
|
||||
if (data?.index === 1) {
|
||||
tablesToTruncate.push(statement.tableName);
|
||||
statementsToExecute.push(
|
||||
`truncate table ${
|
||||
tableNameWithSchemaFrom(statement.schema, statement.tableName, renamedSchemas, renamedTables)
|
||||
} cascade;`,
|
||||
);
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
const stmnt = fromJson([statement], 'postgresql', 'push');
|
||||
if (typeof stmnt !== 'undefined') {
|
||||
statementsToExecute.push(...stmnt);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
statementsToExecute: [...new Set(statementsToExecute)],
|
||||
shouldAskForApprove,
|
||||
infoToPrint,
|
||||
matViewsToRemove: [...new Set(matViewsToRemove)],
|
||||
columnsToRemove: [...new Set(columnsToRemove)],
|
||||
schemasToRemove: [...new Set(schemasToRemove)],
|
||||
tablesToTruncate: [...new Set(tablesToTruncate)],
|
||||
tablesToRemove: [...new Set(tablesToRemove)],
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,179 @@
|
||||
import chalk from 'chalk';
|
||||
import { writeFileSync } from 'fs';
|
||||
import {
|
||||
Column,
|
||||
Index,
|
||||
PgSchema,
|
||||
PgSchemaV4,
|
||||
PgSchemaV5,
|
||||
pgSchemaV5,
|
||||
PgSchemaV6,
|
||||
pgSchemaV6,
|
||||
Table,
|
||||
TableV5,
|
||||
} from '../../serializer/pgSchema';
|
||||
import { prepareOutFolder, validateWithReport } from '../../utils';
|
||||
|
||||
export const upPgHandler = (out: string) => {
|
||||
const { snapshots } = prepareOutFolder(out, 'postgresql');
|
||||
const report = validateWithReport(snapshots, 'postgresql');
|
||||
|
||||
report.nonLatest
|
||||
.map((it) => ({
|
||||
path: it,
|
||||
raw: report.rawMap[it]!! as Record<string, any>,
|
||||
}))
|
||||
.forEach((it) => {
|
||||
const path = it.path;
|
||||
|
||||
let resultV6 = it.raw;
|
||||
if (it.raw.version === '5') {
|
||||
resultV6 = updateUpToV6(it.raw);
|
||||
}
|
||||
|
||||
const result = updateUpToV7(resultV6);
|
||||
|
||||
console.log(`[${chalk.green('✓')}] ${path}`);
|
||||
|
||||
writeFileSync(path, JSON.stringify(result, null, 2));
|
||||
});
|
||||
|
||||
console.log("Everything's fine 🐶🔥");
|
||||
};
|
||||
|
||||
export const updateUpToV6 = (json: Record<string, any>): PgSchemaV6 => {
|
||||
const schema = pgSchemaV5.parse(json);
|
||||
const tables = Object.fromEntries(
|
||||
Object.entries(schema.tables).map((it) => {
|
||||
const table = it[1];
|
||||
const schema = table.schema || 'public';
|
||||
return [`${schema}.${table.name}`, table];
|
||||
}),
|
||||
);
|
||||
const enums = Object.fromEntries(
|
||||
Object.entries(schema.enums).map((it) => {
|
||||
const en = it[1];
|
||||
return [
|
||||
`public.${en.name}`,
|
||||
{
|
||||
name: en.name,
|
||||
schema: 'public',
|
||||
values: Object.values(en.values),
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
return {
|
||||
...schema,
|
||||
version: '6',
|
||||
dialect: 'postgresql',
|
||||
tables: tables,
|
||||
enums,
|
||||
};
|
||||
};
|
||||
|
||||
// Changed index format stored in snapshot for PostgreSQL in 0.22.0
|
||||
export const updateUpToV7 = (json: Record<string, any>): PgSchema => {
|
||||
const schema = pgSchemaV6.parse(json);
|
||||
const tables = Object.fromEntries(
|
||||
Object.entries(schema.tables).map((it) => {
|
||||
const table = it[1];
|
||||
const mappedIndexes = Object.fromEntries(
|
||||
Object.entries(table.indexes).map((idx) => {
|
||||
const { columns, ...rest } = idx[1];
|
||||
const mappedColumns = columns.map<Index['columns'][number]>((it) => {
|
||||
return {
|
||||
expression: it,
|
||||
isExpression: false,
|
||||
asc: true,
|
||||
nulls: 'last',
|
||||
opClass: undefined,
|
||||
};
|
||||
});
|
||||
return [idx[0], { columns: mappedColumns, with: {}, ...rest }];
|
||||
}),
|
||||
);
|
||||
return [it[0], { ...table, indexes: mappedIndexes, policies: {}, isRLSEnabled: false, checkConstraints: {} }];
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
...schema,
|
||||
version: '7',
|
||||
dialect: 'postgresql',
|
||||
sequences: {},
|
||||
tables: tables,
|
||||
policies: {},
|
||||
views: {},
|
||||
roles: {},
|
||||
};
|
||||
};
|
||||
|
||||
// major migration with of folder structure, etc...
|
||||
export const upPgHandlerV4toV5 = (obj: PgSchemaV4): PgSchemaV5 => {
|
||||
const mappedTables: Record<string, TableV5> = {};
|
||||
|
||||
for (const [key, table] of Object.entries(obj.tables)) {
|
||||
const mappedColumns: Record<string, Column> = {};
|
||||
for (const [ckey, column] of Object.entries(table.columns)) {
|
||||
let newDefault: any = column.default;
|
||||
let newType: string = column.type;
|
||||
if (column.type.toLowerCase() === 'date') {
|
||||
if (typeof column.default !== 'undefined') {
|
||||
if (column.default.startsWith("'") && column.default.endsWith("'")) {
|
||||
newDefault = `'${
|
||||
column.default
|
||||
.substring(1, column.default.length - 1)
|
||||
.split('T')[0]
|
||||
}'`;
|
||||
} else {
|
||||
newDefault = column.default.split('T')[0];
|
||||
}
|
||||
}
|
||||
} else if (column.type.toLowerCase().startsWith('timestamp')) {
|
||||
if (typeof column.default !== 'undefined') {
|
||||
if (column.default.startsWith("'") && column.default.endsWith("'")) {
|
||||
newDefault = `'${
|
||||
column.default
|
||||
.substring(1, column.default.length - 1)
|
||||
.replace('T', ' ')
|
||||
.slice(0, 23)
|
||||
}'`;
|
||||
} else {
|
||||
newDefault = column.default.replace('T', ' ').slice(0, 23);
|
||||
}
|
||||
}
|
||||
newType = column.type
|
||||
.toLowerCase()
|
||||
.replace('timestamp (', 'timestamp(');
|
||||
} else if (column.type.toLowerCase().startsWith('time')) {
|
||||
newType = column.type.toLowerCase().replace('time (', 'time(');
|
||||
} else if (column.type.toLowerCase().startsWith('interval')) {
|
||||
newType = column.type.toLowerCase().replace(' (', '(');
|
||||
}
|
||||
mappedColumns[ckey] = { ...column, default: newDefault, type: newType };
|
||||
}
|
||||
|
||||
mappedTables[key] = {
|
||||
...table,
|
||||
columns: mappedColumns,
|
||||
compositePrimaryKeys: {},
|
||||
uniqueConstraints: {},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
version: '5',
|
||||
dialect: obj.dialect,
|
||||
id: obj.id,
|
||||
prevId: obj.prevId,
|
||||
tables: mappedTables,
|
||||
enums: obj.enums,
|
||||
schemas: obj.schemas,
|
||||
_meta: {
|
||||
schemas: {} as Record<string, string>,
|
||||
tables: {} as Record<string, string>,
|
||||
columns: {} as Record<string, string>,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,639 @@
|
||||
import chalk from 'chalk';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { render } from 'hanji';
|
||||
import { serializePg } from 'src/serializer';
|
||||
import { fromJson } from '../../sqlgenerator';
|
||||
import { Select } from '../selector-ui';
|
||||
import { Entities } from '../validations/cli';
|
||||
import { CasingType } from '../validations/common';
|
||||
import { LibSQLCredentials } from '../validations/libsql';
|
||||
import type { MysqlCredentials } from '../validations/mysql';
|
||||
import { withStyle } from '../validations/outputs';
|
||||
import type { PostgresCredentials } from '../validations/postgres';
|
||||
import { SingleStoreCredentials } from '../validations/singlestore';
|
||||
import type { SqliteCredentials } from '../validations/sqlite';
|
||||
import { libSqlLogSuggestionsAndReturn } from './libSqlPushUtils';
|
||||
import {
|
||||
filterStatements as mySqlFilterStatements,
|
||||
logSuggestionsAndReturn as mySqlLogSuggestionsAndReturn,
|
||||
} from './mysqlPushUtils';
|
||||
import { pgSuggestions } from './pgPushUtils';
|
||||
import {
|
||||
filterStatements as singleStoreFilterStatements,
|
||||
logSuggestionsAndReturn as singleStoreLogSuggestionsAndReturn,
|
||||
} from './singlestorePushUtils';
|
||||
import { logSuggestionsAndReturn as sqliteSuggestions } from './sqlitePushUtils';
|
||||
|
||||
export const mysqlPush = async (
|
||||
schemaPath: string | string[],
|
||||
credentials: MysqlCredentials,
|
||||
tablesFilter: string[],
|
||||
strict: boolean,
|
||||
verbose: boolean,
|
||||
force: boolean,
|
||||
casing: CasingType | undefined,
|
||||
) => {
|
||||
const { connectToMySQL } = await import('../connections');
|
||||
const { mysqlPushIntrospect } = await import('./mysqlIntrospect');
|
||||
|
||||
const { db, database } = await connectToMySQL(credentials);
|
||||
|
||||
const { schema } = await mysqlPushIntrospect(db, database, tablesFilter);
|
||||
const { prepareMySQLPush } = await import('./migrate');
|
||||
|
||||
const statements = await prepareMySQLPush(schemaPath, schema, casing);
|
||||
|
||||
const filteredStatements = mySqlFilterStatements(
|
||||
statements.statements ?? [],
|
||||
statements.validatedCur,
|
||||
statements.validatedPrev,
|
||||
);
|
||||
|
||||
try {
|
||||
if (filteredStatements.length === 0) {
|
||||
render(`[${chalk.blue('i')}] No changes detected`);
|
||||
} else {
|
||||
const {
|
||||
shouldAskForApprove,
|
||||
statementsToExecute,
|
||||
columnsToRemove,
|
||||
tablesToRemove,
|
||||
tablesToTruncate,
|
||||
infoToPrint,
|
||||
} = await mySqlLogSuggestionsAndReturn(
|
||||
db,
|
||||
filteredStatements,
|
||||
statements.validatedCur,
|
||||
);
|
||||
|
||||
const filteredSqlStatements = fromJson(filteredStatements, 'mysql');
|
||||
|
||||
const uniqueSqlStatementsToExecute: string[] = [];
|
||||
statementsToExecute.forEach((ss) => {
|
||||
if (!uniqueSqlStatementsToExecute.includes(ss)) {
|
||||
uniqueSqlStatementsToExecute.push(ss);
|
||||
}
|
||||
});
|
||||
const uniqueFilteredSqlStatements: string[] = [];
|
||||
filteredSqlStatements.forEach((ss) => {
|
||||
if (!uniqueFilteredSqlStatements.includes(ss)) {
|
||||
uniqueFilteredSqlStatements.push(ss);
|
||||
}
|
||||
});
|
||||
|
||||
if (verbose) {
|
||||
console.log();
|
||||
console.log(
|
||||
withStyle.warning('You are about to execute current statements:'),
|
||||
);
|
||||
console.log();
|
||||
console.log(
|
||||
[...uniqueSqlStatementsToExecute, ...uniqueFilteredSqlStatements]
|
||||
.map((s) => chalk.blue(s))
|
||||
.join('\n'),
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
|
||||
if (!force && strict) {
|
||||
if (!shouldAskForApprove) {
|
||||
const { status, data } = await render(
|
||||
new Select(['No, abort', `Yes, I want to execute all statements`]),
|
||||
);
|
||||
if (data?.index === 0) {
|
||||
render(`[${chalk.red('x')}] All changes were aborted`);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!force && shouldAskForApprove) {
|
||||
console.log(withStyle.warning('Found data-loss statements:'));
|
||||
console.log(infoToPrint.join('\n'));
|
||||
console.log();
|
||||
console.log(
|
||||
chalk.red.bold(
|
||||
'THIS ACTION WILL CAUSE DATA LOSS AND CANNOT BE REVERTED\n',
|
||||
),
|
||||
);
|
||||
|
||||
console.log(chalk.white('Do you still want to push changes?'));
|
||||
|
||||
const { status, data } = await render(
|
||||
new Select([
|
||||
'No, abort',
|
||||
`Yes, I want to${
|
||||
tablesToRemove.length > 0
|
||||
? ` remove ${tablesToRemove.length} ${tablesToRemove.length > 1 ? 'tables' : 'table'},`
|
||||
: ' '
|
||||
}${
|
||||
columnsToRemove.length > 0
|
||||
? ` remove ${columnsToRemove.length} ${columnsToRemove.length > 1 ? 'columns' : 'column'},`
|
||||
: ' '
|
||||
}${
|
||||
tablesToTruncate.length > 0
|
||||
? ` truncate ${tablesToTruncate.length} ${tablesToTruncate.length > 1 ? 'tables' : 'table'}`
|
||||
: ''
|
||||
}`
|
||||
.replace(/(^,)|(,$)/g, '')
|
||||
.replace(/ +(?= )/g, ''),
|
||||
]),
|
||||
);
|
||||
if (data?.index === 0) {
|
||||
render(`[${chalk.red('x')}] All changes were aborted`);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
for (const dStmnt of uniqueSqlStatementsToExecute) {
|
||||
await db.query(dStmnt);
|
||||
}
|
||||
|
||||
for (const statement of uniqueFilteredSqlStatements) {
|
||||
await db.query(statement);
|
||||
}
|
||||
if (filteredStatements.length > 0) {
|
||||
render(`[${chalk.green('✓')}] Changes applied`);
|
||||
} else {
|
||||
render(`[${chalk.blue('i')}] No changes detected`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
export const singlestorePush = async (
|
||||
schemaPath: string | string[],
|
||||
credentials: SingleStoreCredentials,
|
||||
tablesFilter: string[],
|
||||
strict: boolean,
|
||||
verbose: boolean,
|
||||
force: boolean,
|
||||
casing: CasingType | undefined,
|
||||
) => {
|
||||
const { connectToSingleStore } = await import('../connections');
|
||||
const { singlestorePushIntrospect } = await import('./singlestoreIntrospect');
|
||||
|
||||
const { db, database } = await connectToSingleStore(credentials);
|
||||
|
||||
const { schema } = await singlestorePushIntrospect(
|
||||
db,
|
||||
database,
|
||||
tablesFilter,
|
||||
);
|
||||
const { prepareSingleStorePush } = await import('./migrate');
|
||||
|
||||
const statements = await prepareSingleStorePush(schemaPath, schema, casing);
|
||||
|
||||
const filteredStatements = singleStoreFilterStatements(
|
||||
statements.statements ?? [],
|
||||
statements.validatedCur,
|
||||
statements.validatedPrev,
|
||||
);
|
||||
|
||||
try {
|
||||
if (filteredStatements.length === 0) {
|
||||
render(`[${chalk.blue('i')}] No changes detected`);
|
||||
} else {
|
||||
const {
|
||||
shouldAskForApprove,
|
||||
statementsToExecute,
|
||||
columnsToRemove,
|
||||
tablesToRemove,
|
||||
tablesToTruncate,
|
||||
infoToPrint,
|
||||
schemasToRemove,
|
||||
} = await singleStoreLogSuggestionsAndReturn(
|
||||
db,
|
||||
filteredStatements,
|
||||
statements.validatedCur,
|
||||
statements.validatedPrev,
|
||||
);
|
||||
|
||||
if (verbose) {
|
||||
console.log();
|
||||
console.log(
|
||||
withStyle.warning('You are about to execute current statements:'),
|
||||
);
|
||||
console.log();
|
||||
console.log(statementsToExecute.map((s) => chalk.blue(s)).join('\n'));
|
||||
console.log();
|
||||
}
|
||||
|
||||
if (!force && strict) {
|
||||
if (!shouldAskForApprove) {
|
||||
const { status, data } = await render(
|
||||
new Select(['No, abort', `Yes, I want to execute all statements`]),
|
||||
);
|
||||
if (data?.index === 0) {
|
||||
render(`[${chalk.red('x')}] All changes were aborted`);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!force && shouldAskForApprove) {
|
||||
console.log(withStyle.warning('Found data-loss statements:'));
|
||||
console.log(infoToPrint.join('\n'));
|
||||
console.log();
|
||||
console.log(
|
||||
chalk.red.bold(
|
||||
'THIS ACTION WILL CAUSE DATA LOSS AND CANNOT BE REVERTED\n',
|
||||
),
|
||||
);
|
||||
|
||||
console.log(chalk.white('Do you still want to push changes?'));
|
||||
|
||||
const { status, data } = await render(
|
||||
new Select([
|
||||
'No, abort',
|
||||
`Yes, I want to${
|
||||
tablesToRemove.length > 0
|
||||
? ` remove ${tablesToRemove.length} ${tablesToRemove.length > 1 ? 'tables' : 'table'},`
|
||||
: ' '
|
||||
}${
|
||||
columnsToRemove.length > 0
|
||||
? ` remove ${columnsToRemove.length} ${columnsToRemove.length > 1 ? 'columns' : 'column'},`
|
||||
: ' '
|
||||
}${
|
||||
tablesToTruncate.length > 0
|
||||
? ` truncate ${tablesToTruncate.length} ${tablesToTruncate.length > 1 ? 'tables' : 'table'}`
|
||||
: ''
|
||||
}`
|
||||
.replace(/(^,)|(,$)/g, '')
|
||||
.replace(/ +(?= )/g, ''),
|
||||
]),
|
||||
);
|
||||
if (data?.index === 0) {
|
||||
render(`[${chalk.red('x')}] All changes were aborted`);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
for (const dStmnt of statementsToExecute) {
|
||||
await db.query(dStmnt);
|
||||
}
|
||||
|
||||
if (filteredStatements.length > 0) {
|
||||
render(`[${chalk.green('✓')}] Changes applied`);
|
||||
} else {
|
||||
render(`[${chalk.blue('i')}] No changes detected`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
export const pgPush = async (
|
||||
schemaPath: string | string[],
|
||||
verbose: boolean,
|
||||
strict: boolean,
|
||||
credentials: PostgresCredentials,
|
||||
tablesFilter: string[],
|
||||
schemasFilter: string[],
|
||||
entities: Entities,
|
||||
force: boolean,
|
||||
casing: CasingType | undefined,
|
||||
) => {
|
||||
const { preparePostgresDB } = await import('../connections');
|
||||
const { pgPushIntrospect } = await import('./pgIntrospect');
|
||||
|
||||
const db = await preparePostgresDB(credentials);
|
||||
const serialized = await serializePg(schemaPath, casing, schemasFilter);
|
||||
|
||||
const { schema } = await pgPushIntrospect(db, tablesFilter, schemasFilter, entities, serialized);
|
||||
|
||||
const { preparePgPush } = await import('./migrate');
|
||||
|
||||
const statements = await preparePgPush(
|
||||
{ id: randomUUID(), prevId: schema.id, ...serialized },
|
||||
schema,
|
||||
);
|
||||
|
||||
try {
|
||||
if (statements.sqlStatements.length === 0) {
|
||||
render(`[${chalk.blue('i')}] No changes detected`);
|
||||
} else {
|
||||
// const filteredStatements = filterStatements(statements.statements);
|
||||
const {
|
||||
shouldAskForApprove,
|
||||
statementsToExecute,
|
||||
columnsToRemove,
|
||||
tablesToRemove,
|
||||
matViewsToRemove,
|
||||
tablesToTruncate,
|
||||
infoToPrint,
|
||||
schemasToRemove,
|
||||
} = await pgSuggestions(db, statements.statements);
|
||||
|
||||
if (verbose) {
|
||||
console.log();
|
||||
// console.log(chalk.gray('Verbose logs:'));
|
||||
console.log(
|
||||
withStyle.warning('You are about to execute current statements:'),
|
||||
);
|
||||
console.log();
|
||||
console.log(statementsToExecute.map((s) => chalk.blue(s)).join('\n'));
|
||||
console.log();
|
||||
}
|
||||
|
||||
if (!force && strict) {
|
||||
if (!shouldAskForApprove) {
|
||||
const { status, data } = await render(
|
||||
new Select(['No, abort', `Yes, I want to execute all statements`]),
|
||||
);
|
||||
if (data?.index === 0) {
|
||||
render(`[${chalk.red('x')}] All changes were aborted`);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!force && shouldAskForApprove) {
|
||||
console.log(withStyle.warning('Found data-loss statements:'));
|
||||
console.log(infoToPrint.join('\n'));
|
||||
console.log();
|
||||
console.log(
|
||||
chalk.red.bold(
|
||||
'THIS ACTION WILL CAUSE DATA LOSS AND CANNOT BE REVERTED\n',
|
||||
),
|
||||
);
|
||||
|
||||
console.log(chalk.white('Do you still want to push changes?'));
|
||||
|
||||
const { status, data } = await render(
|
||||
new Select([
|
||||
'No, abort',
|
||||
`Yes, I want to${
|
||||
tablesToRemove.length > 0
|
||||
? ` remove ${tablesToRemove.length} ${tablesToRemove.length > 1 ? 'tables' : 'table'},`
|
||||
: ' '
|
||||
}${
|
||||
columnsToRemove.length > 0
|
||||
? ` remove ${columnsToRemove.length} ${columnsToRemove.length > 1 ? 'columns' : 'column'},`
|
||||
: ' '
|
||||
}${
|
||||
tablesToTruncate.length > 0
|
||||
? ` truncate ${tablesToTruncate.length} ${tablesToTruncate.length > 1 ? 'tables' : 'table'}`
|
||||
: ''
|
||||
}${
|
||||
matViewsToRemove.length > 0
|
||||
? ` remove ${matViewsToRemove.length} ${
|
||||
matViewsToRemove.length > 1 ? 'materialized views' : 'materialize view'
|
||||
},`
|
||||
: ' '
|
||||
}`
|
||||
.replace(/(^,)|(,$)/g, '')
|
||||
.replace(/ +(?= )/g, ''),
|
||||
]),
|
||||
);
|
||||
if (data?.index === 0) {
|
||||
render(`[${chalk.red('x')}] All changes were aborted`);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
for (const dStmnt of statementsToExecute) {
|
||||
await db.query(dStmnt);
|
||||
}
|
||||
|
||||
if (statements.statements.length > 0) {
|
||||
render(`[${chalk.green('✓')}] Changes applied`);
|
||||
} else {
|
||||
render(`[${chalk.blue('i')}] No changes detected`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
export const sqlitePush = async (
|
||||
schemaPath: string | string[],
|
||||
verbose: boolean,
|
||||
strict: boolean,
|
||||
credentials: SqliteCredentials,
|
||||
tablesFilter: string[],
|
||||
force: boolean,
|
||||
casing: CasingType | undefined,
|
||||
) => {
|
||||
const { connectToSQLite } = await import('../connections');
|
||||
const { sqlitePushIntrospect } = await import('./sqliteIntrospect');
|
||||
|
||||
const db = await connectToSQLite(credentials);
|
||||
const { schema } = await sqlitePushIntrospect(db, tablesFilter);
|
||||
const { prepareSQLitePush } = await import('./migrate');
|
||||
|
||||
const statements = await prepareSQLitePush(schemaPath, schema, casing);
|
||||
|
||||
if (statements.sqlStatements.length === 0) {
|
||||
render(`\n[${chalk.blue('i')}] No changes detected`);
|
||||
} else {
|
||||
const {
|
||||
shouldAskForApprove,
|
||||
statementsToExecute,
|
||||
columnsToRemove,
|
||||
tablesToRemove,
|
||||
tablesToTruncate,
|
||||
infoToPrint,
|
||||
schemasToRemove,
|
||||
} = await sqliteSuggestions(
|
||||
db,
|
||||
statements.statements,
|
||||
statements.squashedPrev,
|
||||
statements.squashedCur,
|
||||
statements.meta!,
|
||||
);
|
||||
|
||||
if (verbose && statementsToExecute.length > 0) {
|
||||
console.log();
|
||||
console.log(
|
||||
withStyle.warning('You are about to execute current statements:'),
|
||||
);
|
||||
console.log();
|
||||
console.log(statementsToExecute.map((s) => chalk.blue(s)).join('\n'));
|
||||
console.log();
|
||||
}
|
||||
|
||||
if (!force && strict) {
|
||||
if (!shouldAskForApprove) {
|
||||
const { status, data } = await render(
|
||||
new Select(['No, abort', `Yes, I want to execute all statements`]),
|
||||
);
|
||||
if (data?.index === 0) {
|
||||
render(`[${chalk.red('x')}] All changes were aborted`);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!force && shouldAskForApprove) {
|
||||
console.log(withStyle.warning('Found data-loss statements:'));
|
||||
console.log(infoToPrint.join('\n'));
|
||||
console.log();
|
||||
console.log(
|
||||
chalk.red.bold(
|
||||
'THIS ACTION WILL CAUSE DATA LOSS AND CANNOT BE REVERTED\n',
|
||||
),
|
||||
);
|
||||
|
||||
console.log(chalk.white('Do you still want to push changes?'));
|
||||
|
||||
const { status, data } = await render(
|
||||
new Select([
|
||||
'No, abort',
|
||||
`Yes, I want to${
|
||||
tablesToRemove.length > 0
|
||||
? ` remove ${tablesToRemove.length} ${tablesToRemove.length > 1 ? 'tables' : 'table'},`
|
||||
: ' '
|
||||
}${
|
||||
columnsToRemove.length > 0
|
||||
? ` remove ${columnsToRemove.length} ${columnsToRemove.length > 1 ? 'columns' : 'column'},`
|
||||
: ' '
|
||||
}${
|
||||
tablesToTruncate.length > 0
|
||||
? ` truncate ${tablesToTruncate.length} ${tablesToTruncate.length > 1 ? 'tables' : 'table'}`
|
||||
: ''
|
||||
}`
|
||||
.trimEnd()
|
||||
.replace(/(^,)|(,$)/g, '')
|
||||
.replace(/ +(?= )/g, ''),
|
||||
]),
|
||||
);
|
||||
if (data?.index === 0) {
|
||||
render(`[${chalk.red('x')}] All changes were aborted`);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
if (statementsToExecute.length === 0) {
|
||||
render(`\n[${chalk.blue('i')}] No changes detected`);
|
||||
} else {
|
||||
// D1-HTTP does not support transactions
|
||||
// there might a be a better way to fix this
|
||||
// in the db connection itself
|
||||
const isNotD1 = !('driver' in credentials && credentials.driver === 'd1-http');
|
||||
isNotD1 ?? await db.run('begin');
|
||||
try {
|
||||
for (const dStmnt of statementsToExecute) {
|
||||
await db.run(dStmnt);
|
||||
}
|
||||
isNotD1 ?? await db.run('commit');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
isNotD1 ?? await db.run('rollback');
|
||||
process.exit(1);
|
||||
}
|
||||
render(`[${chalk.green('✓')}] Changes applied`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const libSQLPush = async (
|
||||
schemaPath: string | string[],
|
||||
verbose: boolean,
|
||||
strict: boolean,
|
||||
credentials: LibSQLCredentials,
|
||||
tablesFilter: string[],
|
||||
force: boolean,
|
||||
casing: CasingType | undefined,
|
||||
) => {
|
||||
const { connectToLibSQL } = await import('../connections');
|
||||
const { sqlitePushIntrospect } = await import('./sqliteIntrospect');
|
||||
|
||||
const db = await connectToLibSQL(credentials);
|
||||
const { schema } = await sqlitePushIntrospect(db, tablesFilter);
|
||||
|
||||
const { prepareLibSQLPush } = await import('./migrate');
|
||||
|
||||
const statements = await prepareLibSQLPush(schemaPath, schema, casing);
|
||||
|
||||
if (statements.sqlStatements.length === 0) {
|
||||
render(`\n[${chalk.blue('i')}] No changes detected`);
|
||||
} else {
|
||||
const {
|
||||
shouldAskForApprove,
|
||||
statementsToExecute,
|
||||
columnsToRemove,
|
||||
tablesToRemove,
|
||||
tablesToTruncate,
|
||||
infoToPrint,
|
||||
} = await libSqlLogSuggestionsAndReturn(
|
||||
db,
|
||||
statements.statements,
|
||||
statements.squashedPrev,
|
||||
statements.squashedCur,
|
||||
statements.meta!,
|
||||
);
|
||||
|
||||
if (verbose && statementsToExecute.length > 0) {
|
||||
console.log();
|
||||
console.log(
|
||||
withStyle.warning('You are about to execute current statements:'),
|
||||
);
|
||||
console.log();
|
||||
console.log(statementsToExecute.map((s) => chalk.blue(s)).join('\n'));
|
||||
console.log();
|
||||
}
|
||||
|
||||
if (!force && strict) {
|
||||
if (!shouldAskForApprove) {
|
||||
const { status, data } = await render(
|
||||
new Select(['No, abort', `Yes, I want to execute all statements`]),
|
||||
);
|
||||
if (data?.index === 0) {
|
||||
render(`[${chalk.red('x')}] All changes were aborted`);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!force && shouldAskForApprove) {
|
||||
console.log(withStyle.warning('Found data-loss statements:'));
|
||||
console.log(infoToPrint.join('\n'));
|
||||
console.log();
|
||||
console.log(
|
||||
chalk.red.bold(
|
||||
'THIS ACTION WILL CAUSE DATA LOSS AND CANNOT BE REVERTED\n',
|
||||
),
|
||||
);
|
||||
|
||||
console.log(chalk.white('Do you still want to push changes?'));
|
||||
|
||||
const { status, data } = await render(
|
||||
new Select([
|
||||
'No, abort',
|
||||
`Yes, I want to${
|
||||
tablesToRemove.length > 0
|
||||
? ` remove ${tablesToRemove.length} ${tablesToRemove.length > 1 ? 'tables' : 'table'},`
|
||||
: ' '
|
||||
}${
|
||||
columnsToRemove.length > 0
|
||||
? ` remove ${columnsToRemove.length} ${columnsToRemove.length > 1 ? 'columns' : 'column'},`
|
||||
: ' '
|
||||
}${
|
||||
tablesToTruncate.length > 0
|
||||
? ` truncate ${tablesToTruncate.length} ${tablesToTruncate.length > 1 ? 'tables' : 'table'}`
|
||||
: ''
|
||||
}`
|
||||
.trimEnd()
|
||||
.replace(/(^,)|(,$)/g, '')
|
||||
.replace(/ +(?= )/g, ''),
|
||||
]),
|
||||
);
|
||||
if (data?.index === 0) {
|
||||
render(`[${chalk.red('x')}] All changes were aborted`);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
if (statementsToExecute.length === 0) {
|
||||
render(`\n[${chalk.blue('i')}] No changes detected`);
|
||||
} else {
|
||||
await db.batchWithPragma!(statementsToExecute);
|
||||
render(`[${chalk.green('✓')}] Changes applied`);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import { renderWithTask } from 'hanji';
|
||||
import { Minimatch } from 'minimatch';
|
||||
import { originUUID } from '../../global';
|
||||
import type { SingleStoreSchema } from '../../serializer/singlestoreSchema';
|
||||
import { fromDatabase } from '../../serializer/singlestoreSerializer';
|
||||
import type { DB } from '../../utils';
|
||||
import { ProgressView } from '../views';
|
||||
|
||||
export const singlestorePushIntrospect = async (
|
||||
db: DB,
|
||||
databaseName: string,
|
||||
filters: string[],
|
||||
) => {
|
||||
const matchers = filters.map((it) => {
|
||||
return new Minimatch(it);
|
||||
});
|
||||
|
||||
const filter = (tableName: string) => {
|
||||
if (matchers.length === 0) return true;
|
||||
|
||||
let flags: boolean[] = [];
|
||||
|
||||
for (let matcher of matchers) {
|
||||
if (matcher.negate) {
|
||||
if (!matcher.match(tableName)) {
|
||||
flags.push(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (matcher.match(tableName)) {
|
||||
flags.push(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (flags.length > 0) {
|
||||
return flags.every(Boolean);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const progress = new ProgressView(
|
||||
'Pulling schema from database...',
|
||||
'Pulling schema from database...',
|
||||
);
|
||||
const res = await renderWithTask(
|
||||
progress,
|
||||
fromDatabase(db, databaseName, filter),
|
||||
);
|
||||
|
||||
const schema = { id: originUUID, prevId: '', ...res } as SingleStoreSchema;
|
||||
const { internal, ...schemaWithoutInternals } = schema;
|
||||
return { schema: schemaWithoutInternals };
|
||||
};
|
||||
@@ -0,0 +1,456 @@
|
||||
import chalk from 'chalk';
|
||||
import { render } from 'hanji';
|
||||
import { fromJson } from 'src/sqlgenerator';
|
||||
import { TypeOf } from 'zod';
|
||||
import { JsonAlterColumnTypeStatement, JsonStatement } from '../../jsonStatements';
|
||||
import { Column, SingleStoreSchemaSquashed, SingleStoreSquasher } from '../../serializer/singlestoreSchema';
|
||||
import { singlestoreSchema } from '../../serializer/singlestoreSchema';
|
||||
import { type DB, findAddedAndRemoved } from '../../utils';
|
||||
import { Select } from '../selector-ui';
|
||||
import { withStyle } from '../validations/outputs';
|
||||
|
||||
export const filterStatements = (
|
||||
statements: JsonStatement[],
|
||||
currentSchema: TypeOf<typeof singlestoreSchema>,
|
||||
prevSchema: TypeOf<typeof singlestoreSchema>,
|
||||
) => {
|
||||
return statements.filter((statement) => {
|
||||
if (statement.type === 'alter_table_alter_column_set_type') {
|
||||
// Don't need to handle it on migrations step and introspection
|
||||
// but for both it should be skipped
|
||||
if (
|
||||
statement.oldDataType.startsWith('tinyint')
|
||||
&& statement.newDataType.startsWith('boolean')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
statement.oldDataType.startsWith('bigint unsigned')
|
||||
&& statement.newDataType.startsWith('serial')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
statement.oldDataType.startsWith('serial')
|
||||
&& statement.newDataType.startsWith('bigint unsigned')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} else if (statement.type === 'alter_table_alter_column_set_default') {
|
||||
if (
|
||||
statement.newDefaultValue === false
|
||||
&& statement.oldDefaultValue === 0
|
||||
&& statement.newDataType === 'boolean'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
statement.newDefaultValue === true
|
||||
&& statement.oldDefaultValue === 1
|
||||
&& statement.newDataType === 'boolean'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} else if (statement.type === 'delete_unique_constraint') {
|
||||
const unsquashed = SingleStoreSquasher.unsquashUnique(statement.data);
|
||||
// only if constraint was removed from a serial column, than treat it as removed
|
||||
// const serialStatement = statements.find(
|
||||
// (it) => it.type === "alter_table_alter_column_set_type"
|
||||
// ) as JsonAlterColumnTypeStatement;
|
||||
// if (
|
||||
// serialStatement?.oldDataType.startsWith("bigint unsigned") &&
|
||||
// serialStatement?.newDataType.startsWith("serial") &&
|
||||
// serialStatement.columnName ===
|
||||
// SingleStoreSquasher.unsquashUnique(statement.data).columns[0]
|
||||
// ) {
|
||||
// return false;
|
||||
// }
|
||||
// Check if uniqueindex was only on this column, that is serial
|
||||
|
||||
// if now serial and was not serial and was unique index
|
||||
if (
|
||||
unsquashed.columns.length === 1
|
||||
&& currentSchema.tables[statement.tableName].columns[unsquashed.columns[0]]
|
||||
.type === 'serial'
|
||||
&& prevSchema.tables[statement.tableName].columns[unsquashed.columns[0]]
|
||||
.type === 'serial'
|
||||
&& currentSchema.tables[statement.tableName].columns[unsquashed.columns[0]]
|
||||
.name === unsquashed.columns[0]
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} else if (statement.type === 'alter_table_alter_column_drop_notnull') {
|
||||
// only if constraint was removed from a serial column, than treat it as removed
|
||||
const serialStatement = statements.find(
|
||||
(it) => it.type === 'alter_table_alter_column_set_type',
|
||||
) as JsonAlterColumnTypeStatement;
|
||||
if (
|
||||
serialStatement?.oldDataType.startsWith('bigint unsigned')
|
||||
&& serialStatement?.newDataType.startsWith('serial')
|
||||
&& serialStatement.columnName === statement.columnName
|
||||
&& serialStatement.tableName === statement.tableName
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (statement.newDataType === 'serial' && !statement.columnNotNull) {
|
||||
return false;
|
||||
}
|
||||
if (statement.columnAutoIncrement) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
export function findColumnTypeAlternations(
|
||||
columns1: Record<string, Column>,
|
||||
columns2: Record<string, Column>,
|
||||
): string[] {
|
||||
const changes: string[] = [];
|
||||
|
||||
for (const key in columns1) {
|
||||
if (columns1.hasOwnProperty(key) && columns2.hasOwnProperty(key)) {
|
||||
const col1 = columns1[key];
|
||||
const col2 = columns2[key];
|
||||
if (col1.type !== col2.type) {
|
||||
changes.push(col2.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
export const logSuggestionsAndReturn = async (
|
||||
db: DB,
|
||||
statements: JsonStatement[],
|
||||
json2: TypeOf<typeof singlestoreSchema>,
|
||||
json1: TypeOf<typeof singlestoreSchema>,
|
||||
) => {
|
||||
let shouldAskForApprove = false;
|
||||
const statementsToExecute: string[] = [];
|
||||
const infoToPrint: string[] = [];
|
||||
|
||||
const tablesToRemove: string[] = [];
|
||||
const columnsToRemove: string[] = [];
|
||||
const schemasToRemove: string[] = [];
|
||||
const tablesToTruncate: string[] = [];
|
||||
|
||||
for (const statement of statements) {
|
||||
if (statement.type === 'drop_table') {
|
||||
const res = await db.query(
|
||||
`select count(*) as count from \`${statement.tableName}\``,
|
||||
);
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about to delete ${
|
||||
chalk.underline(
|
||||
statement.tableName,
|
||||
)
|
||||
} table with ${count} items`,
|
||||
);
|
||||
tablesToRemove.push(statement.tableName);
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
} else if (statement.type === 'alter_table_drop_column') {
|
||||
const res = await db.query(
|
||||
`select count(*) as count from \`${statement.tableName}\``,
|
||||
);
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about to delete ${
|
||||
chalk.underline(
|
||||
statement.columnName,
|
||||
)
|
||||
} column in ${statement.tableName} table with ${count} items`,
|
||||
);
|
||||
columnsToRemove.push(`${statement.tableName}_${statement.columnName}`);
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
} else if (statement.type === 'drop_schema') {
|
||||
const res = await db.query(
|
||||
`select count(*) as count from information_schema.tables where table_schema = \`${statement.name}\`;`,
|
||||
);
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about to delete ${
|
||||
chalk.underline(
|
||||
statement.name,
|
||||
)
|
||||
} schema with ${count} tables`,
|
||||
);
|
||||
schemasToRemove.push(statement.name);
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
} else if (statement.type === 'alter_table_alter_column_set_type') {
|
||||
const res = await db.query(
|
||||
`select count(*) as count from \`${statement.tableName}\``,
|
||||
);
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about to change ${
|
||||
chalk.underline(
|
||||
statement.columnName,
|
||||
)
|
||||
} column type from ${
|
||||
chalk.underline(
|
||||
statement.oldDataType,
|
||||
)
|
||||
} to ${chalk.underline(statement.newDataType)} with ${count} items`,
|
||||
);
|
||||
statementsToExecute.push(`truncate table ${statement.tableName};`);
|
||||
tablesToTruncate.push(statement.tableName);
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
} else if (statement.type === 'alter_table_alter_column_drop_default') {
|
||||
if (statement.columnNotNull) {
|
||||
const res = await db.query(
|
||||
`select count(*) as count from \`${statement.tableName}\``,
|
||||
);
|
||||
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about to remove default value from ${
|
||||
chalk.underline(
|
||||
statement.columnName,
|
||||
)
|
||||
} not-null column with ${count} items`,
|
||||
);
|
||||
|
||||
tablesToTruncate.push(statement.tableName);
|
||||
statementsToExecute.push(`truncate table ${statement.tableName};`);
|
||||
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
}
|
||||
// shouldAskForApprove = true;
|
||||
} else if (statement.type === 'alter_table_alter_column_set_notnull') {
|
||||
if (typeof statement.columnDefault === 'undefined') {
|
||||
const res = await db.query(
|
||||
`select count(*) as count from \`${statement.tableName}\``,
|
||||
);
|
||||
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about to set not-null constraint to ${
|
||||
chalk.underline(
|
||||
statement.columnName,
|
||||
)
|
||||
} column without default, which contains ${count} items`,
|
||||
);
|
||||
|
||||
tablesToTruncate.push(statement.tableName);
|
||||
statementsToExecute.push(`truncate table ${statement.tableName};`);
|
||||
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
}
|
||||
} else if (statement.type === 'alter_table_alter_column_drop_pk') {
|
||||
const res = await db.query(
|
||||
`select count(*) as count from \`${statement.tableName}\``,
|
||||
);
|
||||
|
||||
// if drop pk and json2 has autoincrement in table -> exit process with error
|
||||
if (
|
||||
Object.values(json2.tables[statement.tableName].columns).filter(
|
||||
(column) => column.autoincrement,
|
||||
).length > 0
|
||||
) {
|
||||
console.log(
|
||||
`${
|
||||
withStyle.errorWarning(
|
||||
`You have removed the primary key from a ${statement.tableName} table without removing the auto-increment property from this table. As the database error states: 'there can be only one auto column, and it must be defined as a key. Make sure to remove autoincrement from ${statement.tableName} table`,
|
||||
)
|
||||
}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about to change ${
|
||||
chalk.underline(
|
||||
statement.tableName,
|
||||
)
|
||||
} primary key. This statements may fail and you table may left without primary key`,
|
||||
);
|
||||
|
||||
tablesToTruncate.push(statement.tableName);
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
} else if (statement.type === 'delete_composite_pk') {
|
||||
// if drop pk and json2 has autoincrement in table -> exit process with error
|
||||
if (
|
||||
Object.values(json2.tables[statement.tableName].columns).filter(
|
||||
(column) => column.autoincrement,
|
||||
).length > 0
|
||||
) {
|
||||
console.log(
|
||||
`${
|
||||
withStyle.errorWarning(
|
||||
`You have removed the primary key from a ${statement.tableName} table without removing the auto-increment property from this table. As the database error states: 'there can be only one auto column, and it must be defined as a key. Make sure to remove autoincrement from ${statement.tableName} table`,
|
||||
)
|
||||
}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (statement.type === 'alter_table_add_column') {
|
||||
if (
|
||||
statement.column.notNull
|
||||
&& typeof statement.column.default === 'undefined'
|
||||
) {
|
||||
const res = await db.query(
|
||||
`select count(*) as count from \`${statement.tableName}\``,
|
||||
);
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about to add not-null ${
|
||||
chalk.underline(
|
||||
statement.column.name,
|
||||
)
|
||||
} column without default value, which contains ${count} items`,
|
||||
);
|
||||
|
||||
tablesToTruncate.push(statement.tableName);
|
||||
statementsToExecute.push(`truncate table ${statement.tableName};`);
|
||||
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
}
|
||||
} else if (statement.type === 'create_unique_constraint') {
|
||||
const res = await db.query(
|
||||
`select count(*) as count from \`${statement.tableName}\``,
|
||||
);
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
const unsquashedUnique = SingleStoreSquasher.unsquashUnique(statement.data);
|
||||
console.log(
|
||||
`· You're about to add ${
|
||||
chalk.underline(
|
||||
unsquashedUnique.name,
|
||||
)
|
||||
} unique constraint to the table, which contains ${count} items. If this statement fails, you will receive an error from the database. Do you want to truncate ${
|
||||
chalk.underline(
|
||||
statement.tableName,
|
||||
)
|
||||
} table?\n`,
|
||||
);
|
||||
const { status, data } = await render(
|
||||
new Select([
|
||||
'No, add the constraint without truncating the table',
|
||||
`Yes, truncate the table`,
|
||||
]),
|
||||
);
|
||||
if (data?.index === 1) {
|
||||
tablesToTruncate.push(statement.tableName);
|
||||
statementsToExecute.push(`truncate table ${statement.tableName};`);
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
}
|
||||
} else if (statement.type === 'singlestore_recreate_table') {
|
||||
const tableName = statement.tableName;
|
||||
|
||||
const prevColumns = json1.tables[tableName].columns;
|
||||
const currentColumns = json2.tables[tableName].columns;
|
||||
const { removedColumns, addedColumns } = findAddedAndRemoved(
|
||||
Object.keys(prevColumns),
|
||||
Object.keys(currentColumns),
|
||||
);
|
||||
|
||||
if (removedColumns.length) {
|
||||
for (const removedColumn of removedColumns) {
|
||||
const res = await db.query<{ count: string }>(
|
||||
`select count(\`${tableName}\`.\`${removedColumn}\`) as count from \`${tableName}\``,
|
||||
);
|
||||
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about to delete ${
|
||||
chalk.underline(
|
||||
removedColumn,
|
||||
)
|
||||
} column in ${tableName} table with ${count} items`,
|
||||
);
|
||||
columnsToRemove.push(removedColumn);
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (addedColumns.length) {
|
||||
for (const addedColumn of addedColumns) {
|
||||
const [res] = await db.query<{ count: string }>(
|
||||
`select count(*) as count from \`${tableName}\``,
|
||||
);
|
||||
|
||||
const columnConf = json2.tables[tableName].columns[addedColumn];
|
||||
|
||||
const count = Number(res.count);
|
||||
if (count > 0 && columnConf.notNull && !columnConf.default) {
|
||||
infoToPrint.push(
|
||||
`· You're about to add not-null ${
|
||||
chalk.underline(
|
||||
addedColumn,
|
||||
)
|
||||
} column without default value to table, which contains ${count} items`,
|
||||
);
|
||||
shouldAskForApprove = true;
|
||||
tablesToTruncate.push(tableName);
|
||||
|
||||
statementsToExecute.push(`TRUNCATE TABLE \`${tableName}\`;`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const columnWithChangedType = findColumnTypeAlternations(prevColumns, currentColumns);
|
||||
for (const column of columnWithChangedType) {
|
||||
const [res] = await db.query<{ count: string }>(
|
||||
`select count(*) as count from \`${tableName}\` WHERE \`${tableName}\`.\`${column}\` IS NOT NULL;`,
|
||||
);
|
||||
|
||||
const count = Number(res.count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about recreate ${chalk.underline(tableName)} table with data type changing for ${
|
||||
chalk.underline(
|
||||
column,
|
||||
)
|
||||
} column, which contains ${count} items`,
|
||||
);
|
||||
shouldAskForApprove = true;
|
||||
tablesToTruncate.push(tableName);
|
||||
|
||||
statementsToExecute.push(`TRUNCATE TABLE \`${tableName}\`;`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const stmnt = fromJson([statement], 'singlestore', 'push');
|
||||
if (typeof stmnt !== 'undefined') {
|
||||
statementsToExecute.push(...stmnt);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
statementsToExecute,
|
||||
shouldAskForApprove,
|
||||
infoToPrint,
|
||||
columnsToRemove: [...new Set(columnsToRemove)],
|
||||
schemasToRemove: [...new Set(schemasToRemove)],
|
||||
tablesToTruncate: [...new Set(tablesToTruncate)],
|
||||
tablesToRemove: [...new Set(tablesToRemove)],
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export const upSinglestoreHandler = (out: string) => {};
|
||||
@@ -0,0 +1,96 @@
|
||||
import { renderWithTask } from 'hanji';
|
||||
import { Minimatch } from 'minimatch';
|
||||
import { originUUID } from '../../global';
|
||||
import { schemaToTypeScript } from '../../introspect-sqlite';
|
||||
import type { SQLiteSchema } from '../../serializer/sqliteSchema';
|
||||
import { fromDatabase } from '../../serializer/sqliteSerializer';
|
||||
import type { SQLiteDB } from '../../utils';
|
||||
import type { Casing } from '../validations/common';
|
||||
import type { SqliteCredentials } from '../validations/sqlite';
|
||||
import { IntrospectProgress, ProgressView } from '../views';
|
||||
|
||||
export const sqliteIntrospect = async (
|
||||
credentials: SqliteCredentials,
|
||||
filters: string[],
|
||||
casing: Casing,
|
||||
) => {
|
||||
const { connectToSQLite } = await import('../connections');
|
||||
const db = await connectToSQLite(credentials);
|
||||
|
||||
const matchers = filters.map((it) => {
|
||||
return new Minimatch(it);
|
||||
});
|
||||
|
||||
const filter = (tableName: string) => {
|
||||
if (matchers.length === 0) return true;
|
||||
|
||||
let flags: boolean[] = [];
|
||||
|
||||
for (let matcher of matchers) {
|
||||
if (matcher.negate) {
|
||||
if (!matcher.match(tableName)) {
|
||||
flags.push(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (matcher.match(tableName)) {
|
||||
flags.push(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (flags.length > 0) {
|
||||
return flags.every(Boolean);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const progress = new IntrospectProgress();
|
||||
const res = await renderWithTask(
|
||||
progress,
|
||||
fromDatabase(db, filter, (stage, count, status) => {
|
||||
progress.update(stage, count, status);
|
||||
}),
|
||||
);
|
||||
|
||||
const schema = { id: originUUID, prevId: '', ...res } as SQLiteSchema;
|
||||
const ts = schemaToTypeScript(schema, casing);
|
||||
return { schema, ts };
|
||||
};
|
||||
|
||||
export const sqlitePushIntrospect = async (db: SQLiteDB, filters: string[]) => {
|
||||
const matchers = filters.map((it) => {
|
||||
return new Minimatch(it);
|
||||
});
|
||||
|
||||
const filter = (tableName: string) => {
|
||||
if (matchers.length === 0) return true;
|
||||
|
||||
let flags: boolean[] = [];
|
||||
|
||||
for (let matcher of matchers) {
|
||||
if (matcher.negate) {
|
||||
if (!matcher.match(tableName)) {
|
||||
flags.push(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (matcher.match(tableName)) {
|
||||
flags.push(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (flags.length > 0) {
|
||||
return flags.every(Boolean);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const progress = new ProgressView(
|
||||
'Pulling schema from database...',
|
||||
'Pulling schema from database...',
|
||||
);
|
||||
const res = await renderWithTask(progress, fromDatabase(db, filter));
|
||||
|
||||
const schema = { id: originUUID, prevId: '', ...res } as SQLiteSchema;
|
||||
return { schema };
|
||||
};
|
||||
@@ -0,0 +1,322 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { SQLiteSchemaInternal, SQLiteSchemaSquashed, SQLiteSquasher } from '../../serializer/sqliteSchema';
|
||||
import {
|
||||
CreateSqliteIndexConvertor,
|
||||
fromJson,
|
||||
SQLiteCreateTableConvertor,
|
||||
SQLiteDropTableConvertor,
|
||||
SqliteRenameTableConvertor,
|
||||
} from '../../sqlgenerator';
|
||||
|
||||
import type { JsonStatement } from '../../jsonStatements';
|
||||
import { findAddedAndRemoved, type SQLiteDB } from '../../utils';
|
||||
|
||||
export const _moveDataStatements = (
|
||||
tableName: string,
|
||||
json: SQLiteSchemaSquashed,
|
||||
dataLoss: boolean = false,
|
||||
) => {
|
||||
const statements: string[] = [];
|
||||
|
||||
const newTableName = `__new_${tableName}`;
|
||||
|
||||
// create table statement from a new json2 with proper name
|
||||
const tableColumns = Object.values(json.tables[tableName].columns);
|
||||
const referenceData = Object.values(json.tables[tableName].foreignKeys);
|
||||
const compositePKs = Object.values(
|
||||
json.tables[tableName].compositePrimaryKeys,
|
||||
).map((it) => SQLiteSquasher.unsquashPK(it));
|
||||
const checkConstraints = Object.values(json.tables[tableName].checkConstraints);
|
||||
|
||||
const mappedCheckConstraints: string[] = checkConstraints.map((it) =>
|
||||
it.replaceAll(`"${tableName}".`, `"${newTableName}".`)
|
||||
.replaceAll(`\`${tableName}\`.`, `\`${newTableName}\`.`)
|
||||
.replaceAll(`${tableName}.`, `${newTableName}.`)
|
||||
.replaceAll(`'${tableName}'.`, `\`${newTableName}\`.`)
|
||||
);
|
||||
|
||||
const fks = referenceData.map((it) => SQLiteSquasher.unsquashPushFK(it));
|
||||
|
||||
// create new table
|
||||
statements.push(
|
||||
new SQLiteCreateTableConvertor().convert({
|
||||
type: 'sqlite_create_table',
|
||||
tableName: newTableName,
|
||||
columns: tableColumns,
|
||||
referenceData: fks,
|
||||
compositePKs,
|
||||
checkConstraints: mappedCheckConstraints,
|
||||
}),
|
||||
);
|
||||
|
||||
// move data
|
||||
if (!dataLoss) {
|
||||
const columns = Object.keys(json.tables[tableName].columns).map(
|
||||
(c) => `"${c}"`,
|
||||
);
|
||||
|
||||
statements.push(
|
||||
`INSERT INTO \`${newTableName}\`(${
|
||||
columns.join(
|
||||
', ',
|
||||
)
|
||||
}) SELECT ${columns.join(', ')} FROM \`${tableName}\`;`,
|
||||
);
|
||||
}
|
||||
|
||||
statements.push(
|
||||
new SQLiteDropTableConvertor().convert({
|
||||
type: 'drop_table',
|
||||
tableName: tableName,
|
||||
schema: '',
|
||||
}),
|
||||
);
|
||||
|
||||
// rename table
|
||||
statements.push(
|
||||
new SqliteRenameTableConvertor().convert({
|
||||
fromSchema: '',
|
||||
tableNameFrom: newTableName,
|
||||
tableNameTo: tableName,
|
||||
toSchema: '',
|
||||
type: 'rename_table',
|
||||
}),
|
||||
);
|
||||
|
||||
for (const idx of Object.values(json.tables[tableName].indexes)) {
|
||||
statements.push(
|
||||
new CreateSqliteIndexConvertor().convert({
|
||||
type: 'create_index',
|
||||
tableName: tableName,
|
||||
schema: '',
|
||||
data: idx,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return statements;
|
||||
};
|
||||
|
||||
export const getOldTableName = (
|
||||
tableName: string,
|
||||
meta: SQLiteSchemaInternal['_meta'],
|
||||
) => {
|
||||
for (const key of Object.keys(meta.tables)) {
|
||||
const value = meta.tables[key];
|
||||
if (`"${tableName}"` === value) {
|
||||
return key.substring(1, key.length - 1);
|
||||
}
|
||||
}
|
||||
return tableName;
|
||||
};
|
||||
|
||||
export const getNewTableName = (
|
||||
tableName: string,
|
||||
meta: SQLiteSchemaInternal['_meta'],
|
||||
) => {
|
||||
if (typeof meta.tables[`"${tableName}"`] !== 'undefined') {
|
||||
return meta.tables[`"${tableName}"`].substring(
|
||||
1,
|
||||
meta.tables[`"${tableName}"`].length - 1,
|
||||
);
|
||||
}
|
||||
return tableName;
|
||||
};
|
||||
|
||||
export const logSuggestionsAndReturn = async (
|
||||
connection: SQLiteDB,
|
||||
statements: JsonStatement[],
|
||||
json1: SQLiteSchemaSquashed,
|
||||
json2: SQLiteSchemaSquashed,
|
||||
meta: SQLiteSchemaInternal['_meta'],
|
||||
) => {
|
||||
let shouldAskForApprove = false;
|
||||
const statementsToExecute: string[] = [];
|
||||
const infoToPrint: string[] = [];
|
||||
|
||||
const tablesToRemove: string[] = [];
|
||||
const columnsToRemove: string[] = [];
|
||||
const schemasToRemove: string[] = [];
|
||||
const tablesToTruncate: string[] = [];
|
||||
|
||||
for (const statement of statements) {
|
||||
if (statement.type === 'drop_table') {
|
||||
const res = await connection.query<{ count: string }>(
|
||||
`select count(*) as count from \`${statement.tableName}\``,
|
||||
);
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about to delete ${
|
||||
chalk.underline(
|
||||
statement.tableName,
|
||||
)
|
||||
} table with ${count} items`,
|
||||
);
|
||||
tablesToRemove.push(statement.tableName);
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
|
||||
const fromJsonStatement = fromJson([statement], 'sqlite', 'push');
|
||||
statementsToExecute.push(
|
||||
...(Array.isArray(fromJsonStatement) ? fromJsonStatement : [fromJsonStatement]),
|
||||
);
|
||||
} else if (statement.type === 'alter_table_drop_column') {
|
||||
const tableName = statement.tableName;
|
||||
const columnName = statement.columnName;
|
||||
|
||||
const res = await connection.query<{ count: string }>(
|
||||
`select count(\`${tableName}\`.\`${columnName}\`) as count from \`${tableName}\``,
|
||||
);
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about to delete ${
|
||||
chalk.underline(
|
||||
columnName,
|
||||
)
|
||||
} column in ${tableName} table with ${count} items`,
|
||||
);
|
||||
columnsToRemove.push(`${tableName}_${statement.columnName}`);
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
|
||||
const fromJsonStatement = fromJson([statement], 'sqlite', 'push');
|
||||
statementsToExecute.push(
|
||||
...(Array.isArray(fromJsonStatement) ? fromJsonStatement : [fromJsonStatement]),
|
||||
);
|
||||
} else if (
|
||||
statement.type === 'sqlite_alter_table_add_column'
|
||||
&& (statement.column.notNull && !statement.column.default)
|
||||
) {
|
||||
const tableName = statement.tableName;
|
||||
const columnName = statement.column.name;
|
||||
const res = await connection.query<{ count: string }>(
|
||||
`select count(*) as count from \`${tableName}\``,
|
||||
);
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about to add not-null ${
|
||||
chalk.underline(
|
||||
columnName,
|
||||
)
|
||||
} column without default value, which contains ${count} items`,
|
||||
);
|
||||
|
||||
tablesToTruncate.push(tableName);
|
||||
statementsToExecute.push(`delete from ${tableName};`);
|
||||
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
|
||||
const fromJsonStatement = fromJson([statement], 'sqlite', 'push');
|
||||
statementsToExecute.push(
|
||||
...(Array.isArray(fromJsonStatement) ? fromJsonStatement : [fromJsonStatement]),
|
||||
);
|
||||
} else if (statement.type === 'recreate_table') {
|
||||
const tableName = statement.tableName;
|
||||
const oldTableName = getOldTableName(tableName, meta);
|
||||
|
||||
let dataLoss = false;
|
||||
|
||||
const prevColumnNames = Object.keys(json1.tables[oldTableName].columns);
|
||||
const currentColumnNames = Object.keys(json2.tables[tableName].columns);
|
||||
const { removedColumns, addedColumns } = findAddedAndRemoved(
|
||||
prevColumnNames,
|
||||
currentColumnNames,
|
||||
);
|
||||
|
||||
if (removedColumns.length) {
|
||||
for (const removedColumn of removedColumns) {
|
||||
const res = await connection.query<{ count: string }>(
|
||||
`select count(\`${tableName}\`.\`${removedColumn}\`) as count from \`${tableName}\``,
|
||||
);
|
||||
|
||||
const count = Number(res[0].count);
|
||||
if (count > 0) {
|
||||
infoToPrint.push(
|
||||
`· You're about to delete ${
|
||||
chalk.underline(
|
||||
removedColumn,
|
||||
)
|
||||
} column in ${tableName} table with ${count} items`,
|
||||
);
|
||||
columnsToRemove.push(removedColumn);
|
||||
shouldAskForApprove = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (addedColumns.length) {
|
||||
for (const addedColumn of addedColumns) {
|
||||
const [res] = await connection.query<{ count: string }>(
|
||||
`select count(*) as count from \`${tableName}\``,
|
||||
);
|
||||
|
||||
const columnConf = json2.tables[tableName].columns[addedColumn];
|
||||
|
||||
const count = Number(res.count);
|
||||
if (count > 0 && columnConf.notNull && !columnConf.default) {
|
||||
dataLoss = true;
|
||||
infoToPrint.push(
|
||||
`· You're about to add not-null ${
|
||||
chalk.underline(
|
||||
addedColumn,
|
||||
)
|
||||
} column without default value to table, which contains ${count} items`,
|
||||
);
|
||||
shouldAskForApprove = true;
|
||||
tablesToTruncate.push(tableName);
|
||||
|
||||
statementsToExecute.push(`DELETE FROM \`${tableName}\`;`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check if some tables referencing current for pragma
|
||||
const tablesReferencingCurrent: string[] = [];
|
||||
|
||||
for (const table of Object.values(json2.tables)) {
|
||||
const tablesRefs = Object.values(json2.tables[table.name].foreignKeys)
|
||||
.filter((t) => SQLiteSquasher.unsquashPushFK(t).tableTo === tableName)
|
||||
.map((it) => SQLiteSquasher.unsquashPushFK(it).tableFrom);
|
||||
|
||||
tablesReferencingCurrent.push(...tablesRefs);
|
||||
}
|
||||
|
||||
if (!tablesReferencingCurrent.length) {
|
||||
statementsToExecute.push(..._moveDataStatements(tableName, json2, dataLoss));
|
||||
continue;
|
||||
}
|
||||
|
||||
const [{ foreign_keys: pragmaState }] = await connection.query<{
|
||||
foreign_keys: number;
|
||||
}>(`PRAGMA foreign_keys;`);
|
||||
|
||||
if (pragmaState) {
|
||||
statementsToExecute.push(`PRAGMA foreign_keys=OFF;`);
|
||||
}
|
||||
statementsToExecute.push(..._moveDataStatements(tableName, json2, dataLoss));
|
||||
if (pragmaState) {
|
||||
statementsToExecute.push(`PRAGMA foreign_keys=ON;`);
|
||||
}
|
||||
} else {
|
||||
const fromJsonStatement = fromJson([statement], 'sqlite', 'push');
|
||||
statementsToExecute.push(
|
||||
...(Array.isArray(fromJsonStatement) ? fromJsonStatement : [fromJsonStatement]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
statementsToExecute,
|
||||
shouldAskForApprove,
|
||||
infoToPrint,
|
||||
columnsToRemove: [...new Set(columnsToRemove)],
|
||||
schemasToRemove: [...new Set(schemasToRemove)],
|
||||
tablesToTruncate: [...new Set(tablesToTruncate)],
|
||||
tablesToRemove: [...new Set(tablesToRemove)],
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import chalk from 'chalk';
|
||||
import { writeFileSync } from 'fs';
|
||||
import { mapEntries } from 'src/global';
|
||||
import { SQLiteSchema, sqliteSchemaV5 } from 'src/serializer/sqliteSchema';
|
||||
import { prepareOutFolder, validateWithReport } from 'src/utils';
|
||||
|
||||
export const upSqliteHandler = (out: string) => {
|
||||
const { snapshots } = prepareOutFolder(out, 'sqlite');
|
||||
const report = validateWithReport(snapshots, 'sqlite');
|
||||
|
||||
report.nonLatest
|
||||
.map((it) => ({
|
||||
path: it,
|
||||
raw: report.rawMap[it]!! as Record<string, any>,
|
||||
}))
|
||||
.forEach((it) => {
|
||||
const path = it.path;
|
||||
const result = updateUpToV6(it.raw);
|
||||
|
||||
console.log(`[${chalk.green('✓')}] ${path}`);
|
||||
|
||||
writeFileSync(path, JSON.stringify(result, null, 2));
|
||||
});
|
||||
|
||||
console.log("Everything's fine 🐶🔥");
|
||||
};
|
||||
|
||||
const updateUpToV6 = (json: Record<string, any>): SQLiteSchema => {
|
||||
const schema = sqliteSchemaV5.parse(json);
|
||||
|
||||
const tables = mapEntries(schema.tables, (tableKey, table) => {
|
||||
const columns = mapEntries(table.columns, (key, value) => {
|
||||
if (
|
||||
value.default
|
||||
&& (typeof value.default === 'object' || Array.isArray(value.default))
|
||||
) {
|
||||
value.default = `'${JSON.stringify(value.default)}'`;
|
||||
}
|
||||
return [key, value];
|
||||
});
|
||||
table.columns = columns;
|
||||
return [tableKey, table];
|
||||
});
|
||||
|
||||
return {
|
||||
...schema,
|
||||
version: '6',
|
||||
dialect: 'sqlite',
|
||||
tables: tables,
|
||||
views: {},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,938 @@
|
||||
import chalk from 'chalk';
|
||||
import { existsSync } from 'fs';
|
||||
import { render } from 'hanji';
|
||||
import { join, resolve } from 'path';
|
||||
import { object, string } from 'zod';
|
||||
import { getTablesFilterByExtensions } from '../../extensions/getTablesFilterByExtensions';
|
||||
import { assertUnreachable } from '../../global';
|
||||
import { type Dialect, dialect } from '../../schemaValidator';
|
||||
import { prepareFilenames } from '../../serializer';
|
||||
import type { Entities } from '../validations/cli';
|
||||
import { pullParams, pushParams } from '../validations/cli';
|
||||
import type { Casing, CasingType, CliConfig, Driver, Prefix } from '../validations/common';
|
||||
import { configCommonSchema, configMigrations, wrapParam } from '../validations/common';
|
||||
import type { GelCredentials } from '../validations/gel';
|
||||
import { gelCredentials, printConfigConnectionIssues as printIssuesGel } from '../validations/gel';
|
||||
import type { LibSQLCredentials } from '../validations/libsql';
|
||||
import { libSQLCredentials, printConfigConnectionIssues as printIssuesLibSQL } from '../validations/libsql';
|
||||
import type { MysqlCredentials } from '../validations/mysql';
|
||||
import { mysqlCredentials, printConfigConnectionIssues as printIssuesMysql } from '../validations/mysql';
|
||||
import { outputs } from '../validations/outputs';
|
||||
import type { PostgresCredentials } from '../validations/postgres';
|
||||
import { postgresCredentials, printConfigConnectionIssues as printIssuesPg } from '../validations/postgres';
|
||||
import type { SingleStoreCredentials } from '../validations/singlestore';
|
||||
import {
|
||||
printConfigConnectionIssues as printIssuesSingleStore,
|
||||
singlestoreCredentials,
|
||||
} from '../validations/singlestore';
|
||||
import type { SqliteCredentials } from '../validations/sqlite';
|
||||
import { printConfigConnectionIssues as printIssuesSqlite, sqliteCredentials } from '../validations/sqlite';
|
||||
import { studioCliParams, studioConfig } from '../validations/studio';
|
||||
import { error } from '../views';
|
||||
|
||||
// NextJs default config is target: es5, which esbuild-register can't consume
|
||||
const assertES5 = async () => {
|
||||
try {
|
||||
await import('./_es5');
|
||||
} catch (e: any) {
|
||||
if ('errors' in e && Array.isArray(e.errors) && e.errors.length > 0) {
|
||||
const es5Error = (e.errors as any[]).filter((it) => it.text?.includes(`("es5") is not supported yet`)).length > 0;
|
||||
if (es5Error) {
|
||||
console.log(
|
||||
error(
|
||||
`Please change compilerOptions.target from 'es5' to 'es6' or above in your tsconfig.json`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
export class InMemoryMutex {
|
||||
private lockPromise: Promise<void> | null = null;
|
||||
|
||||
async withLock<T>(fn: () => Promise<T>): Promise<T> {
|
||||
// Wait for any existing lock
|
||||
while (this.lockPromise) {
|
||||
await this.lockPromise;
|
||||
}
|
||||
|
||||
let resolveLock: (() => void) | undefined;
|
||||
this.lockPromise = new Promise<void>((resolve) => {
|
||||
resolveLock = resolve;
|
||||
});
|
||||
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
this.lockPromise = null;
|
||||
resolveLock!(); // non-null assertion: TS now knows it's definitely assigned
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const registerMutex = new InMemoryMutex();
|
||||
|
||||
let tsxRegistered = false;
|
||||
const ensureTsxRegistered = () => {
|
||||
if (tsxRegistered) return;
|
||||
|
||||
const isBun = typeof (globalThis as any).Bun !== 'undefined';
|
||||
const isDeno = typeof (globalThis as any).Deno !== 'undefined';
|
||||
if (isBun || isDeno) {
|
||||
tsxRegistered = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const tsx = require('tsx/cjs/api');
|
||||
tsx.register();
|
||||
tsxRegistered = true;
|
||||
};
|
||||
|
||||
export const safeRegister = async <T>(fn: () => Promise<T>) => {
|
||||
return registerMutex.withLock(async () => {
|
||||
ensureTsxRegistered();
|
||||
await assertES5();
|
||||
return fn();
|
||||
});
|
||||
};
|
||||
|
||||
export const prepareCheckParams = async (
|
||||
options: {
|
||||
config?: string;
|
||||
dialect?: Dialect;
|
||||
out?: string;
|
||||
},
|
||||
from: 'cli' | 'config',
|
||||
): Promise<{ out: string; dialect: Dialect }> => {
|
||||
const config = from === 'config'
|
||||
? await drizzleConfigFromFile(options.config as string | undefined)
|
||||
: options;
|
||||
|
||||
if (!config.out || !config.dialect) {
|
||||
let text = `Please provide required params for AWS Data API driver:\n`;
|
||||
console.log(error(text));
|
||||
console.log(wrapParam('database', config.out));
|
||||
console.log(wrapParam('secretArn', config.dialect));
|
||||
process.exit(1);
|
||||
}
|
||||
return { out: config.out, dialect: config.dialect };
|
||||
};
|
||||
|
||||
export const prepareDropParams = async (
|
||||
options: {
|
||||
config?: string;
|
||||
out?: string;
|
||||
driver?: Driver;
|
||||
dialect?: Dialect;
|
||||
},
|
||||
from: 'cli' | 'config',
|
||||
): Promise<{ out: string; bundle: boolean }> => {
|
||||
const config = from === 'config'
|
||||
? await drizzleConfigFromFile(options.config as string | undefined)
|
||||
: options;
|
||||
|
||||
if (config.dialect === 'gel') {
|
||||
console.log(
|
||||
error(
|
||||
`You can't use 'drop' command with Gel dialect`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return { out: config.out || 'drizzle', bundle: config.driver === 'expo' };
|
||||
};
|
||||
|
||||
export type GenerateConfig = {
|
||||
dialect: Dialect;
|
||||
schema: string | string[];
|
||||
out: string;
|
||||
breakpoints: boolean;
|
||||
name?: string;
|
||||
prefix: Prefix;
|
||||
custom: boolean;
|
||||
bundle: boolean;
|
||||
casing?: CasingType;
|
||||
driver?: Driver;
|
||||
};
|
||||
|
||||
export type ExportConfig = {
|
||||
dialect: Dialect;
|
||||
schema: string | string[];
|
||||
sql: boolean;
|
||||
};
|
||||
|
||||
export const prepareGenerateConfig = async (
|
||||
options: {
|
||||
config?: string;
|
||||
schema?: string;
|
||||
out?: string;
|
||||
breakpoints?: boolean;
|
||||
custom?: boolean;
|
||||
name?: string;
|
||||
dialect?: Dialect;
|
||||
driver?: Driver;
|
||||
prefix?: Prefix;
|
||||
casing?: CasingType;
|
||||
},
|
||||
from: 'config' | 'cli',
|
||||
): Promise<GenerateConfig> => {
|
||||
const config = from === 'config' ? await drizzleConfigFromFile(options.config) : options;
|
||||
|
||||
const { schema, out, breakpoints, dialect, driver, casing } = config;
|
||||
|
||||
if (!schema || !dialect) {
|
||||
console.log(error('Please provide required params:'));
|
||||
console.log(wrapParam('schema', schema));
|
||||
console.log(wrapParam('dialect', dialect));
|
||||
console.log(wrapParam('out', out, true));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const fileNames = prepareFilenames(schema);
|
||||
if (fileNames.length === 0) {
|
||||
render(`[${chalk.blue('i')}] No schema file in ${schema} was found`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const prefix = ('migrations' in config ? config.migrations?.prefix : options.prefix)
|
||||
|| 'index';
|
||||
|
||||
return {
|
||||
dialect: dialect,
|
||||
name: options.name,
|
||||
custom: options.custom || false,
|
||||
prefix,
|
||||
breakpoints: breakpoints ?? true,
|
||||
schema: schema,
|
||||
out: out || 'drizzle',
|
||||
bundle: driver === 'expo' || driver === 'durable-sqlite',
|
||||
casing,
|
||||
driver,
|
||||
};
|
||||
};
|
||||
|
||||
export const prepareExportConfig = async (
|
||||
options: {
|
||||
config?: string;
|
||||
schema?: string;
|
||||
dialect?: Dialect;
|
||||
sql: boolean;
|
||||
},
|
||||
from: 'config' | 'cli',
|
||||
): Promise<ExportConfig> => {
|
||||
const config = from === 'config' ? await drizzleConfigFromFile(options.config, true) : options;
|
||||
|
||||
const { schema, dialect, sql } = config;
|
||||
|
||||
if (!schema || !dialect) {
|
||||
console.log(error('Please provide required params:'));
|
||||
console.log(wrapParam('schema', schema));
|
||||
console.log(wrapParam('dialect', dialect));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const fileNames = prepareFilenames(schema);
|
||||
if (fileNames.length === 0) {
|
||||
render(`[${chalk.blue('i')}] No schema file in ${schema} was found`);
|
||||
process.exit(0);
|
||||
}
|
||||
return {
|
||||
dialect: dialect,
|
||||
schema: schema,
|
||||
sql: sql,
|
||||
};
|
||||
};
|
||||
|
||||
export const flattenDatabaseCredentials = (config: any) => {
|
||||
if ('dbCredentials' in config) {
|
||||
const { dbCredentials, ...rest } = config;
|
||||
return {
|
||||
...rest,
|
||||
...dbCredentials,
|
||||
};
|
||||
}
|
||||
return config;
|
||||
};
|
||||
|
||||
const flattenPull = (config: any) => {
|
||||
if ('dbCredentials' in config) {
|
||||
const { dbCredentials, introspect, ...rest } = config;
|
||||
return {
|
||||
...rest,
|
||||
...dbCredentials,
|
||||
casing: introspect?.casing,
|
||||
};
|
||||
}
|
||||
return config;
|
||||
};
|
||||
|
||||
export const preparePushConfig = async (
|
||||
options: Record<string, unknown>,
|
||||
from: 'cli' | 'config',
|
||||
): Promise<
|
||||
(
|
||||
| {
|
||||
dialect: 'mysql';
|
||||
credentials: MysqlCredentials;
|
||||
}
|
||||
| {
|
||||
dialect: 'postgresql';
|
||||
credentials: PostgresCredentials;
|
||||
}
|
||||
| {
|
||||
dialect: 'sqlite';
|
||||
credentials: SqliteCredentials;
|
||||
}
|
||||
| {
|
||||
dialect: 'turso';
|
||||
credentials: LibSQLCredentials;
|
||||
}
|
||||
| {
|
||||
dialect: 'singlestore';
|
||||
credentials: SingleStoreCredentials;
|
||||
}
|
||||
) & {
|
||||
schemaPath: string | string[];
|
||||
verbose: boolean;
|
||||
strict: boolean;
|
||||
force: boolean;
|
||||
tablesFilter: string[];
|
||||
schemasFilter: string[];
|
||||
casing?: CasingType;
|
||||
entities?: Entities;
|
||||
}
|
||||
> => {
|
||||
const raw = flattenDatabaseCredentials(
|
||||
from === 'config'
|
||||
? await drizzleConfigFromFile(options.config as string | undefined)
|
||||
: options,
|
||||
);
|
||||
|
||||
raw.verbose ||= options.verbose; // if provided in cli to debug
|
||||
raw.strict ||= options.strict; // if provided in cli only
|
||||
|
||||
const parsed = pushParams.safeParse(raw);
|
||||
|
||||
if (parsed.error) {
|
||||
console.log(error('Please provide required params:'));
|
||||
console.log(wrapParam('dialect', raw.dialect));
|
||||
console.log(wrapParam('schema', raw.schema));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const config = parsed.data;
|
||||
|
||||
const schemaFiles = prepareFilenames(config.schema);
|
||||
if (schemaFiles.length === 0) {
|
||||
render(`[${chalk.blue('i')}] No schema file in ${config.schema} was found`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const tablesFilterConfig = config.tablesFilter;
|
||||
const tablesFilter = tablesFilterConfig
|
||||
? typeof tablesFilterConfig === 'string'
|
||||
? [tablesFilterConfig]
|
||||
: tablesFilterConfig
|
||||
: [];
|
||||
|
||||
const schemasFilterConfig = config.schemaFilter;
|
||||
|
||||
const schemasFilter = schemasFilterConfig
|
||||
? typeof schemasFilterConfig === 'string'
|
||||
? [schemasFilterConfig]
|
||||
: schemasFilterConfig
|
||||
: [];
|
||||
|
||||
tablesFilter.push(...getTablesFilterByExtensions(config));
|
||||
|
||||
if (config.dialect === 'postgresql') {
|
||||
const parsed = postgresCredentials.safeParse(config);
|
||||
if (!parsed.success) {
|
||||
printIssuesPg(config);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return {
|
||||
dialect: 'postgresql',
|
||||
schemaPath: config.schema,
|
||||
strict: config.strict ?? false,
|
||||
verbose: config.verbose ?? false,
|
||||
force: (options.force as boolean) ?? false,
|
||||
credentials: parsed.data,
|
||||
casing: config.casing,
|
||||
tablesFilter,
|
||||
schemasFilter,
|
||||
entities: config.entities,
|
||||
};
|
||||
}
|
||||
|
||||
if (config.dialect === 'mysql') {
|
||||
const parsed = mysqlCredentials.safeParse(config);
|
||||
if (!parsed.success) {
|
||||
printIssuesMysql(config);
|
||||
process.exit(1);
|
||||
}
|
||||
return {
|
||||
dialect: 'mysql',
|
||||
schemaPath: config.schema,
|
||||
strict: config.strict ?? false,
|
||||
verbose: config.verbose ?? false,
|
||||
force: (options.force as boolean) ?? false,
|
||||
credentials: parsed.data,
|
||||
casing: config.casing,
|
||||
tablesFilter,
|
||||
schemasFilter,
|
||||
};
|
||||
}
|
||||
|
||||
if (config.dialect === 'singlestore') {
|
||||
const parsed = singlestoreCredentials.safeParse(config);
|
||||
if (!parsed.success) {
|
||||
printIssuesSingleStore(config);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return {
|
||||
dialect: 'singlestore',
|
||||
schemaPath: config.schema,
|
||||
strict: config.strict ?? false,
|
||||
verbose: config.verbose ?? false,
|
||||
force: (options.force as boolean) ?? false,
|
||||
credentials: parsed.data,
|
||||
tablesFilter,
|
||||
schemasFilter,
|
||||
};
|
||||
}
|
||||
|
||||
if (config.dialect === 'sqlite') {
|
||||
const parsed = sqliteCredentials.safeParse(config);
|
||||
if (!parsed.success) {
|
||||
printIssuesSqlite(config, 'push');
|
||||
process.exit(1);
|
||||
}
|
||||
return {
|
||||
dialect: 'sqlite',
|
||||
schemaPath: config.schema,
|
||||
strict: config.strict ?? false,
|
||||
verbose: config.verbose ?? false,
|
||||
force: (options.force as boolean) ?? false,
|
||||
credentials: parsed.data,
|
||||
casing: config.casing,
|
||||
tablesFilter,
|
||||
schemasFilter,
|
||||
};
|
||||
}
|
||||
|
||||
if (config.dialect === 'turso') {
|
||||
const parsed = libSQLCredentials.safeParse(config);
|
||||
if (!parsed.success) {
|
||||
printIssuesSqlite(config, 'push');
|
||||
process.exit(1);
|
||||
}
|
||||
return {
|
||||
dialect: 'turso',
|
||||
schemaPath: config.schema,
|
||||
strict: config.strict ?? false,
|
||||
verbose: config.verbose ?? false,
|
||||
force: (options.force as boolean) ?? false,
|
||||
credentials: parsed.data,
|
||||
casing: config.casing,
|
||||
tablesFilter,
|
||||
schemasFilter,
|
||||
};
|
||||
}
|
||||
|
||||
if (config.dialect === 'gel') {
|
||||
console.log(
|
||||
error(
|
||||
`You can't use 'push' command with Gel dialect`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
assertUnreachable(config.dialect);
|
||||
};
|
||||
|
||||
export const preparePullConfig = async (
|
||||
options: Record<string, unknown>,
|
||||
from: 'cli' | 'config',
|
||||
): Promise<
|
||||
(
|
||||
| {
|
||||
dialect: 'mysql';
|
||||
credentials: MysqlCredentials;
|
||||
}
|
||||
| {
|
||||
dialect: 'postgresql';
|
||||
credentials: PostgresCredentials;
|
||||
}
|
||||
| {
|
||||
dialect: 'sqlite';
|
||||
credentials: SqliteCredentials;
|
||||
}
|
||||
| {
|
||||
dialect: 'turso';
|
||||
credentials: LibSQLCredentials;
|
||||
}
|
||||
| {
|
||||
dialect: 'singlestore';
|
||||
credentials: SingleStoreCredentials;
|
||||
}
|
||||
| {
|
||||
dialect: 'gel';
|
||||
credentials?: GelCredentials;
|
||||
}
|
||||
) & {
|
||||
out: string;
|
||||
breakpoints: boolean;
|
||||
casing: Casing;
|
||||
tablesFilter: string[];
|
||||
schemasFilter: string[];
|
||||
prefix: Prefix;
|
||||
entities: Entities;
|
||||
}
|
||||
> => {
|
||||
const raw = flattenPull(
|
||||
from === 'config'
|
||||
? await drizzleConfigFromFile(options.config as string | undefined)
|
||||
: options,
|
||||
);
|
||||
const parsed = pullParams.safeParse(raw);
|
||||
|
||||
if (parsed.error) {
|
||||
console.log(error('Please provide required params:'));
|
||||
console.log(wrapParam('dialect', raw.dialect));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const config = parsed.data;
|
||||
const dialect = config.dialect;
|
||||
|
||||
const tablesFilterConfig = config.tablesFilter;
|
||||
const tablesFilter = tablesFilterConfig
|
||||
? typeof tablesFilterConfig === 'string'
|
||||
? [tablesFilterConfig]
|
||||
: tablesFilterConfig
|
||||
: [];
|
||||
|
||||
if (config.extensionsFilters) {
|
||||
if (
|
||||
config.extensionsFilters.includes('postgis')
|
||||
&& dialect === 'postgresql'
|
||||
) {
|
||||
tablesFilter.push(
|
||||
...['!geography_columns', '!geometry_columns', '!spatial_ref_sys'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const schemasFilterConfig = config.schemaFilter; // TODO: consistent naming
|
||||
const schemasFilter = schemasFilterConfig
|
||||
? typeof schemasFilterConfig === 'string'
|
||||
? [schemasFilterConfig]
|
||||
: schemasFilterConfig
|
||||
: [];
|
||||
|
||||
if (dialect === 'postgresql') {
|
||||
const parsed = postgresCredentials.safeParse(config);
|
||||
if (!parsed.success) {
|
||||
printIssuesPg(config);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return {
|
||||
dialect: 'postgresql',
|
||||
out: config.out,
|
||||
breakpoints: config.breakpoints,
|
||||
casing: config.casing,
|
||||
credentials: parsed.data,
|
||||
tablesFilter,
|
||||
schemasFilter,
|
||||
prefix: config.migrations?.prefix || 'index',
|
||||
entities: config.entities,
|
||||
};
|
||||
}
|
||||
|
||||
if (dialect === 'mysql') {
|
||||
const parsed = mysqlCredentials.safeParse(config);
|
||||
if (!parsed.success) {
|
||||
printIssuesMysql(config);
|
||||
process.exit(1);
|
||||
}
|
||||
return {
|
||||
dialect: 'mysql',
|
||||
out: config.out,
|
||||
breakpoints: config.breakpoints,
|
||||
casing: config.casing,
|
||||
credentials: parsed.data,
|
||||
tablesFilter,
|
||||
schemasFilter,
|
||||
prefix: config.migrations?.prefix || 'index',
|
||||
entities: config.entities,
|
||||
};
|
||||
}
|
||||
|
||||
if (dialect === 'singlestore') {
|
||||
const parsed = singlestoreCredentials.safeParse(config);
|
||||
if (!parsed.success) {
|
||||
printIssuesSingleStore(config);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return {
|
||||
dialect: 'singlestore',
|
||||
out: config.out,
|
||||
breakpoints: config.breakpoints,
|
||||
casing: config.casing,
|
||||
credentials: parsed.data,
|
||||
tablesFilter,
|
||||
schemasFilter,
|
||||
prefix: config.migrations?.prefix || 'index',
|
||||
entities: config.entities,
|
||||
};
|
||||
}
|
||||
|
||||
if (dialect === 'sqlite') {
|
||||
const parsed = sqliteCredentials.safeParse(config);
|
||||
if (!parsed.success) {
|
||||
printIssuesSqlite(config, 'pull');
|
||||
process.exit(1);
|
||||
}
|
||||
return {
|
||||
dialect: 'sqlite',
|
||||
out: config.out,
|
||||
breakpoints: config.breakpoints,
|
||||
casing: config.casing,
|
||||
credentials: parsed.data,
|
||||
tablesFilter,
|
||||
schemasFilter,
|
||||
prefix: config.migrations?.prefix || 'index',
|
||||
entities: config.entities,
|
||||
};
|
||||
}
|
||||
|
||||
if (dialect === 'turso') {
|
||||
const parsed = libSQLCredentials.safeParse(config);
|
||||
if (!parsed.success) {
|
||||
printIssuesLibSQL(config, 'pull');
|
||||
process.exit(1);
|
||||
}
|
||||
return {
|
||||
dialect,
|
||||
out: config.out,
|
||||
breakpoints: config.breakpoints,
|
||||
casing: config.casing,
|
||||
credentials: parsed.data,
|
||||
tablesFilter,
|
||||
schemasFilter,
|
||||
prefix: config.migrations?.prefix || 'index',
|
||||
entities: config.entities,
|
||||
};
|
||||
}
|
||||
|
||||
if (dialect === 'gel') {
|
||||
const parsed = gelCredentials.safeParse(config);
|
||||
if (!parsed.success) {
|
||||
printIssuesGel(config);
|
||||
process.exit(1);
|
||||
}
|
||||
return {
|
||||
dialect,
|
||||
out: config.out,
|
||||
breakpoints: config.breakpoints,
|
||||
casing: config.casing,
|
||||
credentials: parsed.data,
|
||||
tablesFilter,
|
||||
schemasFilter,
|
||||
prefix: config.migrations?.prefix || 'index',
|
||||
entities: config.entities,
|
||||
};
|
||||
}
|
||||
|
||||
assertUnreachable(dialect);
|
||||
};
|
||||
|
||||
export const prepareStudioConfig = async (options: Record<string, unknown>) => {
|
||||
const params = studioCliParams.parse(options);
|
||||
const config = await drizzleConfigFromFile(params.config);
|
||||
const result = studioConfig.safeParse(config);
|
||||
if (!result.success) {
|
||||
if (!('dialect' in config)) {
|
||||
console.log(outputs.studio.noDialect());
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!('dbCredentials' in config)) {
|
||||
console.log(outputs.studio.noCredentials());
|
||||
process.exit(1);
|
||||
}
|
||||
const { host, port } = params;
|
||||
const { dialect, schema, casing } = result.data;
|
||||
const flattened = flattenDatabaseCredentials(config);
|
||||
|
||||
if (dialect === 'postgresql') {
|
||||
const parsed = postgresCredentials.safeParse(flattened);
|
||||
if (!parsed.success) {
|
||||
printIssuesPg(flattened as Record<string, unknown>);
|
||||
process.exit(1);
|
||||
}
|
||||
const credentials = parsed.data;
|
||||
return {
|
||||
dialect,
|
||||
schema,
|
||||
host,
|
||||
port,
|
||||
credentials,
|
||||
casing,
|
||||
};
|
||||
}
|
||||
|
||||
if (dialect === 'mysql') {
|
||||
const parsed = mysqlCredentials.safeParse(flattened);
|
||||
if (!parsed.success) {
|
||||
printIssuesMysql(flattened as Record<string, unknown>);
|
||||
process.exit(1);
|
||||
}
|
||||
const credentials = parsed.data;
|
||||
return {
|
||||
dialect,
|
||||
schema,
|
||||
host,
|
||||
port,
|
||||
credentials,
|
||||
casing,
|
||||
};
|
||||
}
|
||||
|
||||
if (dialect === 'singlestore') {
|
||||
const parsed = singlestoreCredentials.safeParse(flattened);
|
||||
if (!parsed.success) {
|
||||
printIssuesSingleStore(flattened as Record<string, unknown>);
|
||||
process.exit(1);
|
||||
}
|
||||
const credentials = parsed.data;
|
||||
return {
|
||||
dialect,
|
||||
schema,
|
||||
host,
|
||||
port,
|
||||
credentials,
|
||||
casing,
|
||||
};
|
||||
}
|
||||
|
||||
if (dialect === 'sqlite') {
|
||||
const parsed = sqliteCredentials.safeParse(flattened);
|
||||
if (!parsed.success) {
|
||||
printIssuesSqlite(flattened as Record<string, unknown>, 'studio');
|
||||
process.exit(1);
|
||||
}
|
||||
const credentials = parsed.data;
|
||||
return {
|
||||
dialect,
|
||||
schema,
|
||||
host,
|
||||
port,
|
||||
credentials,
|
||||
casing,
|
||||
};
|
||||
}
|
||||
|
||||
if (dialect === 'turso') {
|
||||
const parsed = libSQLCredentials.safeParse(flattened);
|
||||
if (!parsed.success) {
|
||||
printIssuesLibSQL(flattened as Record<string, unknown>, 'studio');
|
||||
process.exit(1);
|
||||
}
|
||||
const credentials = parsed.data;
|
||||
return {
|
||||
dialect,
|
||||
schema,
|
||||
host,
|
||||
port,
|
||||
credentials,
|
||||
casing,
|
||||
};
|
||||
}
|
||||
|
||||
if (dialect === 'gel') {
|
||||
console.log(
|
||||
error(
|
||||
`You can't use 'studio' command with Gel dialect`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
assertUnreachable(dialect);
|
||||
};
|
||||
|
||||
export const migrateConfig = object({
|
||||
dialect,
|
||||
out: string().optional().default('drizzle'),
|
||||
migrations: configMigrations,
|
||||
});
|
||||
|
||||
export const prepareMigrateConfig = async (configPath: string | undefined) => {
|
||||
const config = await drizzleConfigFromFile(configPath);
|
||||
const parsed = migrateConfig.safeParse(config);
|
||||
if (parsed.error) {
|
||||
console.log(error('Please provide required params:'));
|
||||
console.log(wrapParam('dialect', config.dialect));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { dialect, out } = parsed.data;
|
||||
const { schema, table } = parsed.data.migrations || {};
|
||||
const flattened = flattenDatabaseCredentials(config);
|
||||
|
||||
if (dialect === 'postgresql') {
|
||||
const parsed = postgresCredentials.safeParse(flattened);
|
||||
if (!parsed.success) {
|
||||
printIssuesPg(flattened as Record<string, unknown>);
|
||||
process.exit(1);
|
||||
}
|
||||
const credentials = parsed.data;
|
||||
return {
|
||||
dialect,
|
||||
out,
|
||||
credentials,
|
||||
schema,
|
||||
table,
|
||||
};
|
||||
}
|
||||
|
||||
if (dialect === 'mysql') {
|
||||
const parsed = mysqlCredentials.safeParse(flattened);
|
||||
if (!parsed.success) {
|
||||
printIssuesMysql(flattened as Record<string, unknown>);
|
||||
process.exit(1);
|
||||
}
|
||||
const credentials = parsed.data;
|
||||
return {
|
||||
dialect,
|
||||
out,
|
||||
credentials,
|
||||
schema,
|
||||
table,
|
||||
};
|
||||
}
|
||||
|
||||
if (dialect === 'singlestore') {
|
||||
const parsed = singlestoreCredentials.safeParse(flattened);
|
||||
if (!parsed.success) {
|
||||
printIssuesSingleStore(flattened as Record<string, unknown>);
|
||||
process.exit(1);
|
||||
}
|
||||
const credentials = parsed.data;
|
||||
return {
|
||||
dialect,
|
||||
out,
|
||||
credentials,
|
||||
schema,
|
||||
table,
|
||||
};
|
||||
}
|
||||
|
||||
if (dialect === 'sqlite') {
|
||||
const parsed = sqliteCredentials.safeParse(flattened);
|
||||
if (!parsed.success) {
|
||||
printIssuesSqlite(flattened as Record<string, unknown>, 'migrate');
|
||||
process.exit(1);
|
||||
}
|
||||
const credentials = parsed.data;
|
||||
return {
|
||||
dialect,
|
||||
out,
|
||||
credentials,
|
||||
schema,
|
||||
table,
|
||||
};
|
||||
}
|
||||
if (dialect === 'turso') {
|
||||
const parsed = libSQLCredentials.safeParse(flattened);
|
||||
if (!parsed.success) {
|
||||
printIssuesLibSQL(flattened as Record<string, unknown>, 'migrate');
|
||||
process.exit(1);
|
||||
}
|
||||
const credentials = parsed.data;
|
||||
return {
|
||||
dialect,
|
||||
out,
|
||||
credentials,
|
||||
schema,
|
||||
table,
|
||||
};
|
||||
}
|
||||
|
||||
if (dialect === 'gel') {
|
||||
console.log(
|
||||
error(
|
||||
`You can't use 'migrate' command with Gel dialect`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
assertUnreachable(dialect);
|
||||
};
|
||||
|
||||
export const drizzleConfigFromFile = async (
|
||||
configPath?: string,
|
||||
isExport?: boolean,
|
||||
): Promise<CliConfig> => {
|
||||
const prefix = process.env.TEST_CONFIG_PATH_PREFIX || '';
|
||||
|
||||
const defaultTsConfigExists = existsSync(resolve(join(prefix, 'drizzle.config.ts')));
|
||||
const defaultJsConfigExists = existsSync(resolve(join(prefix, 'drizzle.config.js')));
|
||||
existsSync(
|
||||
join(resolve('drizzle.config.json')),
|
||||
);
|
||||
|
||||
const defaultConfigPath = defaultTsConfigExists
|
||||
? 'drizzle.config.ts'
|
||||
: defaultJsConfigExists
|
||||
? 'drizzle.config.js'
|
||||
: 'drizzle.config.json';
|
||||
|
||||
if (!configPath && !isExport) {
|
||||
console.log(
|
||||
chalk.gray(
|
||||
`No config path provided, using default '${defaultConfigPath}'`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const path: string = resolve(join(prefix, configPath ?? defaultConfigPath));
|
||||
|
||||
if (!existsSync(path)) {
|
||||
console.log(`${path} file does not exist`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!isExport) console.log(chalk.grey(`Reading config file '${path}'`));
|
||||
|
||||
return safeRegister(async () => {
|
||||
const required = require(`${path}`);
|
||||
const content = required.default ?? required;
|
||||
|
||||
// --- get response and then check by each dialect independently
|
||||
const res = configCommonSchema.safeParse(content);
|
||||
if (!res.success) {
|
||||
console.log(res.error);
|
||||
if (!('dialect' in content)) {
|
||||
console.log(error("Please specify 'dialect' param in config file"));
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return res.data;
|
||||
});
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,48 @@
|
||||
import { command, run } from '@drizzle-team/brocli';
|
||||
import chalk from 'chalk';
|
||||
import { check, drop, exportRaw, generate, migrate, pull, push, studio, up } from './schema';
|
||||
import { ormCoreVersions } from './utils';
|
||||
|
||||
const version = async () => {
|
||||
const { npmVersion } = await ormCoreVersions();
|
||||
const ormVersion = npmVersion ? `drizzle-orm: v${npmVersion}` : '';
|
||||
const envVersion = process.env.DRIZZLE_KIT_VERSION;
|
||||
const kitVersion = envVersion ? `v${envVersion}` : '--';
|
||||
const versions = `drizzle-kit: ${kitVersion}\n${ormVersion}`;
|
||||
console.log(chalk.gray(versions), '\n');
|
||||
};
|
||||
|
||||
const legacyCommand = (name: string, newName: string) => {
|
||||
return command({
|
||||
name,
|
||||
hidden: true,
|
||||
handler: () => {
|
||||
console.log(
|
||||
`This command is deprecated, please use updated '${newName}' command (see https://orm.drizzle.team/kit-docs/upgrade-21#how-to-migrate-to-0210)`,
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const legacy = [
|
||||
legacyCommand('generate:pg', 'generate'),
|
||||
legacyCommand('generate:mysql', 'generate'),
|
||||
legacyCommand('generate:sqlite', 'generate'),
|
||||
legacyCommand('push:pg', 'push'),
|
||||
legacyCommand('push:mysql', 'push'),
|
||||
legacyCommand('push:sqlite', 'push'),
|
||||
legacyCommand('introspect:pg', 'introspect'),
|
||||
legacyCommand('introspect:mysql', 'introspect'),
|
||||
legacyCommand('introspect:sqlite', 'introspect'),
|
||||
legacyCommand('up:pg', 'up'),
|
||||
legacyCommand('up:mysql', 'up'),
|
||||
legacyCommand('up:sqlite', 'up'),
|
||||
legacyCommand('check:pg', 'check'),
|
||||
legacyCommand('check:mysql', 'check'),
|
||||
legacyCommand('check:sqlite', 'check'),
|
||||
];
|
||||
|
||||
run([generate, migrate, pull, push, studio, up, check, drop, exportRaw, ...legacy], {
|
||||
name: 'drizzle-kit',
|
||||
version: version,
|
||||
});
|
||||
@@ -0,0 +1,856 @@
|
||||
import { boolean, command, number, string } from '@drizzle-team/brocli';
|
||||
import chalk from 'chalk';
|
||||
import 'dotenv/config';
|
||||
import { mkdirSync } from 'fs';
|
||||
import { renderWithTask } from 'hanji';
|
||||
import { dialects } from 'src/schemaValidator';
|
||||
import '../@types/utils';
|
||||
import { assertUnreachable } from '../global';
|
||||
import type { Setup } from '../serializer/studio';
|
||||
import { assertV1OutFolder } from '../utils';
|
||||
import { certs } from '../utils/certs';
|
||||
import { checkHandler } from './commands/check';
|
||||
import { dropMigration } from './commands/drop';
|
||||
import { upMysqlHandler } from './commands/mysqlUp';
|
||||
import { upPgHandler } from './commands/pgUp';
|
||||
import { upSinglestoreHandler } from './commands/singlestoreUp';
|
||||
import { upSqliteHandler } from './commands/sqliteUp';
|
||||
import {
|
||||
prepareCheckParams,
|
||||
prepareDropParams,
|
||||
prepareExportConfig,
|
||||
prepareGenerateConfig,
|
||||
prepareMigrateConfig,
|
||||
preparePullConfig,
|
||||
preparePushConfig,
|
||||
prepareStudioConfig,
|
||||
} from './commands/utils';
|
||||
import { assertOrmCoreVersion, assertPackages, assertStudioNodeVersion, ormVersionGt } from './utils';
|
||||
import { assertCollisions, drivers, prefixes } from './validations/common';
|
||||
import { withStyle } from './validations/outputs';
|
||||
import { error, grey, MigrateProgress } from './views';
|
||||
|
||||
const optionDialect = string('dialect')
|
||||
.enum(...dialects)
|
||||
.desc(
|
||||
`Database dialect: 'gel', 'postgresql', 'mysql', 'sqlite', 'turso' or 'singlestore'`,
|
||||
);
|
||||
const optionOut = string().desc("Output folder, 'drizzle' by default");
|
||||
const optionConfig = string().desc('Path to drizzle config file');
|
||||
const optionBreakpoints = boolean().desc(
|
||||
`Prepare SQL statements with breakpoints`,
|
||||
);
|
||||
|
||||
const optionDriver = string()
|
||||
.enum(...drivers)
|
||||
.desc('Database driver');
|
||||
|
||||
const optionCasing = string().enum('camelCase', 'snake_case').desc('Casing for serialization');
|
||||
|
||||
export const generate = command({
|
||||
name: 'generate',
|
||||
options: {
|
||||
config: optionConfig,
|
||||
dialect: optionDialect,
|
||||
driver: optionDriver,
|
||||
casing: optionCasing,
|
||||
schema: string().desc('Path to a schema file or folder'),
|
||||
out: optionOut,
|
||||
name: string().desc('Migration file name'),
|
||||
breakpoints: optionBreakpoints,
|
||||
custom: boolean()
|
||||
.desc('Prepare empty migration file for custom SQL')
|
||||
.default(false),
|
||||
prefix: string()
|
||||
.enum(...prefixes)
|
||||
.default('index'),
|
||||
},
|
||||
transform: async (opts) => {
|
||||
const from = assertCollisions(
|
||||
'generate',
|
||||
opts,
|
||||
['prefix', 'name', 'custom'],
|
||||
['driver', 'breakpoints', 'schema', 'out', 'dialect', 'casing'],
|
||||
);
|
||||
return prepareGenerateConfig(opts, from);
|
||||
},
|
||||
handler: async (opts) => {
|
||||
await assertOrmCoreVersion();
|
||||
await assertPackages('drizzle-orm');
|
||||
|
||||
// const parsed = cliConfigGenerate.parse(opts);
|
||||
|
||||
const {
|
||||
prepareAndMigratePg,
|
||||
prepareAndMigrateMysql,
|
||||
prepareAndMigrateSqlite,
|
||||
prepareAndMigrateLibSQL,
|
||||
prepareAndMigrateSingleStore,
|
||||
} = await import('./commands/migrate');
|
||||
|
||||
const dialect = opts.dialect;
|
||||
if (dialect === 'postgresql') {
|
||||
await prepareAndMigratePg(opts);
|
||||
} else if (dialect === 'mysql') {
|
||||
await prepareAndMigrateMysql(opts);
|
||||
} else if (dialect === 'sqlite') {
|
||||
await prepareAndMigrateSqlite(opts);
|
||||
} else if (dialect === 'turso') {
|
||||
await prepareAndMigrateLibSQL(opts);
|
||||
} else if (dialect === 'singlestore') {
|
||||
await prepareAndMigrateSingleStore(opts);
|
||||
} else if (dialect === 'gel') {
|
||||
console.log(
|
||||
error(
|
||||
`You can't use 'generate' command with Gel dialect`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
} else {
|
||||
assertUnreachable(dialect);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const migrate = command({
|
||||
name: 'migrate',
|
||||
options: {
|
||||
config: optionConfig,
|
||||
},
|
||||
transform: async (opts) => {
|
||||
return await prepareMigrateConfig(opts.config);
|
||||
},
|
||||
handler: async (opts) => {
|
||||
await assertOrmCoreVersion();
|
||||
await assertPackages('drizzle-orm');
|
||||
|
||||
const { dialect, schema, table, out, credentials } = opts;
|
||||
try {
|
||||
if (dialect === 'postgresql') {
|
||||
if ('driver' in credentials) {
|
||||
const { driver } = credentials;
|
||||
if (driver === 'aws-data-api') {
|
||||
if (!(await ormVersionGt('0.30.10'))) {
|
||||
console.log(
|
||||
"To use 'aws-data-api' driver - please update drizzle-orm to the latest version",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (driver === 'pglite') {
|
||||
if (!(await ormVersionGt('0.30.6'))) {
|
||||
console.log(
|
||||
"To use 'pglite' driver - please update drizzle-orm to the latest version",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
assertUnreachable(driver);
|
||||
}
|
||||
}
|
||||
const { preparePostgresDB } = await import('./connections');
|
||||
const { migrate } = await preparePostgresDB(credentials);
|
||||
await renderWithTask(
|
||||
new MigrateProgress(),
|
||||
migrate({
|
||||
migrationsFolder: out,
|
||||
migrationsTable: table,
|
||||
migrationsSchema: schema,
|
||||
}),
|
||||
);
|
||||
} else if (dialect === 'mysql') {
|
||||
const { connectToMySQL } = await import('./connections');
|
||||
const { migrate } = await connectToMySQL(credentials);
|
||||
await renderWithTask(
|
||||
new MigrateProgress(),
|
||||
migrate({
|
||||
migrationsFolder: out,
|
||||
migrationsTable: table,
|
||||
migrationsSchema: schema,
|
||||
}),
|
||||
);
|
||||
} else if (dialect === 'singlestore') {
|
||||
const { connectToSingleStore } = await import('./connections');
|
||||
const { migrate } = await connectToSingleStore(credentials);
|
||||
await renderWithTask(
|
||||
new MigrateProgress(),
|
||||
migrate({
|
||||
migrationsFolder: out,
|
||||
migrationsTable: table,
|
||||
migrationsSchema: schema,
|
||||
}),
|
||||
);
|
||||
} else if (dialect === 'sqlite') {
|
||||
const { connectToSQLite } = await import('./connections');
|
||||
const { migrate } = await connectToSQLite(credentials);
|
||||
await renderWithTask(
|
||||
new MigrateProgress(),
|
||||
migrate({
|
||||
migrationsFolder: opts.out,
|
||||
migrationsTable: table,
|
||||
migrationsSchema: schema,
|
||||
}),
|
||||
);
|
||||
} else if (dialect === 'turso') {
|
||||
const { connectToLibSQL } = await import('./connections');
|
||||
const { migrate } = await connectToLibSQL(credentials);
|
||||
await renderWithTask(
|
||||
new MigrateProgress(),
|
||||
migrate({
|
||||
migrationsFolder: opts.out,
|
||||
migrationsTable: table,
|
||||
migrationsSchema: schema,
|
||||
}),
|
||||
);
|
||||
} else if (dialect === 'gel') {
|
||||
console.log(
|
||||
error(
|
||||
`You can't use 'migrate' command with Gel dialect`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
} else {
|
||||
assertUnreachable(dialect);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
},
|
||||
});
|
||||
|
||||
const optionsFilters = {
|
||||
tablesFilter: string().desc('Table name filters'),
|
||||
schemaFilters: string().desc('Schema name filters'),
|
||||
extensionsFilters: string().desc(
|
||||
'`Database extensions internal database filters',
|
||||
),
|
||||
} as const;
|
||||
|
||||
const optionsDatabaseCredentials = {
|
||||
url: string().desc('Database connection URL'),
|
||||
host: string().desc('Database host'),
|
||||
port: string().desc('Database port'),
|
||||
user: string().desc('Database user'),
|
||||
password: string().desc('Database password'),
|
||||
database: string().desc('Database name'),
|
||||
ssl: string().desc('ssl mode'),
|
||||
// Turso
|
||||
authToken: string('auth-token').desc('Database auth token [Turso]'),
|
||||
// gel
|
||||
tlsSecurity: string('tlsSecurity').desc('tls security mode'),
|
||||
// specific cases
|
||||
driver: optionDriver,
|
||||
} as const;
|
||||
|
||||
export const push = command({
|
||||
name: 'push',
|
||||
options: {
|
||||
config: optionConfig,
|
||||
dialect: optionDialect,
|
||||
casing: optionCasing,
|
||||
schema: string().desc('Path to a schema file or folder'),
|
||||
...optionsFilters,
|
||||
...optionsDatabaseCredentials,
|
||||
verbose: boolean()
|
||||
.desc('Print all statements for each push')
|
||||
.default(false),
|
||||
strict: boolean().desc('Always ask for confirmation').default(false),
|
||||
force: boolean()
|
||||
.desc(
|
||||
'Auto-approve all data loss statements. Note: Data loss statements may truncate your tables and data',
|
||||
)
|
||||
.default(false),
|
||||
},
|
||||
transform: async (opts) => {
|
||||
const from = assertCollisions(
|
||||
'push',
|
||||
opts,
|
||||
['force', 'verbose', 'strict'],
|
||||
[
|
||||
'schema',
|
||||
'dialect',
|
||||
'driver',
|
||||
'url',
|
||||
'host',
|
||||
'port',
|
||||
'user',
|
||||
'password',
|
||||
'database',
|
||||
'ssl',
|
||||
'authToken',
|
||||
'schemaFilters',
|
||||
'extensionsFilters',
|
||||
'tablesFilter',
|
||||
'casing',
|
||||
'tlsSecurity',
|
||||
],
|
||||
);
|
||||
|
||||
return preparePushConfig(opts, from);
|
||||
},
|
||||
handler: async (config) => {
|
||||
await assertPackages('drizzle-orm');
|
||||
await assertOrmCoreVersion();
|
||||
|
||||
const {
|
||||
dialect,
|
||||
schemaPath,
|
||||
strict,
|
||||
verbose,
|
||||
credentials,
|
||||
tablesFilter,
|
||||
schemasFilter,
|
||||
force,
|
||||
casing,
|
||||
entities,
|
||||
} = config;
|
||||
|
||||
try {
|
||||
if (dialect === 'mysql') {
|
||||
const { mysqlPush } = await import('./commands/push');
|
||||
await mysqlPush(
|
||||
schemaPath,
|
||||
credentials,
|
||||
tablesFilter,
|
||||
strict,
|
||||
verbose,
|
||||
force,
|
||||
casing,
|
||||
);
|
||||
} else if (dialect === 'postgresql') {
|
||||
if ('driver' in credentials) {
|
||||
const { driver } = credentials;
|
||||
if (driver === 'aws-data-api') {
|
||||
if (!(await ormVersionGt('0.30.10'))) {
|
||||
console.log(
|
||||
"To use 'aws-data-api' driver - please update drizzle-orm to the latest version",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (driver === 'pglite') {
|
||||
if (!(await ormVersionGt('0.30.6'))) {
|
||||
console.log(
|
||||
"To use 'pglite' driver - please update drizzle-orm to the latest version",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
assertUnreachable(driver);
|
||||
}
|
||||
}
|
||||
|
||||
const { pgPush } = await import('./commands/push');
|
||||
await pgPush(
|
||||
schemaPath,
|
||||
verbose,
|
||||
strict,
|
||||
credentials,
|
||||
tablesFilter,
|
||||
schemasFilter,
|
||||
entities,
|
||||
force,
|
||||
casing,
|
||||
);
|
||||
} else if (dialect === 'sqlite') {
|
||||
const { sqlitePush } = await import('./commands/push');
|
||||
await sqlitePush(
|
||||
schemaPath,
|
||||
verbose,
|
||||
strict,
|
||||
credentials,
|
||||
tablesFilter,
|
||||
force,
|
||||
casing,
|
||||
);
|
||||
} else if (dialect === 'turso') {
|
||||
const { libSQLPush } = await import('./commands/push');
|
||||
await libSQLPush(
|
||||
schemaPath,
|
||||
verbose,
|
||||
strict,
|
||||
credentials,
|
||||
tablesFilter,
|
||||
force,
|
||||
casing,
|
||||
);
|
||||
} else if (dialect === 'singlestore') {
|
||||
const { singlestorePush } = await import('./commands/push');
|
||||
await singlestorePush(
|
||||
schemaPath,
|
||||
credentials,
|
||||
tablesFilter,
|
||||
strict,
|
||||
verbose,
|
||||
force,
|
||||
casing,
|
||||
);
|
||||
} else if (dialect === 'gel') {
|
||||
console.log(
|
||||
error(
|
||||
`You can't use 'push' command with Gel dialect`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
} else {
|
||||
assertUnreachable(dialect);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
process.exit(0);
|
||||
},
|
||||
});
|
||||
|
||||
export const check = command({
|
||||
name: 'check',
|
||||
options: {
|
||||
config: optionConfig,
|
||||
dialect: optionDialect,
|
||||
out: optionOut,
|
||||
},
|
||||
transform: async (opts) => {
|
||||
const from = assertCollisions('check', opts, [], ['dialect', 'out']);
|
||||
return prepareCheckParams(opts, from);
|
||||
},
|
||||
handler: async (config) => {
|
||||
await assertOrmCoreVersion();
|
||||
|
||||
const { out, dialect } = config;
|
||||
checkHandler(out, dialect);
|
||||
console.log("Everything's fine 🐶🔥");
|
||||
},
|
||||
});
|
||||
|
||||
export const up = command({
|
||||
name: 'up',
|
||||
options: {
|
||||
config: optionConfig,
|
||||
dialect: optionDialect,
|
||||
out: optionOut,
|
||||
},
|
||||
transform: async (opts) => {
|
||||
const from = assertCollisions('check', opts, [], ['dialect', 'out']);
|
||||
return prepareCheckParams(opts, from);
|
||||
},
|
||||
handler: async (config) => {
|
||||
await assertOrmCoreVersion();
|
||||
|
||||
const { out, dialect } = config;
|
||||
await assertPackages('drizzle-orm');
|
||||
|
||||
if (dialect === 'postgresql') {
|
||||
upPgHandler(out);
|
||||
}
|
||||
|
||||
if (dialect === 'mysql') {
|
||||
upMysqlHandler(out);
|
||||
}
|
||||
|
||||
if (dialect === 'sqlite' || dialect === 'turso') {
|
||||
upSqliteHandler(out);
|
||||
}
|
||||
|
||||
if (dialect === 'singlestore') {
|
||||
upSinglestoreHandler(out);
|
||||
}
|
||||
|
||||
if (dialect === 'gel') {
|
||||
console.log(
|
||||
error(
|
||||
`You can't use 'up' command with Gel dialect`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const pull = command({
|
||||
name: 'introspect',
|
||||
aliases: ['pull'],
|
||||
options: {
|
||||
config: optionConfig,
|
||||
dialect: optionDialect,
|
||||
out: optionOut,
|
||||
breakpoints: optionBreakpoints,
|
||||
casing: string('introspect-casing').enum('camel', 'preserve'),
|
||||
...optionsFilters,
|
||||
...optionsDatabaseCredentials,
|
||||
},
|
||||
transform: async (opts) => {
|
||||
const from = assertCollisions(
|
||||
'introspect',
|
||||
opts,
|
||||
[],
|
||||
[
|
||||
'dialect',
|
||||
'driver',
|
||||
'out',
|
||||
'url',
|
||||
'host',
|
||||
'port',
|
||||
'user',
|
||||
'password',
|
||||
'database',
|
||||
'ssl',
|
||||
'authToken',
|
||||
'casing',
|
||||
'breakpoints',
|
||||
'tablesFilter',
|
||||
'schemaFilters',
|
||||
'extensionsFilters',
|
||||
'tlsSecurity',
|
||||
],
|
||||
);
|
||||
return preparePullConfig(opts, from);
|
||||
},
|
||||
handler: async (config) => {
|
||||
await assertPackages('drizzle-orm');
|
||||
await assertOrmCoreVersion();
|
||||
|
||||
const {
|
||||
dialect,
|
||||
credentials,
|
||||
out,
|
||||
casing,
|
||||
breakpoints,
|
||||
tablesFilter,
|
||||
schemasFilter,
|
||||
prefix,
|
||||
entities,
|
||||
} = config;
|
||||
mkdirSync(out, { recursive: true });
|
||||
|
||||
console.log(
|
||||
grey(
|
||||
`Pulling from [${
|
||||
schemasFilter
|
||||
.map((it) => `'${it}'`)
|
||||
.join(', ')
|
||||
}] list of schemas`,
|
||||
),
|
||||
);
|
||||
console.log();
|
||||
|
||||
try {
|
||||
if (dialect === 'postgresql') {
|
||||
if ('driver' in credentials) {
|
||||
const { driver } = credentials;
|
||||
if (driver === 'aws-data-api') {
|
||||
if (!(await ormVersionGt('0.30.10'))) {
|
||||
console.log(
|
||||
"To use 'aws-data-api' driver - please update drizzle-orm to the latest version",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (driver === 'pglite') {
|
||||
if (!(await ormVersionGt('0.30.6'))) {
|
||||
console.log(
|
||||
"To use 'pglite' driver - please update drizzle-orm to the latest version",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
assertUnreachable(driver);
|
||||
}
|
||||
}
|
||||
|
||||
const { introspectPostgres } = await import('./commands/introspect');
|
||||
await introspectPostgres(
|
||||
casing,
|
||||
out,
|
||||
breakpoints,
|
||||
credentials,
|
||||
tablesFilter,
|
||||
schemasFilter,
|
||||
prefix,
|
||||
entities,
|
||||
);
|
||||
} else if (dialect === 'mysql') {
|
||||
const { introspectMysql } = await import('./commands/introspect');
|
||||
await introspectMysql(
|
||||
casing,
|
||||
out,
|
||||
breakpoints,
|
||||
credentials,
|
||||
tablesFilter,
|
||||
prefix,
|
||||
);
|
||||
} else if (dialect === 'sqlite') {
|
||||
const { introspectSqlite } = await import('./commands/introspect');
|
||||
await introspectSqlite(
|
||||
casing,
|
||||
out,
|
||||
breakpoints,
|
||||
credentials,
|
||||
tablesFilter,
|
||||
prefix,
|
||||
);
|
||||
} else if (dialect === 'turso') {
|
||||
const { introspectLibSQL } = await import('./commands/introspect');
|
||||
await introspectLibSQL(
|
||||
casing,
|
||||
out,
|
||||
breakpoints,
|
||||
credentials,
|
||||
tablesFilter,
|
||||
prefix,
|
||||
);
|
||||
} else if (dialect === 'singlestore') {
|
||||
const { introspectSingleStore } = await import('./commands/introspect');
|
||||
await introspectSingleStore(
|
||||
casing,
|
||||
out,
|
||||
breakpoints,
|
||||
credentials,
|
||||
tablesFilter,
|
||||
prefix,
|
||||
);
|
||||
} else if (dialect === 'gel') {
|
||||
const { introspectGel } = await import('./commands/introspect');
|
||||
await introspectGel(
|
||||
casing,
|
||||
out,
|
||||
breakpoints,
|
||||
credentials,
|
||||
tablesFilter,
|
||||
schemasFilter,
|
||||
prefix,
|
||||
entities,
|
||||
);
|
||||
} else {
|
||||
assertUnreachable(dialect);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
process.exit(0);
|
||||
},
|
||||
});
|
||||
|
||||
export const drop = command({
|
||||
name: 'drop',
|
||||
options: {
|
||||
config: optionConfig,
|
||||
out: optionOut,
|
||||
driver: optionDriver,
|
||||
},
|
||||
transform: async (opts) => {
|
||||
const from = assertCollisions('check', opts, [], ['driver', 'out']);
|
||||
return prepareDropParams(opts, from);
|
||||
},
|
||||
handler: async (config) => {
|
||||
await assertOrmCoreVersion();
|
||||
|
||||
assertV1OutFolder(config.out);
|
||||
await dropMigration(config);
|
||||
},
|
||||
});
|
||||
|
||||
export const studio = command({
|
||||
name: 'studio',
|
||||
options: {
|
||||
config: optionConfig,
|
||||
port: number().desc('Custom port for drizzle studio [default=4983]'),
|
||||
host: string().desc('Custom host for drizzle studio [default=0.0.0.0]'),
|
||||
verbose: boolean()
|
||||
.default(false)
|
||||
.desc('Print all stataments that are executed by Studio'),
|
||||
},
|
||||
handler: async (opts) => {
|
||||
await assertOrmCoreVersion();
|
||||
await assertPackages('drizzle-orm');
|
||||
|
||||
assertStudioNodeVersion();
|
||||
|
||||
const {
|
||||
dialect,
|
||||
schema: schemaPath,
|
||||
port,
|
||||
host,
|
||||
credentials,
|
||||
casing,
|
||||
} = await prepareStudioConfig(opts);
|
||||
|
||||
const {
|
||||
drizzleForPostgres,
|
||||
preparePgSchema,
|
||||
prepareMySqlSchema,
|
||||
drizzleForMySQL,
|
||||
prepareSQLiteSchema,
|
||||
drizzleForSQLite,
|
||||
prepareSingleStoreSchema,
|
||||
drizzleForSingleStore,
|
||||
drizzleForLibSQL,
|
||||
} = await import('../serializer/studio');
|
||||
|
||||
let setup: Setup;
|
||||
try {
|
||||
if (dialect === 'postgresql') {
|
||||
if ('driver' in credentials) {
|
||||
const { driver } = credentials;
|
||||
if (driver === 'aws-data-api') {
|
||||
if (!(await ormVersionGt('0.30.10'))) {
|
||||
console.log(
|
||||
"To use 'aws-data-api' driver - please update drizzle-orm to the latest version",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (driver === 'pglite') {
|
||||
if (!(await ormVersionGt('0.30.6'))) {
|
||||
console.log(
|
||||
"To use 'pglite' driver - please update drizzle-orm to the latest version",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
assertUnreachable(driver);
|
||||
}
|
||||
}
|
||||
|
||||
const { schema, relations, files } = schemaPath
|
||||
? await preparePgSchema(schemaPath)
|
||||
: { schema: {}, relations: {}, files: [] };
|
||||
setup = await drizzleForPostgres(credentials, schema, relations, files, casing);
|
||||
} else if (dialect === 'mysql') {
|
||||
const { schema, relations, files } = schemaPath
|
||||
? await prepareMySqlSchema(schemaPath)
|
||||
: { schema: {}, relations: {}, files: [] };
|
||||
setup = await drizzleForMySQL(credentials, schema, relations, files, casing);
|
||||
} else if (dialect === 'sqlite') {
|
||||
const { schema, relations, files } = schemaPath
|
||||
? await prepareSQLiteSchema(schemaPath)
|
||||
: { schema: {}, relations: {}, files: [] };
|
||||
setup = await drizzleForSQLite(credentials, schema, relations, files, casing);
|
||||
} else if (dialect === 'turso') {
|
||||
const { schema, relations, files } = schemaPath
|
||||
? await prepareSQLiteSchema(schemaPath)
|
||||
: { schema: {}, relations: {}, files: [] };
|
||||
setup = await drizzleForLibSQL(credentials, schema, relations, files, casing);
|
||||
} else if (dialect === 'singlestore') {
|
||||
const { schema, relations, files } = schemaPath
|
||||
? await prepareSingleStoreSchema(schemaPath)
|
||||
: { schema: {}, relations: {}, files: [] };
|
||||
setup = await drizzleForSingleStore(
|
||||
credentials,
|
||||
schema,
|
||||
relations,
|
||||
files,
|
||||
casing,
|
||||
);
|
||||
} else if (dialect === 'gel') {
|
||||
console.log(
|
||||
error(
|
||||
`You can't use 'studio' command with Gel dialect`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
} else {
|
||||
assertUnreachable(dialect);
|
||||
}
|
||||
|
||||
const { prepareServer } = await import('../serializer/studio');
|
||||
|
||||
const server = await prepareServer(setup);
|
||||
|
||||
console.log();
|
||||
console.log(
|
||||
withStyle.fullWarning(
|
||||
'Drizzle Studio is currently in Beta. If you find anything that is not working as expected or should be improved, feel free to create an issue on GitHub: https://github.com/drizzle-team/drizzle-kit-mirror/issues/new or write to us on Discord: https://discord.gg/WcRKz2FFxN',
|
||||
),
|
||||
);
|
||||
|
||||
const { key, cert } = (await certs()) || {};
|
||||
server.start({
|
||||
host,
|
||||
port,
|
||||
key,
|
||||
cert,
|
||||
cb: (err, _address) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
} else {
|
||||
const queryParams: { port?: number; host?: string } = {};
|
||||
if (port !== 4983) {
|
||||
queryParams.port = port;
|
||||
}
|
||||
|
||||
if (host !== '127.0.0.1') {
|
||||
queryParams.host = host;
|
||||
}
|
||||
|
||||
const queryString = Object.keys(queryParams)
|
||||
.map((key: keyof { port?: number; host?: string }) => {
|
||||
return `${key}=${queryParams[key]}`;
|
||||
})
|
||||
.join('&');
|
||||
|
||||
console.log(
|
||||
`\nDrizzle Studio is up and running on ${
|
||||
chalk.blue(
|
||||
`https://local.drizzle.studio${queryString ? `?${queryString}` : ''}`,
|
||||
)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
process.exit(0);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const exportRaw = command({
|
||||
name: 'export',
|
||||
desc: 'Generate diff between current state and empty state in specified formats: sql',
|
||||
options: {
|
||||
sql: boolean('sql').default(true).desc('Generate as sql'),
|
||||
config: optionConfig,
|
||||
dialect: optionDialect,
|
||||
schema: string().desc('Path to a schema file or folder'),
|
||||
},
|
||||
transform: async (opts) => {
|
||||
const from = assertCollisions('export', opts, ['sql'], ['dialect', 'schema']);
|
||||
return prepareExportConfig(opts, from);
|
||||
},
|
||||
handler: async (opts) => {
|
||||
await assertOrmCoreVersion();
|
||||
await assertPackages('drizzle-orm');
|
||||
|
||||
const {
|
||||
prepareAndExportPg,
|
||||
prepareAndExportMysql,
|
||||
prepareAndExportSqlite,
|
||||
prepareAndExportLibSQL,
|
||||
prepareAndExportSinglestore,
|
||||
} = await import(
|
||||
'./commands/migrate'
|
||||
);
|
||||
|
||||
const dialect = opts.dialect;
|
||||
if (dialect === 'postgresql') {
|
||||
await prepareAndExportPg(opts);
|
||||
} else if (dialect === 'mysql') {
|
||||
await prepareAndExportMysql(opts);
|
||||
} else if (dialect === 'sqlite') {
|
||||
await prepareAndExportSqlite(opts);
|
||||
} else if (dialect === 'turso') {
|
||||
await prepareAndExportLibSQL(opts);
|
||||
} else if (dialect === 'singlestore') {
|
||||
await prepareAndExportSinglestore(opts);
|
||||
} else if (dialect === 'gel') {
|
||||
console.log(
|
||||
error(
|
||||
`You can't use 'export' command with Gel dialect`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
} else {
|
||||
assertUnreachable(dialect);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import chalk from 'chalk';
|
||||
import { Prompt, SelectState } from 'hanji';
|
||||
|
||||
export class Select extends Prompt<{ index: number; value: string }> {
|
||||
private readonly data: SelectState<{ label: string; value: string }>;
|
||||
|
||||
constructor(items: string[]) {
|
||||
super();
|
||||
this.on('attach', (terminal) => terminal.toggleCursor('hide'));
|
||||
this.on('detach', (terminal) => terminal.toggleCursor('show'));
|
||||
|
||||
this.data = new SelectState(
|
||||
items.map((it) => ({ label: it, value: `${it}-value` })),
|
||||
);
|
||||
this.data.bind(this);
|
||||
}
|
||||
|
||||
render(status: 'idle' | 'submitted' | 'aborted'): string {
|
||||
if (status === 'submitted' || status === 'aborted') return '';
|
||||
|
||||
let text = ``;
|
||||
this.data.items.forEach((it, idx) => {
|
||||
text += idx === this.data.selectedIdx
|
||||
? `${chalk.green('❯ ' + it.label)}`
|
||||
: ` ${it.label}`;
|
||||
text += idx != this.data.items.length - 1 ? '\n' : '';
|
||||
});
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
result() {
|
||||
return {
|
||||
index: this.data.selectedIdx,
|
||||
value: this.data.items[this.data.selectedIdx]!.value!,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import semver from 'semver';
|
||||
import { err, warning } from './views';
|
||||
|
||||
export const assertExists = (it?: any) => {
|
||||
if (!it) throw new Error();
|
||||
};
|
||||
|
||||
export const ormVersionGt = async (version: string) => {
|
||||
const { npmVersion } = await import('drizzle-orm/version');
|
||||
if (!semver.gte(npmVersion, version)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const assertStudioNodeVersion = () => {
|
||||
if (semver.gte(process.version, '18.0.0')) return;
|
||||
|
||||
err('Drizzle Studio requires NodeJS v18 or above');
|
||||
process.exit(1);
|
||||
};
|
||||
|
||||
export const checkPackage = async (it: string) => {
|
||||
try {
|
||||
await import(it);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const assertPackages = async (...pkgs: string[]) => {
|
||||
try {
|
||||
for (let i = 0; i < pkgs.length; i++) {
|
||||
const it = pkgs[i];
|
||||
await import(it);
|
||||
}
|
||||
} catch (e) {
|
||||
err(
|
||||
`please install required packages: ${
|
||||
pkgs
|
||||
.map((it) => `'${it}'`)
|
||||
.join(' ')
|
||||
}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// ex: either pg or postgres are needed
|
||||
export const assertEitherPackage = async (
|
||||
...pkgs: string[]
|
||||
): Promise<string[]> => {
|
||||
const availables = [] as string[];
|
||||
for (let i = 0; i < pkgs.length; i++) {
|
||||
try {
|
||||
const it = pkgs[i];
|
||||
await import(it);
|
||||
availables.push(it);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (availables.length > 0) {
|
||||
return availables;
|
||||
}
|
||||
|
||||
err(
|
||||
`Please install one of those packages are needed: ${
|
||||
pkgs
|
||||
.map((it) => `'${it}'`)
|
||||
.join(' or ')
|
||||
}`,
|
||||
);
|
||||
process.exit(1);
|
||||
};
|
||||
|
||||
const requiredApiVersion = 10;
|
||||
export const assertOrmCoreVersion = async () => {
|
||||
try {
|
||||
const { compatibilityVersion } = await import('drizzle-orm/version');
|
||||
|
||||
await import('drizzle-orm/relations');
|
||||
|
||||
if (compatibilityVersion && compatibilityVersion === requiredApiVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!compatibilityVersion || compatibilityVersion < requiredApiVersion) {
|
||||
console.log(
|
||||
'This version of drizzle-kit requires newer version of drizzle-orm\nPlease update drizzle-orm package to the latest version 👍',
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
'This version of drizzle-kit is outdated\nPlease update drizzle-kit package to the latest version 👍',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Please install latest version of drizzle-orm');
|
||||
}
|
||||
process.exit(1);
|
||||
};
|
||||
|
||||
export const ormCoreVersions = async () => {
|
||||
try {
|
||||
const { compatibilityVersion, npmVersion } = await import(
|
||||
'drizzle-orm/version'
|
||||
);
|
||||
return { compatibilityVersion, npmVersion };
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import { array, boolean, intersection, literal, object, string, TypeOf, union } from 'zod';
|
||||
import { dialect } from '../../schemaValidator';
|
||||
import { casing, casingType, prefix } from './common';
|
||||
|
||||
export const cliConfigGenerate = object({
|
||||
dialect: dialect.optional(),
|
||||
schema: union([string(), string().array()]).optional(),
|
||||
out: string().optional().default('./drizzle'),
|
||||
config: string().optional(),
|
||||
name: string().optional(),
|
||||
prefix: prefix.optional(),
|
||||
breakpoints: boolean().optional().default(true),
|
||||
custom: boolean().optional().default(false),
|
||||
}).strict();
|
||||
|
||||
export type CliConfigGenerate = TypeOf<typeof cliConfigGenerate>;
|
||||
|
||||
export const pushParams = object({
|
||||
dialect: dialect,
|
||||
casing: casingType.optional(),
|
||||
schema: union([string(), string().array()]),
|
||||
tablesFilter: union([string(), string().array()]).optional(),
|
||||
schemaFilter: union([string(), string().array()])
|
||||
.optional()
|
||||
.default(['public']),
|
||||
extensionsFilters: literal('postgis').array().optional(),
|
||||
verbose: boolean().optional(),
|
||||
strict: boolean().optional(),
|
||||
entities: object({
|
||||
roles: boolean().or(object({
|
||||
provider: string().optional(),
|
||||
include: string().array().optional(),
|
||||
exclude: string().array().optional(),
|
||||
})).optional().default(false),
|
||||
}).optional(),
|
||||
}).passthrough();
|
||||
|
||||
export type PushParams = TypeOf<typeof pushParams>;
|
||||
|
||||
export const pullParams = object({
|
||||
config: string().optional(),
|
||||
dialect: dialect,
|
||||
out: string().optional().default('drizzle'),
|
||||
tablesFilter: union([string(), string().array()]).optional(),
|
||||
schemaFilter: union([string(), string().array()])
|
||||
.optional()
|
||||
.default(['public']),
|
||||
extensionsFilters: literal('postgis').array().optional(),
|
||||
casing,
|
||||
breakpoints: boolean().optional().default(true),
|
||||
migrations: object({
|
||||
prefix: prefix.optional().default('index'),
|
||||
}).optional(),
|
||||
entities: object({
|
||||
roles: boolean().or(object({
|
||||
provider: string().optional(),
|
||||
include: string().array().optional(),
|
||||
exclude: string().array().optional(),
|
||||
})).optional().default(false),
|
||||
}).optional(),
|
||||
}).passthrough();
|
||||
|
||||
export type Entities = TypeOf<typeof pullParams>['entities'];
|
||||
|
||||
export type PullParams = TypeOf<typeof pullParams>;
|
||||
|
||||
export const configCheck = object({
|
||||
dialect: dialect.optional(),
|
||||
out: string().optional(),
|
||||
});
|
||||
|
||||
export const cliConfigCheck = intersection(
|
||||
object({
|
||||
config: string().optional(),
|
||||
}),
|
||||
configCheck,
|
||||
);
|
||||
|
||||
export type CliCheckConfig = TypeOf<typeof cliConfigCheck>;
|
||||
@@ -0,0 +1,193 @@
|
||||
import chalk from 'chalk';
|
||||
import { UnionToIntersection } from 'hono/utils/types';
|
||||
import { any, boolean, enum as enum_, literal, object, string, TypeOf, union } from 'zod';
|
||||
import { dialect } from '../../schemaValidator';
|
||||
import { outputs } from './outputs';
|
||||
|
||||
export type Commands =
|
||||
| 'introspect'
|
||||
| 'generate'
|
||||
| 'check'
|
||||
| 'up'
|
||||
| 'drop'
|
||||
| 'push'
|
||||
| 'export';
|
||||
|
||||
type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
|
||||
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;
|
||||
type LastTupleElement<TArr extends any[]> = TArr extends [
|
||||
...start: infer _,
|
||||
end: infer Last,
|
||||
] ? Last
|
||||
: never;
|
||||
|
||||
export type UniqueArrayOfUnion<TUnion, TArray extends TUnion[]> = Exclude<
|
||||
TUnion,
|
||||
TArray[number]
|
||||
> extends never ? [TUnion]
|
||||
: [...TArray, Exclude<TUnion, TArray[number]>];
|
||||
|
||||
export const assertCollisions = <
|
||||
T extends Record<string, unknown>,
|
||||
TKeys extends (keyof T)[],
|
||||
TRemainingKeys extends Exclude<keyof T, TKeys[number] | 'config'>[],
|
||||
Exhaustive extends TRemainingKeys,
|
||||
UNIQ extends UniqueArrayOfUnion<TRemainingKeys[number], Exhaustive>,
|
||||
>(
|
||||
command: Commands,
|
||||
options: T,
|
||||
whitelist: Exclude<TKeys, 'config'>,
|
||||
remainingKeys: UniqueArrayOfUnion<TRemainingKeys[number], Exhaustive>,
|
||||
): IsUnion<LastTupleElement<UNIQ>> extends false ? 'cli' | 'config' : TKeys => {
|
||||
const { config, ...rest } = options;
|
||||
|
||||
let atLeastOneParam = false;
|
||||
for (const key of Object.keys(rest)) {
|
||||
if (whitelist.includes(key)) continue;
|
||||
|
||||
atLeastOneParam = atLeastOneParam || rest[key] !== undefined;
|
||||
}
|
||||
|
||||
if (!config && atLeastOneParam) {
|
||||
return 'cli' as any;
|
||||
}
|
||||
|
||||
if (!atLeastOneParam) {
|
||||
return 'config' as any;
|
||||
}
|
||||
|
||||
// if config and cli - return error - write a reason
|
||||
console.log(outputs.common.ambiguousParams(command));
|
||||
process.exit(1);
|
||||
};
|
||||
|
||||
export const sqliteDriversLiterals = [
|
||||
literal('d1-http'),
|
||||
literal('expo'),
|
||||
literal('durable-sqlite'),
|
||||
] as const;
|
||||
|
||||
export const postgresqlDriversLiterals = [
|
||||
literal('aws-data-api'),
|
||||
literal('pglite'),
|
||||
] as const;
|
||||
|
||||
export const prefixes = [
|
||||
'index',
|
||||
'timestamp',
|
||||
'supabase',
|
||||
'unix',
|
||||
'none',
|
||||
] as const;
|
||||
export const prefix = enum_(prefixes);
|
||||
export type Prefix = (typeof prefixes)[number];
|
||||
|
||||
{
|
||||
const _: Prefix = '' as TypeOf<typeof prefix>;
|
||||
}
|
||||
|
||||
export const casingTypes = ['snake_case', 'camelCase'] as const;
|
||||
export const casingType = enum_(casingTypes);
|
||||
export type CasingType = (typeof casingTypes)[number];
|
||||
|
||||
export const sqliteDriver = union(sqliteDriversLiterals);
|
||||
export const postgresDriver = union(postgresqlDriversLiterals);
|
||||
export const driver = union([sqliteDriver, postgresDriver]);
|
||||
|
||||
export const configMigrations = object({
|
||||
table: string().optional(),
|
||||
schema: string().optional(),
|
||||
prefix: prefix.optional().default('index'),
|
||||
}).optional();
|
||||
|
||||
export const configCommonSchema = object({
|
||||
dialect: dialect,
|
||||
schema: union([string(), string().array()]).optional(),
|
||||
out: string().optional(),
|
||||
breakpoints: boolean().optional().default(true),
|
||||
verbose: boolean().optional().default(false),
|
||||
driver: driver.optional(),
|
||||
tablesFilter: union([string(), string().array()]).optional(),
|
||||
schemaFilter: union([string(), string().array()]).default(['public']),
|
||||
migrations: configMigrations,
|
||||
dbCredentials: any().optional(),
|
||||
casing: casingType.optional(),
|
||||
sql: boolean().default(true),
|
||||
}).passthrough();
|
||||
|
||||
export const casing = union([literal('camel'), literal('preserve')]).default(
|
||||
'camel',
|
||||
);
|
||||
|
||||
export const introspectParams = object({
|
||||
schema: union([string(), string().array()]).optional(),
|
||||
out: string().optional().default('./drizzle'),
|
||||
breakpoints: boolean().default(true),
|
||||
tablesFilter: union([string(), string().array()]).optional(),
|
||||
schemaFilter: union([string(), string().array()]).default(['public']),
|
||||
introspect: object({
|
||||
casing,
|
||||
}).default({ casing: 'camel' }),
|
||||
});
|
||||
|
||||
export type IntrospectParams = TypeOf<typeof introspectParams>;
|
||||
export type Casing = TypeOf<typeof casing>;
|
||||
|
||||
export const configIntrospectCliSchema = object({
|
||||
schema: union([string(), string().array()]).optional(),
|
||||
out: string().optional().default('./drizzle'),
|
||||
breakpoints: boolean().default(true),
|
||||
tablesFilter: union([string(), string().array()]).optional(),
|
||||
schemaFilter: union([string(), string().array()]).default(['public']),
|
||||
introspectCasing: union([literal('camel'), literal('preserve')]).default(
|
||||
'camel',
|
||||
),
|
||||
});
|
||||
|
||||
export const configGenerateSchema = object({
|
||||
schema: union([string(), string().array()]),
|
||||
out: string().optional().default('./drizzle'),
|
||||
breakpoints: boolean().default(true),
|
||||
});
|
||||
|
||||
export type GenerateSchema = TypeOf<typeof configGenerateSchema>;
|
||||
|
||||
export const configPushSchema = object({
|
||||
dialect: dialect,
|
||||
schema: union([string(), string().array()]),
|
||||
tablesFilter: union([string(), string().array()]).optional(),
|
||||
schemaFilter: union([string(), string().array()]).default(['public']),
|
||||
verbose: boolean().default(false),
|
||||
strict: boolean().default(false),
|
||||
out: string().optional(),
|
||||
});
|
||||
|
||||
export type CliConfig = TypeOf<typeof configCommonSchema>;
|
||||
export const drivers = ['d1-http', 'expo', 'aws-data-api', 'pglite', 'durable-sqlite'] as const;
|
||||
export type Driver = (typeof drivers)[number];
|
||||
const _: Driver = '' as TypeOf<typeof driver>;
|
||||
|
||||
export const wrapParam = (
|
||||
name: string,
|
||||
param: any | undefined,
|
||||
optional: boolean = false,
|
||||
type?: 'url' | 'secret',
|
||||
) => {
|
||||
const check = `[${chalk.green('✓')}]`;
|
||||
const cross = `[${chalk.red('x')}]`;
|
||||
if (typeof param === 'string') {
|
||||
if (param.length === 0) {
|
||||
return ` ${cross} ${name}: ''`;
|
||||
}
|
||||
if (type === 'secret') {
|
||||
return ` ${check} ${name}: '*****'`;
|
||||
} else if (type === 'url') {
|
||||
return ` ${check} ${name}: '${param.replace(/(?<=:\/\/[^:\n]*:)([^@]*)/, '****')}'`;
|
||||
}
|
||||
return ` ${check} ${name}: '${param}'`;
|
||||
}
|
||||
if (optional) {
|
||||
return chalk.gray(` ${name}?: `);
|
||||
}
|
||||
return ` ${cross} ${name}: ${chalk.gray('undefined')}`;
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import { coerce, literal, object, string, TypeOf, undefined as undefinedType, union } from 'zod';
|
||||
import { error } from '../views';
|
||||
import { wrapParam } from './common';
|
||||
|
||||
export const gelCredentials = union([
|
||||
object({
|
||||
driver: undefinedType(),
|
||||
host: string().min(1),
|
||||
port: coerce.number().min(1).optional(),
|
||||
user: string().min(1).optional(),
|
||||
password: string().min(1).optional(),
|
||||
database: string().min(1),
|
||||
tlsSecurity: union([
|
||||
literal('insecure'),
|
||||
literal('no_host_verification'),
|
||||
literal('strict'),
|
||||
literal('default'),
|
||||
]).optional(),
|
||||
}).transform((o) => {
|
||||
delete o.driver;
|
||||
return o as Omit<typeof o, 'driver'>;
|
||||
}),
|
||||
object({
|
||||
driver: undefinedType(),
|
||||
url: string().min(1),
|
||||
tlsSecurity: union([
|
||||
literal('insecure'),
|
||||
literal('no_host_verification'),
|
||||
literal('strict'),
|
||||
literal('default'),
|
||||
]).optional(),
|
||||
}).transform<{
|
||||
url: string;
|
||||
tlsSecurity?:
|
||||
| 'insecure'
|
||||
| 'no_host_verification'
|
||||
| 'strict'
|
||||
| 'default';
|
||||
}>((o) => {
|
||||
delete o.driver;
|
||||
return o;
|
||||
}),
|
||||
object({
|
||||
driver: undefinedType(),
|
||||
}).transform<undefined>((o) => {
|
||||
return undefined;
|
||||
}),
|
||||
]);
|
||||
|
||||
export type GelCredentials = TypeOf<typeof gelCredentials>;
|
||||
|
||||
export const printConfigConnectionIssues = (
|
||||
options: Record<string, unknown>,
|
||||
) => {
|
||||
if ('url' in options) {
|
||||
let text = `Please provide required params for Gel driver:\n`;
|
||||
console.log(error(text));
|
||||
console.log(wrapParam('url', options.url, false, 'url'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if ('host' in options || 'database' in options) {
|
||||
let text = `Please provide required params for Gel driver:\n`;
|
||||
console.log(error(text));
|
||||
console.log(wrapParam('host', options.host));
|
||||
console.log(wrapParam('port', options.port, true));
|
||||
console.log(wrapParam('user', options.user, true));
|
||||
console.log(wrapParam('password', options.password, true, 'secret'));
|
||||
console.log(wrapParam('database', options.database));
|
||||
console.log(wrapParam('tlsSecurity', options.tlsSecurity, true));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(
|
||||
error(
|
||||
`Either connection "url" or "host", "database" are required for Gel database connection`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { softAssertUnreachable } from 'src/global';
|
||||
import { object, string, TypeOf } from 'zod';
|
||||
import { error } from '../views';
|
||||
import { wrapParam } from './common';
|
||||
|
||||
export const libSQLCredentials = object({
|
||||
url: string().min(1),
|
||||
authToken: string().min(1).optional(),
|
||||
});
|
||||
|
||||
export type LibSQLCredentials = {
|
||||
url: string;
|
||||
authToken?: string;
|
||||
};
|
||||
|
||||
const _: LibSQLCredentials = {} as TypeOf<typeof libSQLCredentials>;
|
||||
|
||||
export const printConfigConnectionIssues = (
|
||||
options: Record<string, unknown>,
|
||||
command: 'generate' | 'migrate' | 'push' | 'pull' | 'studio',
|
||||
) => {
|
||||
let text = `Please provide required params for 'turso' dialect:\n`;
|
||||
console.log(error(text));
|
||||
console.log(wrapParam('url', options.url));
|
||||
console.log(wrapParam('authToken', options.authToken, true, 'secret'));
|
||||
process.exit(1);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import { boolean, coerce, object, string, TypeOf, union } from 'zod';
|
||||
import { error } from '../views';
|
||||
import { wrapParam } from './common';
|
||||
import { outputs } from './outputs';
|
||||
|
||||
export const mysqlCredentials = union([
|
||||
object({
|
||||
host: string().min(1),
|
||||
port: coerce.number().min(1).optional(),
|
||||
user: string().min(1).optional(),
|
||||
password: string().min(1).optional(),
|
||||
database: string().min(1),
|
||||
ssl: union([
|
||||
string(),
|
||||
object({
|
||||
pfx: string().optional(),
|
||||
key: string().optional(),
|
||||
passphrase: string().optional(),
|
||||
cert: string().optional(),
|
||||
ca: union([string(), string().array()]).optional(),
|
||||
crl: union([string(), string().array()]).optional(),
|
||||
ciphers: string().optional(),
|
||||
rejectUnauthorized: boolean().optional(),
|
||||
}),
|
||||
]).optional(),
|
||||
}),
|
||||
object({
|
||||
url: string().min(1),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type MysqlCredentials = TypeOf<typeof mysqlCredentials>;
|
||||
|
||||
export const printCliConnectionIssues = (options: any) => {
|
||||
const { uri, host, database } = options || {};
|
||||
|
||||
if (!uri && (!host || !database)) {
|
||||
console.log(outputs.mysql.connection.required());
|
||||
}
|
||||
};
|
||||
|
||||
export const printConfigConnectionIssues = (
|
||||
options: Record<string, unknown>,
|
||||
) => {
|
||||
if ('url' in options) {
|
||||
let text = `Please provide required params for MySQL driver:\n`;
|
||||
console.log(error(text));
|
||||
console.log(wrapParam('url', options.url, false, 'url'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let text = `Please provide required params for MySQL driver:\n`;
|
||||
console.log(error(text));
|
||||
console.log(wrapParam('host', options.host));
|
||||
console.log(wrapParam('port', options.port, true));
|
||||
console.log(wrapParam('user', options.user, true));
|
||||
console.log(wrapParam('password', options.password, true, 'secret'));
|
||||
console.log(wrapParam('database', options.database));
|
||||
console.log(wrapParam('ssl', options.ssl, true));
|
||||
process.exit(1);
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
import chalk from 'chalk';
|
||||
import { sqliteDriversLiterals } from './common';
|
||||
|
||||
export const withStyle = {
|
||||
error: (str: string) => `${chalk.red(`${chalk.white.bgRed(' Invalid input ')} ${str}`)}`,
|
||||
warning: (str: string) => `${chalk.white.bgGray(' Warning ')} ${str}`,
|
||||
errorWarning: (str: string) => `${chalk.red(`${chalk.white.bgRed(' Warning ')} ${str}`)}`,
|
||||
fullWarning: (str: string) => `${chalk.black.bgYellow(' Warning ')} ${chalk.bold(str)}`,
|
||||
suggestion: (str: string) => `${chalk.white.bgGray(' Suggestion ')} ${str}`,
|
||||
info: (str: string) => `${chalk.grey(str)}`,
|
||||
};
|
||||
|
||||
export const outputs = {
|
||||
studio: {
|
||||
drivers: (param: string) =>
|
||||
withStyle.error(
|
||||
`"${param}" is not a valid driver. Available drivers: "pg", "mysql2", "better-sqlite", "libsql", "turso". You can read more about drizzle.config: https://orm.drizzle.team/kit-docs/config-reference`,
|
||||
),
|
||||
noCredentials: () =>
|
||||
withStyle.error(
|
||||
`Please specify a 'dbCredentials' param in config. It will help drizzle to know how to query you database. You can read more about drizzle.config: https://orm.drizzle.team/kit-docs/config-reference`,
|
||||
),
|
||||
noDriver: () =>
|
||||
withStyle.error(
|
||||
`Please specify a 'driver' param in config. It will help drizzle to know how to query you database. You can read more about drizzle.config: https://orm.drizzle.team/kit-docs/config-reference`,
|
||||
),
|
||||
noDialect: () =>
|
||||
withStyle.error(
|
||||
`Please specify 'dialect' param in config, either of 'postgresql', 'mysql', 'sqlite', turso or singlestore`,
|
||||
),
|
||||
},
|
||||
common: {
|
||||
ambiguousParams: (command: string) =>
|
||||
withStyle.error(
|
||||
`You can't use both --config and other cli options for ${command} command`,
|
||||
),
|
||||
schema: (command: string) => withStyle.error(`"--schema" is a required field for ${command} command`),
|
||||
},
|
||||
postgres: {
|
||||
connection: {
|
||||
required: () =>
|
||||
withStyle.error(
|
||||
`Either "url" or "host", "database" are required for database connection`,
|
||||
),
|
||||
awsDataApi: () =>
|
||||
withStyle.error(
|
||||
"You need to provide 'database', 'secretArn' and 'resourceArn' for Drizzle Kit to connect to AWS Data API",
|
||||
),
|
||||
},
|
||||
},
|
||||
mysql: {
|
||||
connection: {
|
||||
driver: () => withStyle.error(`Only "mysql2" is available options for "--driver"`),
|
||||
required: () =>
|
||||
withStyle.error(
|
||||
`Either "url" or "host", "database" are required for database connection`,
|
||||
),
|
||||
},
|
||||
},
|
||||
sqlite: {
|
||||
connection: {
|
||||
driver: () => {
|
||||
const listOfDrivers = sqliteDriversLiterals
|
||||
.map((it) => `'${it.value}'`)
|
||||
.join(', ');
|
||||
return withStyle.error(
|
||||
`Either ${listOfDrivers} are available options for 'driver' param`,
|
||||
);
|
||||
},
|
||||
url: (driver: string) =>
|
||||
withStyle.error(
|
||||
`"url" is a required option for driver "${driver}". You can read more about drizzle.config: https://orm.drizzle.team/kit-docs/config-reference`,
|
||||
),
|
||||
authToken: (driver: string) =>
|
||||
withStyle.error(
|
||||
`"authToken" is a required option for driver "${driver}". You can read more about drizzle.config: https://orm.drizzle.team/kit-docs/config-reference`,
|
||||
),
|
||||
},
|
||||
introspect: {},
|
||||
push: {},
|
||||
},
|
||||
singlestore: {
|
||||
connection: {
|
||||
driver: () => withStyle.error(`Only "mysql2" is available options for "--driver"`),
|
||||
required: () =>
|
||||
withStyle.error(
|
||||
`Either "url" or "host", "database" are required for database connection`,
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
import { boolean, coerce, literal, object, string, TypeOf, undefined, union } from 'zod';
|
||||
import { error } from '../views';
|
||||
import { wrapParam } from './common';
|
||||
|
||||
export const postgresCredentials = union([
|
||||
object({
|
||||
driver: undefined(),
|
||||
host: string().min(1),
|
||||
port: coerce.number().min(1).optional(),
|
||||
user: string().min(1).optional(),
|
||||
password: string().min(1).optional(),
|
||||
database: string().min(1),
|
||||
ssl: union([
|
||||
literal('require'),
|
||||
literal('allow'),
|
||||
literal('prefer'),
|
||||
literal('verify-full'),
|
||||
boolean(),
|
||||
object({}).passthrough(),
|
||||
]).optional(),
|
||||
}).transform((o) => {
|
||||
delete o.driver;
|
||||
return o as Omit<typeof o, 'driver'>;
|
||||
}),
|
||||
object({
|
||||
driver: undefined(),
|
||||
url: string().min(1),
|
||||
}).transform<{ url: string }>((o) => {
|
||||
delete o.driver;
|
||||
return o;
|
||||
}),
|
||||
object({
|
||||
driver: literal('aws-data-api'),
|
||||
database: string().min(1),
|
||||
secretArn: string().min(1),
|
||||
resourceArn: string().min(1),
|
||||
}),
|
||||
object({
|
||||
driver: literal('pglite'),
|
||||
url: string().min(1),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type PostgresCredentials = TypeOf<typeof postgresCredentials>;
|
||||
|
||||
export const printConfigConnectionIssues = (
|
||||
options: Record<string, unknown>,
|
||||
) => {
|
||||
if (options.driver === 'aws-data-api') {
|
||||
let text = `Please provide required params for AWS Data API driver:\n`;
|
||||
console.log(error(text));
|
||||
console.log(wrapParam('database', options.database));
|
||||
console.log(wrapParam('secretArn', options.secretArn, false, 'secret'));
|
||||
console.log(wrapParam('resourceArn', options.resourceArn, false, 'secret'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if ('url' in options) {
|
||||
let text = `Please provide required params for Postgres driver:\n`;
|
||||
console.log(error(text));
|
||||
console.log(wrapParam('url', options.url, false, 'url'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if ('host' in options || 'database' in options) {
|
||||
let text = `Please provide required params for Postgres driver:\n`;
|
||||
console.log(error(text));
|
||||
console.log(wrapParam('host', options.host));
|
||||
console.log(wrapParam('port', options.port, true));
|
||||
console.log(wrapParam('user', options.user, true));
|
||||
console.log(wrapParam('password', options.password, true, 'secret'));
|
||||
console.log(wrapParam('database', options.database));
|
||||
console.log(wrapParam('ssl', options.ssl, true));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(
|
||||
error(
|
||||
`Either connection "url" or "host", "database" are required for PostgreSQL database connection`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import { boolean, coerce, object, string, TypeOf, union } from 'zod';
|
||||
import { error } from '../views';
|
||||
import { wrapParam } from './common';
|
||||
import { outputs } from './outputs';
|
||||
|
||||
export const singlestoreCredentials = union([
|
||||
object({
|
||||
host: string().min(1),
|
||||
port: coerce.number().min(1).optional(),
|
||||
user: string().min(1).optional(),
|
||||
password: string().min(1).optional(),
|
||||
database: string().min(1),
|
||||
ssl: union([
|
||||
string(),
|
||||
object({
|
||||
pfx: string().optional(),
|
||||
key: string().optional(),
|
||||
passphrase: string().optional(),
|
||||
cert: string().optional(),
|
||||
ca: union([string(), string().array()]).optional(),
|
||||
crl: union([string(), string().array()]).optional(),
|
||||
ciphers: string().optional(),
|
||||
rejectUnauthorized: boolean().optional(),
|
||||
}),
|
||||
]).optional(),
|
||||
}),
|
||||
object({
|
||||
url: string().min(1),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type SingleStoreCredentials = TypeOf<typeof singlestoreCredentials>;
|
||||
|
||||
export const printCliConnectionIssues = (options: any) => {
|
||||
const { uri, host, database } = options || {};
|
||||
|
||||
if (!uri && (!host || !database)) {
|
||||
console.log(outputs.singlestore.connection.required());
|
||||
}
|
||||
};
|
||||
|
||||
export const printConfigConnectionIssues = (
|
||||
options: Record<string, unknown>,
|
||||
) => {
|
||||
if ('url' in options) {
|
||||
let text = `Please provide required params for SingleStore driver:\n`;
|
||||
console.log(error(text));
|
||||
console.log(wrapParam('url', options.url, false, 'url'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let text = `Please provide required params for SingleStore driver:\n`;
|
||||
console.log(error(text));
|
||||
console.log(wrapParam('host', options.host));
|
||||
console.log(wrapParam('port', options.port, true));
|
||||
console.log(wrapParam('user', options.user, true));
|
||||
console.log(wrapParam('password', options.password, true, 'secret'));
|
||||
console.log(wrapParam('database', options.database));
|
||||
console.log(wrapParam('ssl', options.ssl, true));
|
||||
process.exit(1);
|
||||
};
|
||||
@@ -0,0 +1,104 @@
|
||||
import { softAssertUnreachable } from 'src/global';
|
||||
import { literal, object, string, TypeOf, undefined, union } from 'zod';
|
||||
import { error } from '../views';
|
||||
import { sqliteDriver, wrapParam } from './common';
|
||||
|
||||
export const sqliteCredentials = union([
|
||||
object({
|
||||
driver: literal('turso'),
|
||||
url: string().min(1),
|
||||
authToken: string().min(1).optional(),
|
||||
}),
|
||||
object({
|
||||
driver: literal('d1-http'),
|
||||
accountId: string().min(1),
|
||||
databaseId: string().min(1),
|
||||
token: string().min(1),
|
||||
}),
|
||||
object({
|
||||
driver: undefined(),
|
||||
url: string().min(1),
|
||||
}).transform<{ url: string }>((o) => {
|
||||
delete o.driver;
|
||||
return o;
|
||||
}),
|
||||
]);
|
||||
|
||||
export type SqliteCredentials =
|
||||
| {
|
||||
driver: 'd1-http';
|
||||
accountId: string;
|
||||
databaseId: string;
|
||||
token: string;
|
||||
}
|
||||
| {
|
||||
url: string;
|
||||
};
|
||||
|
||||
const _: SqliteCredentials = {} as TypeOf<typeof sqliteCredentials>;
|
||||
|
||||
export const printConfigConnectionIssues = (
|
||||
options: Record<string, unknown>,
|
||||
command: 'generate' | 'migrate' | 'push' | 'pull' | 'studio',
|
||||
) => {
|
||||
const parsedDriver = sqliteDriver.safeParse(options.driver);
|
||||
const driver = parsedDriver.success ? parsedDriver.data : ('' as never);
|
||||
|
||||
if (driver === 'expo') {
|
||||
if (command === 'migrate') {
|
||||
console.log(
|
||||
error(
|
||||
`You can't use 'migrate' command with Expo SQLite, please follow migration instructions in our docs - https://orm.drizzle.team/docs/get-started-sqlite#expo-sqlite`,
|
||||
),
|
||||
);
|
||||
} else if (command === 'studio') {
|
||||
console.log(
|
||||
error(
|
||||
`You can't use 'studio' command with Expo SQLite, please use Expo Plugin https://www.npmjs.com/package/expo-drizzle-studio-plugin`,
|
||||
),
|
||||
);
|
||||
} else if (command === 'pull') {
|
||||
console.log(error("You can't use 'pull' command with Expo SQLite"));
|
||||
} else if (command === 'push') {
|
||||
console.log(error("You can't use 'push' command with Expo SQLite"));
|
||||
} else {
|
||||
console.log(error('Unexpected error with expo driver 🤔'));
|
||||
}
|
||||
process.exit(1);
|
||||
} else if (driver === 'd1-http') {
|
||||
let text = `Please provide required params for D1 HTTP driver:\n`;
|
||||
console.log(error(text));
|
||||
console.log(wrapParam('accountId', options.accountId));
|
||||
console.log(wrapParam('databaseId', options.databaseId));
|
||||
console.log(wrapParam('token', options.token, false, 'secret'));
|
||||
process.exit(1);
|
||||
} else if (driver === 'durable-sqlite') {
|
||||
if (command === 'migrate') {
|
||||
console.log(
|
||||
error(
|
||||
`You can't use 'migrate' command with SQLite Durable Objects`,
|
||||
),
|
||||
);
|
||||
} else if (command === 'studio') {
|
||||
console.log(
|
||||
error(
|
||||
`You can't use 'studio' command with SQLite Durable Objects`,
|
||||
),
|
||||
);
|
||||
} else if (command === 'pull') {
|
||||
console.log(error("You can't use 'pull' command with SQLite Durable Objects"));
|
||||
} else if (command === 'push') {
|
||||
console.log(error("You can't use 'push' command with SQLite Durable Objects"));
|
||||
} else {
|
||||
console.log(error('Unexpected error with SQLite Durable Object driver 🤔'));
|
||||
}
|
||||
process.exit(1);
|
||||
} else {
|
||||
softAssertUnreachable(driver);
|
||||
}
|
||||
|
||||
let text = `Please provide required params:\n`;
|
||||
console.log(error(text));
|
||||
console.log(wrapParam('url', options.url));
|
||||
process.exit(1);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { coerce, intersection, object, string, TypeOf, union } from 'zod';
|
||||
import { dialect } from '../../schemaValidator';
|
||||
import { casingType } from './common';
|
||||
import { mysqlCredentials } from './mysql';
|
||||
import { postgresCredentials } from './postgres';
|
||||
import { sqliteCredentials } from './sqlite';
|
||||
|
||||
export const credentials = intersection(
|
||||
postgresCredentials,
|
||||
mysqlCredentials,
|
||||
sqliteCredentials,
|
||||
);
|
||||
|
||||
export type Credentials = TypeOf<typeof credentials>;
|
||||
|
||||
export const studioCliParams = object({
|
||||
port: coerce.number().optional().default(4983),
|
||||
host: string().optional().default('127.0.0.1'),
|
||||
config: string().optional(),
|
||||
});
|
||||
|
||||
export const studioConfig = object({
|
||||
dialect,
|
||||
schema: union([string(), string().array()]).optional(),
|
||||
casing: casingType.optional(),
|
||||
});
|
||||
@@ -0,0 +1,657 @@
|
||||
import chalk from 'chalk';
|
||||
import { Prompt, render, SelectState, TaskView } from 'hanji';
|
||||
import type { CommonSchema } from '../schemaValidator';
|
||||
import { objectValues } from '../utils';
|
||||
import type { Named, NamedWithSchema } from './commands/migrate';
|
||||
|
||||
export const warning = (msg: string) => {
|
||||
render(`[${chalk.yellow('Warning')}] ${msg}`);
|
||||
};
|
||||
export const err = (msg: string) => {
|
||||
render(`${chalk.bold.red('Error')} ${msg}`);
|
||||
};
|
||||
|
||||
export const info = (msg: string, greyMsg: string = ''): string => {
|
||||
return `${chalk.blue.bold('Info:')} ${msg} ${greyMsg ? chalk.grey(greyMsg) : ''}`.trim();
|
||||
};
|
||||
export const grey = (msg: string): string => {
|
||||
return chalk.grey(msg);
|
||||
};
|
||||
|
||||
export const error = (error: string, greyMsg: string = ''): string => {
|
||||
return `${chalk.bgRed.bold(' Error ')} ${error} ${greyMsg ? chalk.grey(greyMsg) : ''}`.trim();
|
||||
};
|
||||
|
||||
export const schema = (schema: CommonSchema): string => {
|
||||
type TableEntry = (typeof schema)['tables'][keyof (typeof schema)['tables']];
|
||||
const tables = Object.values(schema.tables) as unknown as TableEntry[];
|
||||
|
||||
let msg = chalk.bold(`${tables.length} tables\n`);
|
||||
|
||||
msg += tables
|
||||
.map((t) => {
|
||||
const columnsCount = Object.values(t.columns).length;
|
||||
const indexesCount = Object.values(t.indexes).length;
|
||||
let foreignKeys: number = 0;
|
||||
// Singlestore doesn't have foreign keys
|
||||
if (schema.dialect !== 'singlestore') {
|
||||
// @ts-expect-error
|
||||
foreignKeys = Object.values(t.foreignKeys).length;
|
||||
}
|
||||
|
||||
return `${chalk.bold.blue(t.name)} ${
|
||||
chalk.gray(
|
||||
`${columnsCount} columns ${indexesCount} indexes ${foreignKeys} fks`,
|
||||
)
|
||||
}`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
msg += '\n';
|
||||
|
||||
const enums = objectValues(
|
||||
'enums' in schema
|
||||
? 'values' in schema['enums']
|
||||
? schema['enums']
|
||||
: {}
|
||||
: {},
|
||||
);
|
||||
|
||||
if (enums.length > 0) {
|
||||
msg += '\n';
|
||||
msg += chalk.bold(`${enums.length} enums\n`);
|
||||
|
||||
msg += enums
|
||||
.map((it) => {
|
||||
return `${chalk.bold.blue(it.name)} ${
|
||||
chalk.gray(
|
||||
`[${Object.values(it.values).join(', ')}]`,
|
||||
)
|
||||
}`;
|
||||
})
|
||||
.join('\n');
|
||||
msg += '\n';
|
||||
}
|
||||
return msg;
|
||||
};
|
||||
|
||||
export interface RenamePropmtItem<T> {
|
||||
from: T;
|
||||
to: T;
|
||||
}
|
||||
|
||||
export const isRenamePromptItem = <T extends Named>(
|
||||
item: RenamePropmtItem<T> | T,
|
||||
): item is RenamePropmtItem<T> => {
|
||||
return 'from' in item && 'to' in item;
|
||||
};
|
||||
|
||||
export class ResolveColumnSelect<T extends Named> extends Prompt<
|
||||
RenamePropmtItem<T> | T
|
||||
> {
|
||||
private readonly data: SelectState<RenamePropmtItem<T> | T>;
|
||||
|
||||
constructor(
|
||||
private readonly tableName: string,
|
||||
private readonly base: Named,
|
||||
data: (RenamePropmtItem<T> | T)[],
|
||||
) {
|
||||
super();
|
||||
this.on('attach', (terminal) => terminal.toggleCursor('hide'));
|
||||
this.data = new SelectState(data);
|
||||
this.data.bind(this);
|
||||
}
|
||||
|
||||
render(status: 'idle' | 'submitted' | 'aborted'): string {
|
||||
if (status === 'submitted' || status === 'aborted') {
|
||||
return '\n';
|
||||
}
|
||||
|
||||
let text = `\nIs ${
|
||||
chalk.bold.blue(
|
||||
this.base.name,
|
||||
)
|
||||
} column in ${
|
||||
chalk.bold.blue(
|
||||
this.tableName,
|
||||
)
|
||||
} table created or renamed from another column?\n`;
|
||||
|
||||
const isSelectedRenamed = isRenamePromptItem(
|
||||
this.data.items[this.data.selectedIdx],
|
||||
);
|
||||
|
||||
const selectedPrefix = isSelectedRenamed
|
||||
? chalk.yellow('❯ ')
|
||||
: chalk.green('❯ ');
|
||||
|
||||
const labelLength: number = this.data.items
|
||||
.filter((it) => isRenamePromptItem(it))
|
||||
.map((it: RenamePropmtItem<T>) => {
|
||||
return this.base.name.length + 3 + it['from'].name.length;
|
||||
})
|
||||
.reduce((a, b) => {
|
||||
if (a > b) {
|
||||
return a;
|
||||
}
|
||||
return b;
|
||||
}, 0);
|
||||
|
||||
this.data.items.forEach((it, idx) => {
|
||||
const isSelected = idx === this.data.selectedIdx;
|
||||
const isRenamed = isRenamePromptItem(it);
|
||||
const title = isRenamed
|
||||
? `${it.from.name} › ${it.to.name}`.padEnd(labelLength, ' ')
|
||||
: it.name.padEnd(labelLength, ' ');
|
||||
const label = isRenamed
|
||||
? `${chalk.yellow('~')} ${title} ${chalk.gray('rename column')}`
|
||||
: `${chalk.green('+')} ${title} ${chalk.gray('create column')}`;
|
||||
|
||||
text += isSelected ? `${selectedPrefix}${label}` : ` ${label}`;
|
||||
text += idx !== this.data.items.length - 1 ? '\n' : '';
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
result(): RenamePropmtItem<T> | T {
|
||||
return this.data.items[this.data.selectedIdx]!;
|
||||
}
|
||||
}
|
||||
|
||||
export const tableKey = (it: NamedWithSchema) => {
|
||||
return it.schema === 'public' || !it.schema
|
||||
? it.name
|
||||
: `${it.schema}.${it.name}`;
|
||||
};
|
||||
|
||||
export class ResolveSelectNamed<T extends Named> extends Prompt<
|
||||
RenamePropmtItem<T> | T
|
||||
> {
|
||||
private readonly state: SelectState<RenamePropmtItem<T> | T>;
|
||||
|
||||
constructor(
|
||||
private readonly base: T,
|
||||
data: (RenamePropmtItem<T> | T)[],
|
||||
private readonly entityType: 'role' | 'policy',
|
||||
) {
|
||||
super();
|
||||
this.on('attach', (terminal) => terminal.toggleCursor('hide'));
|
||||
this.state = new SelectState(data);
|
||||
this.state.bind(this);
|
||||
}
|
||||
|
||||
render(status: 'idle' | 'submitted' | 'aborted'): string {
|
||||
if (status === 'submitted' || status === 'aborted') {
|
||||
return '';
|
||||
}
|
||||
const key = this.base.name;
|
||||
|
||||
let text = `\nIs ${chalk.bold.blue(key)} ${this.entityType} created or renamed from another ${this.entityType}?\n`;
|
||||
|
||||
const isSelectedRenamed = isRenamePromptItem(
|
||||
this.state.items[this.state.selectedIdx],
|
||||
);
|
||||
|
||||
const selectedPrefix = isSelectedRenamed
|
||||
? chalk.yellow('❯ ')
|
||||
: chalk.green('❯ ');
|
||||
|
||||
const labelLength: number = this.state.items
|
||||
.filter((it) => isRenamePromptItem(it))
|
||||
.map((_) => {
|
||||
const it = _ as RenamePropmtItem<T>;
|
||||
const keyFrom = it.from.name;
|
||||
return key.length + 3 + keyFrom.length;
|
||||
})
|
||||
.reduce((a, b) => {
|
||||
if (a > b) {
|
||||
return a;
|
||||
}
|
||||
return b;
|
||||
}, 0);
|
||||
|
||||
const entityType = this.entityType;
|
||||
this.state.items.forEach((it, idx) => {
|
||||
const isSelected = idx === this.state.selectedIdx;
|
||||
const isRenamed = isRenamePromptItem(it);
|
||||
|
||||
const title = isRenamed
|
||||
? `${it.from.name} › ${it.to.name}`.padEnd(labelLength, ' ')
|
||||
: it.name.padEnd(labelLength, ' ');
|
||||
|
||||
const label = isRenamed
|
||||
? `${chalk.yellow('~')} ${title} ${chalk.gray(`rename ${entityType}`)}`
|
||||
: `${chalk.green('+')} ${title} ${chalk.gray(`create ${entityType}`)}`;
|
||||
|
||||
text += isSelected ? `${selectedPrefix}${label}` : ` ${label}`;
|
||||
text += idx !== this.state.items.length - 1 ? '\n' : '';
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
result(): RenamePropmtItem<T> | T {
|
||||
return this.state.items[this.state.selectedIdx]!;
|
||||
}
|
||||
}
|
||||
|
||||
export class ResolveSelect<T extends NamedWithSchema> extends Prompt<
|
||||
RenamePropmtItem<T> | T
|
||||
> {
|
||||
private readonly state: SelectState<RenamePropmtItem<T> | T>;
|
||||
|
||||
constructor(
|
||||
private readonly base: T,
|
||||
data: (RenamePropmtItem<T> | T)[],
|
||||
private readonly entityType: 'table' | 'enum' | 'sequence' | 'view' | 'role',
|
||||
) {
|
||||
super();
|
||||
this.on('attach', (terminal) => terminal.toggleCursor('hide'));
|
||||
this.state = new SelectState(data);
|
||||
this.state.bind(this);
|
||||
this.base = base;
|
||||
}
|
||||
|
||||
render(status: 'idle' | 'submitted' | 'aborted'): string {
|
||||
if (status === 'submitted' || status === 'aborted') {
|
||||
return '';
|
||||
}
|
||||
const key = tableKey(this.base);
|
||||
|
||||
let text = `\nIs ${chalk.bold.blue(key)} ${this.entityType} created or renamed from another ${this.entityType}?\n`;
|
||||
|
||||
const isSelectedRenamed = isRenamePromptItem(
|
||||
this.state.items[this.state.selectedIdx],
|
||||
);
|
||||
|
||||
const selectedPrefix = isSelectedRenamed
|
||||
? chalk.yellow('❯ ')
|
||||
: chalk.green('❯ ');
|
||||
|
||||
const labelLength: number = this.state.items
|
||||
.filter((it) => isRenamePromptItem(it))
|
||||
.map((_) => {
|
||||
const it = _ as RenamePropmtItem<T>;
|
||||
const keyFrom = tableKey(it.from);
|
||||
return key.length + 3 + keyFrom.length;
|
||||
})
|
||||
.reduce((a, b) => {
|
||||
if (a > b) {
|
||||
return a;
|
||||
}
|
||||
return b;
|
||||
}, 0);
|
||||
|
||||
const entityType = this.entityType;
|
||||
this.state.items.forEach((it, idx) => {
|
||||
const isSelected = idx === this.state.selectedIdx;
|
||||
const isRenamed = isRenamePromptItem(it);
|
||||
|
||||
const title = isRenamed
|
||||
? `${tableKey(it.from)} › ${tableKey(it.to)}`.padEnd(labelLength, ' ')
|
||||
: tableKey(it).padEnd(labelLength, ' ');
|
||||
|
||||
const label = isRenamed
|
||||
? `${chalk.yellow('~')} ${title} ${chalk.gray(`rename ${entityType}`)}`
|
||||
: `${chalk.green('+')} ${title} ${chalk.gray(`create ${entityType}`)}`;
|
||||
|
||||
text += isSelected ? `${selectedPrefix}${label}` : ` ${label}`;
|
||||
text += idx !== this.state.items.length - 1 ? '\n' : '';
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
result(): RenamePropmtItem<T> | T {
|
||||
return this.state.items[this.state.selectedIdx]!;
|
||||
}
|
||||
}
|
||||
|
||||
export class ResolveSchemasSelect<T extends Named> extends Prompt<
|
||||
RenamePropmtItem<T> | T
|
||||
> {
|
||||
private readonly state: SelectState<RenamePropmtItem<T> | T>;
|
||||
|
||||
constructor(private readonly base: Named, data: (RenamePropmtItem<T> | T)[]) {
|
||||
super();
|
||||
this.on('attach', (terminal) => terminal.toggleCursor('hide'));
|
||||
this.state = new SelectState(data);
|
||||
this.state.bind(this);
|
||||
this.base = base;
|
||||
}
|
||||
|
||||
render(status: 'idle' | 'submitted' | 'aborted'): string {
|
||||
if (status === 'submitted' || status === 'aborted') {
|
||||
return '';
|
||||
}
|
||||
|
||||
let text = `\nIs ${
|
||||
chalk.bold.blue(
|
||||
this.base.name,
|
||||
)
|
||||
} schema created or renamed from another schema?\n`;
|
||||
const isSelectedRenamed = isRenamePromptItem(
|
||||
this.state.items[this.state.selectedIdx],
|
||||
);
|
||||
const selectedPrefix = isSelectedRenamed
|
||||
? chalk.yellow('❯ ')
|
||||
: chalk.green('❯ ');
|
||||
|
||||
const labelLength: number = this.state.items
|
||||
.filter((it) => isRenamePromptItem(it))
|
||||
.map((it: RenamePropmtItem<T>) => {
|
||||
return this.base.name.length + 3 + it['from'].name.length;
|
||||
})
|
||||
.reduce((a, b) => {
|
||||
if (a > b) {
|
||||
return a;
|
||||
}
|
||||
return b;
|
||||
}, 0);
|
||||
|
||||
this.state.items.forEach((it, idx) => {
|
||||
const isSelected = idx === this.state.selectedIdx;
|
||||
const isRenamed = isRenamePromptItem(it);
|
||||
const title = isRenamed
|
||||
? `${it.from.name} › ${it.to.name}`.padEnd(labelLength, ' ')
|
||||
: it.name.padEnd(labelLength, ' ');
|
||||
const label = isRenamed
|
||||
? `${chalk.yellow('~')} ${title} ${chalk.gray('rename schema')}`
|
||||
: `${chalk.green('+')} ${title} ${chalk.gray('create schema')}`;
|
||||
|
||||
text += isSelected ? `${selectedPrefix}${label}` : ` ${label}`;
|
||||
text += idx !== this.state.items.length - 1 ? '\n' : '';
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
result(): RenamePropmtItem<T> | T {
|
||||
return this.state.items[this.state.selectedIdx]!;
|
||||
}
|
||||
}
|
||||
|
||||
class Spinner {
|
||||
private offset: number = 0;
|
||||
private readonly iterator: () => void;
|
||||
|
||||
constructor(private readonly frames: string[]) {
|
||||
this.iterator = () => {
|
||||
this.offset += 1;
|
||||
this.offset %= frames.length - 1;
|
||||
};
|
||||
}
|
||||
|
||||
public tick = () => {
|
||||
this.iterator();
|
||||
};
|
||||
|
||||
public value = () => {
|
||||
return this.frames[this.offset];
|
||||
};
|
||||
}
|
||||
|
||||
const _frames = function(values: string[]): () => string {
|
||||
let index = 0;
|
||||
const iterator = () => {
|
||||
const frame = values[index];
|
||||
index += 1;
|
||||
index %= values.length;
|
||||
return frame!;
|
||||
};
|
||||
return iterator;
|
||||
};
|
||||
|
||||
type ValueOf<T> = T[keyof T];
|
||||
export type IntrospectStatus = 'fetching' | 'done';
|
||||
export type IntrospectStage =
|
||||
| 'tables'
|
||||
| 'columns'
|
||||
| 'enums'
|
||||
| 'indexes'
|
||||
| 'policies'
|
||||
| 'checks'
|
||||
| 'fks'
|
||||
| 'views';
|
||||
|
||||
type IntrospectState = {
|
||||
[key in IntrospectStage]: {
|
||||
count: number;
|
||||
name: string;
|
||||
status: IntrospectStatus;
|
||||
};
|
||||
};
|
||||
|
||||
export class IntrospectProgress extends TaskView {
|
||||
private readonly spinner: Spinner = new Spinner('⣷⣯⣟⡿⢿⣻⣽⣾'.split(''));
|
||||
private timeout: NodeJS.Timeout | undefined;
|
||||
|
||||
private state: IntrospectState = {
|
||||
tables: {
|
||||
count: 0,
|
||||
name: 'tables',
|
||||
status: 'fetching',
|
||||
},
|
||||
columns: {
|
||||
count: 0,
|
||||
name: 'columns',
|
||||
status: 'fetching',
|
||||
},
|
||||
enums: {
|
||||
count: 0,
|
||||
name: 'enums',
|
||||
status: 'fetching',
|
||||
},
|
||||
indexes: {
|
||||
count: 0,
|
||||
name: 'indexes',
|
||||
status: 'fetching',
|
||||
},
|
||||
fks: {
|
||||
count: 0,
|
||||
name: 'foreign keys',
|
||||
status: 'fetching',
|
||||
},
|
||||
policies: {
|
||||
count: 0,
|
||||
name: 'policies',
|
||||
status: 'fetching',
|
||||
},
|
||||
checks: {
|
||||
count: 0,
|
||||
name: 'check constraints',
|
||||
status: 'fetching',
|
||||
},
|
||||
views: {
|
||||
count: 0,
|
||||
name: 'views',
|
||||
status: 'fetching',
|
||||
},
|
||||
};
|
||||
|
||||
constructor(private readonly hasEnums: boolean = false) {
|
||||
super();
|
||||
this.timeout = setInterval(() => {
|
||||
this.spinner.tick();
|
||||
this.requestLayout();
|
||||
}, 128);
|
||||
|
||||
this.on('detach', () => clearInterval(this.timeout));
|
||||
}
|
||||
|
||||
public update(
|
||||
stage: IntrospectStage,
|
||||
count: number,
|
||||
status: IntrospectStatus,
|
||||
) {
|
||||
this.state[stage].count = count;
|
||||
this.state[stage].status = status;
|
||||
this.requestLayout();
|
||||
}
|
||||
|
||||
private formatCount = (count: number) => {
|
||||
const width: number = Math.max.apply(
|
||||
null,
|
||||
Object.values(this.state).map((it) => it.count.toFixed(0).length),
|
||||
);
|
||||
|
||||
return count.toFixed(0).padEnd(width, ' ');
|
||||
};
|
||||
|
||||
private statusText = (spinner: string, stage: ValueOf<IntrospectState>) => {
|
||||
const { name, count } = stage;
|
||||
const isDone = stage.status === 'done';
|
||||
|
||||
const prefix = isDone ? `[${chalk.green('✓')}]` : `[${spinner}]`;
|
||||
|
||||
const formattedCount = this.formatCount(count);
|
||||
const suffix = isDone
|
||||
? `${formattedCount} ${name} fetched`
|
||||
: `${formattedCount} ${name} fetching`;
|
||||
|
||||
return `${prefix} ${suffix}\n`;
|
||||
};
|
||||
|
||||
render(): string {
|
||||
let info = '';
|
||||
const spin = this.spinner.value();
|
||||
info += this.statusText(spin, this.state.tables);
|
||||
info += this.statusText(spin, this.state.columns);
|
||||
info += this.hasEnums ? this.statusText(spin, this.state.enums) : '';
|
||||
info += this.statusText(spin, this.state.indexes);
|
||||
info += this.statusText(spin, this.state.fks);
|
||||
info += this.statusText(spin, this.state.policies);
|
||||
info += this.statusText(spin, this.state.checks);
|
||||
info += this.statusText(spin, this.state.views);
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
export class MigrateProgress extends TaskView {
|
||||
private readonly spinner: Spinner = new Spinner('⣷⣯⣟⡿⢿⣻⣽⣾'.split(''));
|
||||
private timeout: NodeJS.Timeout | undefined;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.timeout = setInterval(() => {
|
||||
this.spinner.tick();
|
||||
this.requestLayout();
|
||||
}, 128);
|
||||
|
||||
this.on('detach', () => clearInterval(this.timeout));
|
||||
}
|
||||
|
||||
render(status: 'pending' | 'done' | 'rejected'): string {
|
||||
if (status === 'pending' || status === 'rejected') {
|
||||
const spin = this.spinner.value();
|
||||
return `[${spin}] applying migrations...`;
|
||||
}
|
||||
return `[${chalk.green('✓')}] migrations applied successfully!`;
|
||||
}
|
||||
}
|
||||
|
||||
export class ProgressView extends TaskView {
|
||||
private readonly spinner: Spinner = new Spinner('⣷⣯⣟⡿⢿⣻⣽⣾'.split(''));
|
||||
private timeout: NodeJS.Timeout | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly progressText: string,
|
||||
private readonly successText: string,
|
||||
) {
|
||||
super();
|
||||
this.timeout = setInterval(() => {
|
||||
this.spinner.tick();
|
||||
this.requestLayout();
|
||||
}, 128);
|
||||
|
||||
this.on('detach', () => clearInterval(this.timeout));
|
||||
}
|
||||
|
||||
render(status: 'pending' | 'done' | 'rejected'): string {
|
||||
if (status === 'pending' || status === 'rejected') {
|
||||
const spin = this.spinner.value();
|
||||
return `[${spin}] ${this.progressText}\n`;
|
||||
}
|
||||
return `[${chalk.green('✓')}] ${this.successText}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
export class DropMigrationView<T extends { tag: string }> extends Prompt<T> {
|
||||
private readonly data: SelectState<T>;
|
||||
|
||||
constructor(data: T[]) {
|
||||
super();
|
||||
this.on('attach', (terminal) => terminal.toggleCursor('hide'));
|
||||
this.data = new SelectState(data);
|
||||
this.data.selectedIdx = data.length - 1;
|
||||
this.data.bind(this);
|
||||
}
|
||||
|
||||
render(status: 'idle' | 'submitted' | 'aborted'): string {
|
||||
if (status === 'submitted' || status === 'aborted') {
|
||||
return '\n';
|
||||
}
|
||||
|
||||
let text = chalk.bold('Please select migration to drop:\n');
|
||||
const selectedPrefix = chalk.yellow('❯ ');
|
||||
|
||||
const data = trimmedRange(this.data.items, this.data.selectedIdx, 9);
|
||||
const labelLength: number = data.trimmed
|
||||
.map((it) => it.tag.length)
|
||||
.reduce((a, b) => {
|
||||
if (a > b) {
|
||||
return a;
|
||||
}
|
||||
return b;
|
||||
}, 0);
|
||||
|
||||
text += data.startTrimmed ? ' ...\n' : '';
|
||||
|
||||
data.trimmed.forEach((it, idx) => {
|
||||
const isSelected = idx === this.data.selectedIdx - data.offset;
|
||||
let title = it.tag.padEnd(labelLength, ' ');
|
||||
title = isSelected ? chalk.yellow(title) : title;
|
||||
|
||||
text += isSelected ? `${selectedPrefix}${title}` : ` ${title}`;
|
||||
text += idx !== this.data.items.length - 1 ? '\n' : '';
|
||||
});
|
||||
|
||||
text += data.endTrimmed ? ' ...\n' : '';
|
||||
return text;
|
||||
}
|
||||
|
||||
result(): T {
|
||||
return this.data.items[this.data.selectedIdx]!;
|
||||
}
|
||||
}
|
||||
|
||||
export const trimmedRange = <T>(
|
||||
arr: T[],
|
||||
index: number,
|
||||
limitLines: number,
|
||||
): {
|
||||
trimmed: T[];
|
||||
offset: number;
|
||||
startTrimmed: boolean;
|
||||
endTrimmed: boolean;
|
||||
} => {
|
||||
const limit = limitLines - 2;
|
||||
const sideLimit = Math.round(limit / 2);
|
||||
|
||||
const endTrimmed = arr.length - sideLimit > index;
|
||||
const startTrimmed = index > sideLimit - 1;
|
||||
|
||||
const paddingStart = Math.max(index + sideLimit - arr.length, 0);
|
||||
const paddingEnd = Math.min(index - sideLimit + 1, 0);
|
||||
|
||||
const d1 = endTrimmed ? 1 : 0;
|
||||
const d2 = startTrimmed ? 0 : 1;
|
||||
|
||||
const start = Math.max(0, index - sideLimit + d1 - paddingStart);
|
||||
const end = Math.min(arr.length, index + sideLimit + d2 - paddingEnd);
|
||||
|
||||
return {
|
||||
trimmed: arr.slice(start, end),
|
||||
offset: start,
|
||||
startTrimmed,
|
||||
endTrimmed,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Config } from '../index';
|
||||
|
||||
export const getTablesFilterByExtensions = ({
|
||||
extensionsFilters,
|
||||
dialect,
|
||||
}: Pick<Config, 'extensionsFilters' | 'dialect'>): string[] => {
|
||||
if (extensionsFilters) {
|
||||
if (
|
||||
extensionsFilters.includes('postgis')
|
||||
&& dialect === 'postgresql'
|
||||
) {
|
||||
return ['!geography_columns', '!geometry_columns', '!spatial_ref_sys'];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
export const vectorOps = [
|
||||
'vector_l2_ops',
|
||||
'vector_ip_ops',
|
||||
'vector_cosine_ops',
|
||||
'vector_l1_ops',
|
||||
'bit_hamming_ops',
|
||||
'bit_jaccard_ops',
|
||||
'halfvec_l2_ops',
|
||||
'sparsevec_l2_ops',
|
||||
];
|
||||
@@ -0,0 +1,61 @@
|
||||
export const originUUID = '00000000-0000-0000-0000-000000000000';
|
||||
export const snapshotVersion = '7';
|
||||
|
||||
export function assertUnreachable(x: never | undefined): never {
|
||||
throw new Error("Didn't expect to get here");
|
||||
}
|
||||
|
||||
// don't fail in runtime, types only
|
||||
export function softAssertUnreachable(x: never) {
|
||||
return null as never;
|
||||
}
|
||||
|
||||
export const mapValues = <IN, OUT>(
|
||||
obj: Record<string, IN>,
|
||||
map: (input: IN) => OUT,
|
||||
): Record<string, OUT> => {
|
||||
const result = Object.keys(obj).reduce(function(result, key) {
|
||||
result[key] = map(obj[key]);
|
||||
return result;
|
||||
}, {} as Record<string, OUT>);
|
||||
return result;
|
||||
};
|
||||
|
||||
export const mapKeys = <T>(
|
||||
obj: Record<string, T>,
|
||||
map: (key: string, value: T) => string,
|
||||
): Record<string, T> => {
|
||||
const result = Object.fromEntries(
|
||||
Object.entries(obj).map(([key, val]) => {
|
||||
const newKey = map(key, val);
|
||||
return [newKey, val];
|
||||
}),
|
||||
);
|
||||
return result;
|
||||
};
|
||||
|
||||
export const mapEntries = <T>(
|
||||
obj: Record<string, T>,
|
||||
map: (key: string, value: T) => [string, T],
|
||||
): Record<string, T> => {
|
||||
const result = Object.fromEntries(
|
||||
Object.entries(obj).map(([key, val]) => {
|
||||
const [newKey, newVal] = map(key, val);
|
||||
return [newKey, newVal];
|
||||
}),
|
||||
);
|
||||
return result;
|
||||
};
|
||||
|
||||
export const customMapEntries = <TReturn, T = any>(
|
||||
obj: Record<string, T>,
|
||||
map: (key: string, value: T) => [string, TReturn],
|
||||
): Record<string, TReturn> => {
|
||||
const result = Object.fromEntries(
|
||||
Object.entries(obj).map(([key, val]) => {
|
||||
const [newKey, newVal] = map(key, val);
|
||||
return [newKey, newVal];
|
||||
}),
|
||||
);
|
||||
return result;
|
||||
};
|
||||
@@ -0,0 +1,349 @@
|
||||
import { ConnectionOptions } from 'tls';
|
||||
import type { Driver, Prefix } from './cli/validations/common';
|
||||
import type { Dialect } from './schemaValidator';
|
||||
|
||||
// import {SslOptions} from 'mysql2'
|
||||
type SslOptions = {
|
||||
pfx?: string;
|
||||
key?: string;
|
||||
passphrase?: string;
|
||||
cert?: string;
|
||||
ca?: string | string[];
|
||||
crl?: string | string[];
|
||||
ciphers?: string;
|
||||
rejectUnauthorized?: boolean;
|
||||
};
|
||||
|
||||
type Verify<T, U extends T> = U;
|
||||
|
||||
/**
|
||||
* **You are currently using version 0.21.0+ of drizzle-kit. If you have just upgraded to this version, please make sure to read the changelog to understand what changes have been made and what
|
||||
* adjustments may be necessary for you. See https://orm.drizzle.team/kit-docs/upgrade-21#how-to-migrate-to-0210**
|
||||
*
|
||||
* **Config** usage:
|
||||
*
|
||||
* `dialect` - mandatory and is responsible for explicitly providing a databse dialect you are using for all the commands
|
||||
* *Possible values*: `postgresql`, `mysql`, `sqlite`, `singlestore
|
||||
*
|
||||
* See https://orm.drizzle.team/kit-docs/config-reference#dialect
|
||||
*
|
||||
* ---
|
||||
* `schema` - param lets you define where your schema file/files live.
|
||||
* You can have as many separate schema files as you want and define paths to them using glob or array of globs syntax.
|
||||
*
|
||||
* See https://orm.drizzle.team/kit-docs/config-reference#schema
|
||||
*
|
||||
* ---
|
||||
* `out` - allows you to define the folder for your migrations and a folder, where drizzle will introspect the schema and relations
|
||||
*
|
||||
* See https://orm.drizzle.team/kit-docs/config-reference#out
|
||||
*
|
||||
* ---
|
||||
* `driver` - optional param that is responsible for explicitly providing a driver to use when accessing a database
|
||||
* *Possible values*: `aws-data-api`, `d1-http`, `expo`, `turso`, `pglite`
|
||||
* If you don't use AWS Data API, D1, Turso or Expo - ypu don't need this driver. You can check a driver strategy choice here: https://orm.drizzle.team/kit-docs/upgrade-21
|
||||
*
|
||||
* See https://orm.drizzle.team/kit-docs/config-reference#driver
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* `dbCredentials` - an object to define your connection to the database. For more info please check the docs
|
||||
*
|
||||
* See https://orm.drizzle.team/kit-docs/config-reference#dbcredentials
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* `migrations` - param let’s use specify custom table and schema(PostgreSQL only) for migrations.
|
||||
* By default, all information about executed migrations will be stored in the database inside
|
||||
* the `__drizzle_migrations` table, and for PostgreSQL, inside the drizzle schema.
|
||||
* However, you can configure where to store those records.
|
||||
*
|
||||
* See https://orm.drizzle.team/kit-docs/config-reference#migrations
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* `breakpoints` - param lets you enable/disable SQL statement breakpoints in generated migrations.
|
||||
* It’s optional and true by default, it’s necessary to properly apply migrations on databases,
|
||||
* that do not support multiple DDL alternation statements in one transaction(MySQL, SQLite, SingleStore) and
|
||||
* Drizzle ORM has to apply them sequentially one by one.
|
||||
*
|
||||
* See https://orm.drizzle.team/kit-docs/config-reference#breakpoints
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* `tablesFilters` - param lets you filter tables with glob syntax for db push command.
|
||||
* It’s useful when you have only one database avaialable for several separate projects with separate sql schemas.
|
||||
*
|
||||
* How to define multi-project tables with Drizzle ORM — see https://orm.drizzle.team/docs/goodies#multi-project-schema
|
||||
*
|
||||
* See https://orm.drizzle.team/kit-docs/config-reference#tablesfilters
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* `schemaFilter` - parameter allows you to define which schema in PostgreSQL should be used for either introspect or push commands.
|
||||
* This parameter accepts a single schema as a string or an array of schemas as strings.
|
||||
* No glob pattern is supported here. By default, drizzle will use the public schema for both commands,
|
||||
* but you can add any schema you need.
|
||||
*
|
||||
* For example, having schemaFilter: ["my_schema"] will only look for tables in both the database and
|
||||
* drizzle schema that are a part of the my_schema schema.
|
||||
*
|
||||
* See https://orm.drizzle.team/kit-docs/config-reference#schemafilter
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* `verbose` - command is used for drizzle-kit push commands and prints all statements that will be executed.
|
||||
*
|
||||
* > Note: This command will only print the statements that should be executed.
|
||||
* To approve them before applying, please refer to the `strict` command.
|
||||
*
|
||||
* See https://orm.drizzle.team/kit-docs/config-reference#verbose
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* `strict` - command is used for drizzle-kit push commands and will always ask for your confirmation,
|
||||
* either to execute all statements needed to sync your schema with the database or not.
|
||||
*
|
||||
* See https://orm.drizzle.team/kit-docs/config-reference#strict
|
||||
*/
|
||||
export type Config =
|
||||
& {
|
||||
dialect: Dialect;
|
||||
out?: string;
|
||||
breakpoints?: boolean;
|
||||
tablesFilter?: string | string[];
|
||||
extensionsFilters?: 'postgis'[];
|
||||
schemaFilter?: string | string[];
|
||||
schema?: string | string[];
|
||||
verbose?: boolean;
|
||||
strict?: boolean;
|
||||
casing?: 'camelCase' | 'snake_case';
|
||||
migrations?: {
|
||||
table?: string;
|
||||
schema?: string;
|
||||
prefix?: Prefix;
|
||||
};
|
||||
introspect?: {
|
||||
casing: 'camel' | 'preserve';
|
||||
};
|
||||
entities?: {
|
||||
roles?: boolean | { provider?: 'supabase' | 'neon' | string & {}; exclude?: string[]; include?: string[] };
|
||||
};
|
||||
}
|
||||
& (
|
||||
| {
|
||||
dialect: Verify<Dialect, 'turso'>;
|
||||
dbCredentials: {
|
||||
url: string;
|
||||
authToken?: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
dialect: Verify<Dialect, 'sqlite'>;
|
||||
dbCredentials: {
|
||||
url: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
dialect: Verify<Dialect, 'postgresql'>;
|
||||
dbCredentials:
|
||||
| ({
|
||||
host: string;
|
||||
port?: number;
|
||||
user?: string;
|
||||
password?: string;
|
||||
database: string;
|
||||
ssl?:
|
||||
| boolean
|
||||
| 'require'
|
||||
| 'allow'
|
||||
| 'prefer'
|
||||
| 'verify-full'
|
||||
| ConnectionOptions;
|
||||
} & {})
|
||||
| {
|
||||
url: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
dialect: Verify<Dialect, 'postgresql'>;
|
||||
driver: Verify<Driver, 'aws-data-api'>;
|
||||
dbCredentials: {
|
||||
database: string;
|
||||
secretArn: string;
|
||||
resourceArn: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
dialect: Verify<Dialect, 'postgresql'>;
|
||||
driver: Verify<Driver, 'pglite'>;
|
||||
dbCredentials: {
|
||||
url: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
dialect: Verify<Dialect, 'mysql'>;
|
||||
dbCredentials:
|
||||
| {
|
||||
host: string;
|
||||
port?: number;
|
||||
user?: string;
|
||||
password?: string;
|
||||
database: string;
|
||||
ssl?: string | SslOptions;
|
||||
}
|
||||
| {
|
||||
url: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
dialect: Verify<Dialect, 'sqlite'>;
|
||||
driver: Verify<Driver, 'd1-http'>;
|
||||
dbCredentials: {
|
||||
accountId: string;
|
||||
databaseId: string;
|
||||
token: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
dialect: Verify<Dialect, 'sqlite'>;
|
||||
driver: Verify<Driver, 'expo'>;
|
||||
}
|
||||
| {
|
||||
dialect: Verify<Dialect, 'sqlite'>;
|
||||
driver: Verify<Driver, 'durable-sqlite'>;
|
||||
}
|
||||
| {}
|
||||
| {
|
||||
dialect: Verify<Dialect, 'singlestore'>;
|
||||
dbCredentials:
|
||||
| {
|
||||
host: string;
|
||||
port?: number;
|
||||
user?: string;
|
||||
password?: string;
|
||||
database: string;
|
||||
ssl?: string | SslOptions;
|
||||
}
|
||||
| {
|
||||
url: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
dialect: Verify<Dialect, 'gel'>;
|
||||
dbCredentials?:
|
||||
& {
|
||||
tlsSecurity?:
|
||||
| 'insecure'
|
||||
| 'no_host_verification'
|
||||
| 'strict'
|
||||
| 'default';
|
||||
}
|
||||
& (
|
||||
| {
|
||||
url: string;
|
||||
}
|
||||
| ({
|
||||
host: string;
|
||||
port?: number;
|
||||
user?: string;
|
||||
password?: string;
|
||||
database: string;
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* **You are currently using version 0.21.0+ of drizzle-kit. If you have just upgraded to this version, please make sure to read the changelog to understand what changes have been made and what
|
||||
* adjustments may be necessary for you. See https://orm.drizzle.team/kit-docs/upgrade-21#how-to-migrate-to-0210**
|
||||
*
|
||||
* **Config** usage:
|
||||
*
|
||||
* `dialect` - mandatory and is responsible for explicitly providing a databse dialect you are using for all the commands
|
||||
* *Possible values*: `postgresql`, `mysql`, `sqlite`, `singlestore`, `gel`
|
||||
*
|
||||
* See https://orm.drizzle.team/kit-docs/config-reference#dialect
|
||||
*
|
||||
* ---
|
||||
* `schema` - param lets you define where your schema file/files live.
|
||||
* You can have as many separate schema files as you want and define paths to them using glob or array of globs syntax.
|
||||
*
|
||||
* See https://orm.drizzle.team/kit-docs/config-reference#schema
|
||||
*
|
||||
* ---
|
||||
* `out` - allows you to define the folder for your migrations and a folder, where drizzle will introspect the schema and relations
|
||||
*
|
||||
* See https://orm.drizzle.team/kit-docs/config-reference#out
|
||||
*
|
||||
* ---
|
||||
* `driver` - optional param that is responsible for explicitly providing a driver to use when accessing a database
|
||||
* *Possible values*: `aws-data-api`, `d1-http`, `expo`, `turso`, `pglite`
|
||||
* If you don't use AWS Data API, D1, Turso or Expo - ypu don't need this driver. You can check a driver strategy choice here: https://orm.drizzle.team/kit-docs/upgrade-21
|
||||
*
|
||||
* See https://orm.drizzle.team/kit-docs/config-reference#driver
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* `dbCredentials` - an object to define your connection to the database. For more info please check the docs
|
||||
*
|
||||
* See https://orm.drizzle.team/kit-docs/config-reference#dbcredentials
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* `migrations` - param let’s use specify custom table and schema(PostgreSQL only) for migrations.
|
||||
* By default, all information about executed migrations will be stored in the database inside
|
||||
* the `__drizzle_migrations` table, and for PostgreSQL, inside the drizzle schema.
|
||||
* However, you can configure where to store those records.
|
||||
*
|
||||
* See https://orm.drizzle.team/kit-docs/config-reference#migrations
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* `breakpoints` - param lets you enable/disable SQL statement breakpoints in generated migrations.
|
||||
* It’s optional and true by default, it’s necessary to properly apply migrations on databases,
|
||||
* that do not support multiple DDL alternation statements in one transaction(MySQL, SQLite, SingleStore) and
|
||||
* Drizzle ORM has to apply them sequentially one by one.
|
||||
*
|
||||
* See https://orm.drizzle.team/kit-docs/config-reference#breakpoints
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* `tablesFilters` - param lets you filter tables with glob syntax for db push command.
|
||||
* It’s useful when you have only one database avaialable for several separate projects with separate sql schemas.
|
||||
*
|
||||
* How to define multi-project tables with Drizzle ORM — see https://orm.drizzle.team/docs/goodies#multi-project-schema
|
||||
*
|
||||
* See https://orm.drizzle.team/kit-docs/config-reference#tablesfilters
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* `schemaFilter` - parameter allows you to define which schema in PostgreSQL should be used for either introspect or push commands.
|
||||
* This parameter accepts a single schema as a string or an array of schemas as strings.
|
||||
* No glob pattern is supported here. By default, drizzle will use the public schema for both commands,
|
||||
* but you can add any schema you need.
|
||||
*
|
||||
* For example, having schemaFilter: ["my_schema"] will only look for tables in both the database and
|
||||
* drizzle schema that are a part of the my_schema schema.
|
||||
*
|
||||
* See https://orm.drizzle.team/kit-docs/config-reference#schemafilter
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* `verbose` - command is used for drizzle-kit push commands and prints all statements that will be executed.
|
||||
*
|
||||
* > Note: This command will only print the statements that should be executed.
|
||||
* To approve them before applying, please refer to the `strict` command.
|
||||
*
|
||||
* See https://orm.drizzle.team/kit-docs/config-reference#verbose
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* `strict` - command is used for drizzle-kit push commands and will always ask for your confirmation,
|
||||
* either to execute all statements needed to sync your schema with the database or not.
|
||||
*
|
||||
* See https://orm.drizzle.team/kit-docs/config-reference#strict
|
||||
*/
|
||||
export function defineConfig(config: Config) {
|
||||
return config;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,913 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
import { toCamelCase } from 'drizzle-orm/casing';
|
||||
import './@types/utils';
|
||||
import type { Casing } from './cli/validations/common';
|
||||
import { assertUnreachable } from './global';
|
||||
import {
|
||||
Column,
|
||||
Index,
|
||||
PrimaryKey,
|
||||
SingleStoreSchema,
|
||||
SingleStoreSchemaInternal,
|
||||
UniqueConstraint,
|
||||
} from './serializer/singlestoreSchema';
|
||||
import { indexName } from './serializer/singlestoreSerializer';
|
||||
|
||||
// time precision to fsp
|
||||
// {mode: "string"} for timestamp by default
|
||||
|
||||
const singlestoreImportsList = new Set([
|
||||
'singlestoreTable',
|
||||
'singlestoreEnum',
|
||||
'bigint',
|
||||
'binary',
|
||||
'boolean',
|
||||
'char',
|
||||
'date',
|
||||
'datetime',
|
||||
'decimal',
|
||||
'double',
|
||||
'float',
|
||||
'int',
|
||||
'json',
|
||||
// TODO: add new type BSON
|
||||
// TODO: add new type Blob
|
||||
// TODO: add new type UUID
|
||||
// TODO: add new type GUID
|
||||
// TODO: add new type Vector
|
||||
// TODO: add new type GeoPoint
|
||||
'mediumint',
|
||||
'real',
|
||||
'serial',
|
||||
'smallint',
|
||||
'text',
|
||||
'tinytext',
|
||||
'mediumtext',
|
||||
'longtext',
|
||||
'time',
|
||||
'timestamp',
|
||||
'tinyint',
|
||||
'varbinary',
|
||||
'varchar',
|
||||
'vector',
|
||||
'year',
|
||||
'enum',
|
||||
]);
|
||||
|
||||
const objToStatement = (json: any) => {
|
||||
json = Object.fromEntries(Object.entries(json).filter((it) => it[1]));
|
||||
|
||||
const keys = Object.keys(json);
|
||||
if (keys.length === 0) return;
|
||||
|
||||
let statement = '{ ';
|
||||
statement += keys.map((it) => `"${it}": "${json[it]}"`).join(', ');
|
||||
statement += ' }';
|
||||
return statement;
|
||||
};
|
||||
|
||||
const objToStatement2 = (json: any) => {
|
||||
json = Object.fromEntries(Object.entries(json).filter((it) => it[1]));
|
||||
|
||||
const keys = Object.keys(json);
|
||||
if (keys.length === 0) return;
|
||||
|
||||
let statement = '{ ';
|
||||
statement += keys.map((it) => `${it}: "${json[it]}"`).join(', '); // no "" for keys
|
||||
statement += ' }';
|
||||
return statement;
|
||||
};
|
||||
|
||||
const timeConfig = (json: any) => {
|
||||
json = Object.fromEntries(Object.entries(json).filter((it) => it[1]));
|
||||
|
||||
const keys = Object.keys(json);
|
||||
if (keys.length === 0) return;
|
||||
|
||||
let statement = '{ ';
|
||||
statement += keys.map((it) => `${it}: ${json[it]}`).join(', ');
|
||||
statement += ' }';
|
||||
return statement;
|
||||
};
|
||||
|
||||
const binaryConfig = (json: any) => {
|
||||
json = Object.fromEntries(Object.entries(json).filter((it) => it[1]));
|
||||
|
||||
const keys = Object.keys(json);
|
||||
if (keys.length === 0) return;
|
||||
|
||||
let statement = '{ ';
|
||||
statement += keys.map((it) => `${it}: ${json[it]}`).join(', ');
|
||||
statement += ' }';
|
||||
return statement;
|
||||
};
|
||||
|
||||
const importsPatch = {
|
||||
'double precision': 'doublePrecision',
|
||||
'timestamp without time zone': 'timestamp',
|
||||
} as Record<string, string>;
|
||||
|
||||
const escapeColumnKey = (value: string) => {
|
||||
if (/^(?![a-zA-Z_$][a-zA-Z0-9_$]*$).+$/.test(value)) {
|
||||
return `"${value}"`;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const prepareCasing = (casing?: Casing) => (value: string) => {
|
||||
if (casing === 'preserve') {
|
||||
return escapeColumnKey(value);
|
||||
}
|
||||
if (casing === 'camel') {
|
||||
return escapeColumnKey(value.camelCase());
|
||||
}
|
||||
|
||||
assertUnreachable(casing);
|
||||
};
|
||||
|
||||
const dbColumnName = ({ name, casing, withMode = false }: { name: string; casing: Casing; withMode?: boolean }) => {
|
||||
if (casing === 'preserve') {
|
||||
return '';
|
||||
}
|
||||
if (casing === 'camel') {
|
||||
return toCamelCase(name) === name ? '' : withMode ? `"${name}", ` : `"${name}"`;
|
||||
}
|
||||
|
||||
assertUnreachable(casing);
|
||||
};
|
||||
|
||||
export const schemaToTypeScript = (
|
||||
schema: SingleStoreSchemaInternal,
|
||||
casing: Casing,
|
||||
) => {
|
||||
const withCasing = prepareCasing(casing);
|
||||
|
||||
const imports = Object.values(schema.tables).reduce(
|
||||
(res, it) => {
|
||||
const idxImports = Object.values(it.indexes).map((idx) => idx.isUnique ? 'uniqueIndex' : 'index');
|
||||
const pkImports = Object.values(it.compositePrimaryKeys).map(
|
||||
(it) => 'primaryKey',
|
||||
);
|
||||
const uniqueImports = Object.values(it.uniqueConstraints).map(
|
||||
(it) => 'unique',
|
||||
);
|
||||
|
||||
res.singlestore.push(...idxImports);
|
||||
res.singlestore.push(...pkImports);
|
||||
res.singlestore.push(...uniqueImports);
|
||||
|
||||
const columnImports = Object.values(it.columns)
|
||||
.map((col) => {
|
||||
let patched = importsPatch[col.type] ?? col.type;
|
||||
patched = patched.startsWith('varchar(') ? 'varchar' : patched;
|
||||
patched = patched.startsWith('char(') ? 'char' : patched;
|
||||
patched = patched.startsWith('binary(') ? 'binary' : patched;
|
||||
patched = patched.startsWith('decimal(') ? 'decimal' : patched;
|
||||
patched = patched.startsWith('smallint(') ? 'smallint' : patched;
|
||||
patched = patched.startsWith('enum(') ? 'singlestoreEnum' : patched;
|
||||
patched = patched.startsWith('datetime(') ? 'datetime' : patched;
|
||||
patched = patched.startsWith('varbinary(') ? 'varbinary' : patched;
|
||||
patched = patched.startsWith('int(') ? 'int' : patched;
|
||||
patched = patched.startsWith('double(') ? 'double' : patched;
|
||||
patched = patched.startsWith('float(') ? 'float' : patched;
|
||||
patched = patched.startsWith('int unsigned') ? 'int' : patched;
|
||||
patched = patched.startsWith('tinyint(') ? 'tinyint' : patched;
|
||||
patched = patched.startsWith('mediumint(') ? 'mediumint' : patched;
|
||||
patched = patched.startsWith('bigint(') ? 'bigint' : patched;
|
||||
patched = patched.startsWith('tinyint unsigned') ? 'tinyint' : patched;
|
||||
patched = patched.startsWith('smallint unsigned') ? 'smallint' : patched;
|
||||
patched = patched.startsWith('mediumint unsigned') ? 'mediumint' : patched;
|
||||
patched = patched.startsWith('bigint unsigned') ? 'bigint' : patched;
|
||||
return patched;
|
||||
})
|
||||
.filter((type) => {
|
||||
return singlestoreImportsList.has(type);
|
||||
});
|
||||
|
||||
res.singlestore.push(...columnImports);
|
||||
return res;
|
||||
},
|
||||
{ singlestore: [] as string[] },
|
||||
);
|
||||
|
||||
/* Object.values(schema.views).forEach((it) => {
|
||||
imports.singlestore.push('singlestoreView');
|
||||
|
||||
const columnImports = Object.values(it.columns)
|
||||
.map((col) => {
|
||||
let patched = importsPatch[col.type] ?? col.type;
|
||||
patched = patched.startsWith('varchar(') ? 'varchar' : patched;
|
||||
patched = patched.startsWith('char(') ? 'char' : patched;
|
||||
patched = patched.startsWith('binary(') ? 'binary' : patched;
|
||||
patched = patched.startsWith('decimal(') ? 'decimal' : patched;
|
||||
patched = patched.startsWith('smallint(') ? 'smallint' : patched;
|
||||
patched = patched.startsWith('enum(') ? 'singlestoreEnum' : patched;
|
||||
patched = patched.startsWith('datetime(') ? 'datetime' : patched;
|
||||
patched = patched.startsWith('varbinary(') ? 'varbinary' : patched;
|
||||
patched = patched.startsWith('int(') ? 'int' : patched;
|
||||
patched = patched.startsWith('double(') ? 'double' : patched;
|
||||
patched = patched.startsWith('float(') ? 'float' : patched;
|
||||
patched = patched.startsWith('int unsigned') ? 'int' : patched;
|
||||
patched = patched.startsWith('tinyint(') ? 'tinyint' : patched;
|
||||
patched = patched.startsWith('mediumint(') ? 'mediumint' : patched;
|
||||
patched = patched.startsWith('bigint(') ? 'bigint' : patched;
|
||||
patched = patched.startsWith('tinyint unsigned') ? 'tinyint' : patched;
|
||||
patched = patched.startsWith('smallint unsigned') ? 'smallint' : patched;
|
||||
patched = patched.startsWith('mediumint unsigned') ? 'mediumint' : patched;
|
||||
patched = patched.startsWith('bigint unsigned') ? 'bigint' : patched;
|
||||
return patched;
|
||||
})
|
||||
.filter((type) => {
|
||||
return singlestoreImportsList.has(type);
|
||||
});
|
||||
|
||||
imports.singlestore.push(...columnImports);
|
||||
}); */
|
||||
|
||||
const tableStatements = Object.values(schema.tables).map((table) => {
|
||||
const func = 'singlestoreTable';
|
||||
let statement = '';
|
||||
if (imports.singlestore.includes(withCasing(table.name))) {
|
||||
statement = `// Table name is in conflict with ${
|
||||
withCasing(
|
||||
table.name,
|
||||
)
|
||||
} import.\n// Please change to any other name, that is not in imports list\n`;
|
||||
}
|
||||
statement += `export const ${withCasing(table.name)} = ${func}("${table.name}", {\n`;
|
||||
statement += createTableColumns(
|
||||
Object.values(table.columns),
|
||||
withCasing,
|
||||
casing,
|
||||
table.name,
|
||||
schema,
|
||||
);
|
||||
statement += '}';
|
||||
|
||||
if (
|
||||
Object.keys(table.indexes).length > 0
|
||||
|| Object.keys(table.compositePrimaryKeys).length > 0
|
||||
|| Object.keys(table.uniqueConstraints).length > 0
|
||||
) {
|
||||
statement += ',\n';
|
||||
statement += '(table) => [';
|
||||
statement += createTableIndexes(
|
||||
table.name,
|
||||
Object.values(table.indexes),
|
||||
withCasing,
|
||||
);
|
||||
statement += createTablePKs(
|
||||
Object.values(table.compositePrimaryKeys),
|
||||
withCasing,
|
||||
);
|
||||
statement += createTableUniques(
|
||||
Object.values(table.uniqueConstraints),
|
||||
withCasing,
|
||||
);
|
||||
statement += '\n]';
|
||||
}
|
||||
|
||||
statement += ');';
|
||||
return statement;
|
||||
});
|
||||
|
||||
/* const viewsStatements = Object.values(schema.views).map((view) => {
|
||||
const { columns, name, algorithm, definition, sqlSecurity, withCheckOption } = view;
|
||||
const func = 'singlestoreView';
|
||||
let statement = '';
|
||||
|
||||
if (imports.singlestore.includes(withCasing(name))) {
|
||||
statement = `// Table name is in conflict with ${
|
||||
withCasing(
|
||||
view.name,
|
||||
)
|
||||
} import.\n// Please change to any other name, that is not in imports list\n`;
|
||||
}
|
||||
statement += `export const ${withCasing(name)} = ${func}("${name}", {\n`;
|
||||
statement += createTableColumns(
|
||||
Object.values(columns),
|
||||
withCasing,
|
||||
casing,
|
||||
name,
|
||||
schema,
|
||||
);
|
||||
statement += '})';
|
||||
|
||||
statement += algorithm ? `.algorithm("${algorithm}")` : '';
|
||||
statement += sqlSecurity ? `.sqlSecurity("${sqlSecurity}")` : '';
|
||||
statement += withCheckOption ? `.withCheckOption("${withCheckOption}")` : '';
|
||||
statement += `.as(sql\`${definition?.replaceAll('`', '\\`')}\`);`;
|
||||
|
||||
return statement;
|
||||
}); */
|
||||
|
||||
const uniqueSingleStoreImports = [
|
||||
'singlestoreTable',
|
||||
'singlestoreSchema',
|
||||
'AnySingleStoreColumn',
|
||||
...new Set(imports.singlestore),
|
||||
];
|
||||
const importsTs = `import { ${
|
||||
uniqueSingleStoreImports.join(
|
||||
', ',
|
||||
)
|
||||
} } from "drizzle-orm/singlestore-core"\nimport { sql } from "drizzle-orm"\n\n`;
|
||||
|
||||
let decalrations = '';
|
||||
decalrations += tableStatements.join('\n\n');
|
||||
decalrations += '\n';
|
||||
/* decalrations += viewsStatements.join('\n\n'); */
|
||||
|
||||
const file = importsTs + decalrations;
|
||||
|
||||
const schemaEntry = `
|
||||
{
|
||||
${
|
||||
Object.values(schema.tables)
|
||||
.map((it) => withCasing(it.name))
|
||||
.join(',')
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
return {
|
||||
file, // backward compatible, print to file
|
||||
imports: importsTs,
|
||||
decalrations,
|
||||
schemaEntry,
|
||||
};
|
||||
};
|
||||
|
||||
const mapColumnDefault = (defaultValue: any, isExpression?: boolean) => {
|
||||
if (isExpression) {
|
||||
return `sql\`${defaultValue}\``;
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
const mapColumnDefaultForJson = (defaultValue: any) => {
|
||||
if (
|
||||
typeof defaultValue === 'string'
|
||||
&& defaultValue.startsWith("('")
|
||||
&& defaultValue.endsWith("')")
|
||||
) {
|
||||
return defaultValue.substring(2, defaultValue.length - 2);
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
const column = (
|
||||
type: string,
|
||||
name: string,
|
||||
casing: (value: string) => string,
|
||||
rawCasing: Casing,
|
||||
defaultValue?: any,
|
||||
autoincrement?: boolean,
|
||||
onUpdate?: boolean,
|
||||
isExpression?: boolean,
|
||||
) => {
|
||||
let lowered = type;
|
||||
if (!type.startsWith('enum(')) {
|
||||
lowered = type.toLowerCase();
|
||||
}
|
||||
|
||||
if (lowered === 'serial') {
|
||||
return `${casing(name)}: serial(${dbColumnName({ name, casing: rawCasing })})`;
|
||||
}
|
||||
|
||||
if (lowered.startsWith('int')) {
|
||||
const isUnsigned = lowered.includes('unsigned');
|
||||
const columnName = dbColumnName({ name, casing: rawCasing, withMode: isUnsigned });
|
||||
let out = `${casing(name)}: int(${columnName}${isUnsigned ? '{ unsigned: true }' : ''})`;
|
||||
out += autoincrement ? `.autoincrement()` : '';
|
||||
out += typeof defaultValue !== 'undefined'
|
||||
? `.default(${mapColumnDefault(defaultValue, isExpression)})`
|
||||
: '';
|
||||
return out;
|
||||
}
|
||||
|
||||
if (lowered.startsWith('tinyint')) {
|
||||
const isUnsigned = lowered.includes('unsigned');
|
||||
const columnName = dbColumnName({ name, casing: rawCasing, withMode: isUnsigned });
|
||||
let out: string = `${casing(name)}: tinyint(${columnName}${isUnsigned ? '{ unsigned: true }' : ''})`;
|
||||
out += autoincrement ? `.autoincrement()` : '';
|
||||
out += typeof defaultValue !== 'undefined'
|
||||
? `.default(${mapColumnDefault(defaultValue, isExpression)})`
|
||||
: '';
|
||||
return out;
|
||||
}
|
||||
|
||||
if (lowered.startsWith('smallint')) {
|
||||
const isUnsigned = lowered.includes('unsigned');
|
||||
const columnName = dbColumnName({ name, casing: rawCasing, withMode: isUnsigned });
|
||||
let out = `${casing(name)}: smallint(${columnName}${isUnsigned ? '{ unsigned: true }' : ''})`;
|
||||
out += autoincrement ? `.autoincrement()` : '';
|
||||
out += defaultValue
|
||||
? `.default(${mapColumnDefault(defaultValue, isExpression)})`
|
||||
: '';
|
||||
return out;
|
||||
}
|
||||
|
||||
if (lowered.startsWith('mediumint')) {
|
||||
const isUnsigned = lowered.includes('unsigned');
|
||||
const columnName = dbColumnName({ name, casing: rawCasing, withMode: isUnsigned });
|
||||
let out = `${casing(name)}: mediumint(${columnName}${isUnsigned ? '{ unsigned: true }' : ''})`;
|
||||
out += autoincrement ? `.autoincrement()` : '';
|
||||
out += defaultValue
|
||||
? `.default(${mapColumnDefault(defaultValue, isExpression)})`
|
||||
: '';
|
||||
return out;
|
||||
}
|
||||
|
||||
if (lowered.startsWith('bigint')) {
|
||||
const isUnsigned = lowered.includes('unsigned');
|
||||
let out = `${casing(name)}: bigint(${dbColumnName({ name, casing: rawCasing, withMode: true })}{ mode: "number"${
|
||||
isUnsigned ? ', unsigned: true' : ''
|
||||
} })`;
|
||||
out += autoincrement ? `.autoincrement()` : '';
|
||||
out += defaultValue
|
||||
? `.default(${mapColumnDefault(defaultValue, isExpression)})`
|
||||
: '';
|
||||
return out;
|
||||
}
|
||||
|
||||
if (lowered === 'boolean') {
|
||||
let out = `${casing(name)}: boolean(${dbColumnName({ name, casing: rawCasing })})`;
|
||||
out += defaultValue
|
||||
? `.default(${mapColumnDefault(defaultValue, isExpression)})`
|
||||
: '';
|
||||
return out;
|
||||
}
|
||||
|
||||
if (lowered.startsWith('double')) {
|
||||
let params:
|
||||
| { precision?: string; scale?: string; unsigned?: boolean }
|
||||
| undefined;
|
||||
|
||||
if (lowered.length > (lowered.includes('unsigned') ? 15 : 6)) {
|
||||
const [precision, scale] = lowered
|
||||
.slice(7, lowered.length - (1 + (lowered.includes('unsigned') ? 9 : 0)))
|
||||
.split(',');
|
||||
params = { precision, scale };
|
||||
}
|
||||
|
||||
if (lowered.includes('unsigned')) {
|
||||
params = { ...(params ?? {}), unsigned: true };
|
||||
}
|
||||
|
||||
const timeConfigParams = params ? timeConfig(params) : undefined;
|
||||
|
||||
let out = params
|
||||
? `${casing(name)}: double(${
|
||||
dbColumnName({ name, casing: rawCasing, withMode: timeConfigParams !== undefined })
|
||||
}${timeConfig(params)})`
|
||||
: `${casing(name)}: double(${dbColumnName({ name, casing: rawCasing })})`;
|
||||
|
||||
// let out = `${name.camelCase()}: double("${name}")`;
|
||||
out += defaultValue
|
||||
? `.default(${mapColumnDefault(defaultValue, isExpression)})`
|
||||
: '';
|
||||
return out;
|
||||
}
|
||||
|
||||
if (lowered.startsWith('float')) {
|
||||
let params:
|
||||
| { precision?: string; scale?: string; unsigned?: boolean }
|
||||
| undefined;
|
||||
|
||||
if (lowered.length > (lowered.includes('unsigned') ? 14 : 5)) {
|
||||
const [precision, scale] = lowered
|
||||
.slice(6, lowered.length - (1 + (lowered.includes('unsigned') ? 9 : 0)))
|
||||
.split(',');
|
||||
params = { precision, scale };
|
||||
}
|
||||
|
||||
if (lowered.includes('unsigned')) {
|
||||
params = { ...(params ?? {}), unsigned: true };
|
||||
}
|
||||
|
||||
let out = `${casing(name)}: float(${dbColumnName({ name, casing: rawCasing })}${params ? timeConfig(params) : ''})`;
|
||||
out += defaultValue
|
||||
? `.default(${mapColumnDefault(defaultValue, isExpression)})`
|
||||
: '';
|
||||
return out;
|
||||
}
|
||||
|
||||
if (lowered === 'real') {
|
||||
let out = `${casing(name)}: real(${dbColumnName({ name, casing: rawCasing })})`;
|
||||
out += defaultValue
|
||||
? `.default(${mapColumnDefault(defaultValue, isExpression)})`
|
||||
: '';
|
||||
return out;
|
||||
}
|
||||
|
||||
if (lowered.startsWith('timestamp')) {
|
||||
const keyLength = 'timestamp'.length + 1;
|
||||
let fsp = lowered.length > keyLength
|
||||
? Number(lowered.substring(keyLength, lowered.length - 1))
|
||||
: null;
|
||||
fsp = fsp ? fsp : null;
|
||||
|
||||
const params = timeConfig({ fsp, mode: "'string'" });
|
||||
|
||||
let out = params
|
||||
? `${casing(name)}: timestamp(${
|
||||
dbColumnName({ name, casing: rawCasing, withMode: params !== undefined })
|
||||
}${params})`
|
||||
: `${casing(name)}: timestamp(${dbColumnName({ name, casing: rawCasing })})`;
|
||||
|
||||
// singlestore has only CURRENT_TIMESTAMP, as I found from docs. But will leave now() for just a case
|
||||
defaultValue = defaultValue === 'now()' || defaultValue === 'CURRENT_TIMESTAMP'
|
||||
? '.defaultNow()'
|
||||
: defaultValue
|
||||
? `.default(${mapColumnDefault(defaultValue, isExpression)})`
|
||||
: '';
|
||||
|
||||
out += defaultValue;
|
||||
|
||||
let onUpdateNow = onUpdate ? '.onUpdateNow()' : '';
|
||||
out += onUpdateNow;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
if (lowered.startsWith('time')) {
|
||||
const keyLength = 'time'.length + 1;
|
||||
let fsp = lowered.length > keyLength
|
||||
? Number(lowered.substring(keyLength, lowered.length - 1))
|
||||
: null;
|
||||
fsp = fsp ? fsp : null;
|
||||
|
||||
const params = timeConfig({ fsp });
|
||||
|
||||
let out = params
|
||||
? `${casing(name)}: time(${dbColumnName({ name, casing: rawCasing, withMode: params !== undefined })}${params})`
|
||||
: `${casing(name)}: time(${dbColumnName({ name, casing: rawCasing })})`;
|
||||
|
||||
defaultValue = defaultValue === 'now()'
|
||||
? '.defaultNow()'
|
||||
: defaultValue
|
||||
? `.default(${mapColumnDefault(defaultValue, isExpression)})`
|
||||
: '';
|
||||
|
||||
out += defaultValue;
|
||||
return out;
|
||||
}
|
||||
|
||||
if (lowered === 'date') {
|
||||
let out = `// you can use { mode: 'date' }, if you want to have Date as type for this column\n\t${
|
||||
casing(
|
||||
name,
|
||||
)
|
||||
}: date(${dbColumnName({ name, casing: rawCasing, withMode: true })}{ mode: 'string' })`;
|
||||
|
||||
defaultValue = defaultValue === 'now()'
|
||||
? '.defaultNow()'
|
||||
: defaultValue
|
||||
? `.default(${mapColumnDefault(defaultValue, isExpression)})`
|
||||
: '';
|
||||
|
||||
out += defaultValue;
|
||||
return out;
|
||||
}
|
||||
|
||||
// in singlestore text can't have default value. Will leave it in case smth ;)
|
||||
if (lowered === 'text') {
|
||||
let out = `${casing(name)}: text(${dbColumnName({ name, casing: rawCasing })})`;
|
||||
out += defaultValue
|
||||
? `.default(${mapColumnDefault(defaultValue, isExpression)})`
|
||||
: '';
|
||||
return out;
|
||||
}
|
||||
|
||||
// in singlestore text can't have default value. Will leave it in case smth ;)
|
||||
if (lowered === 'tinytext') {
|
||||
let out = `${casing(name)}: tinytext(${dbColumnName({ name, casing: rawCasing })})`;
|
||||
out += defaultValue
|
||||
? `.default(${mapColumnDefault(defaultValue, isExpression)})`
|
||||
: '';
|
||||
return out;
|
||||
}
|
||||
|
||||
// in singlestore text can't have default value. Will leave it in case smth ;)
|
||||
if (lowered === 'mediumtext') {
|
||||
let out = `${casing(name)}: mediumtext(${dbColumnName({ name, casing: rawCasing })})`;
|
||||
out += defaultValue
|
||||
? `.default(${mapColumnDefault(defaultValue, isExpression)})`
|
||||
: '';
|
||||
return out;
|
||||
}
|
||||
|
||||
// in singlestore text can't have default value. Will leave it in case smth ;)
|
||||
if (lowered === 'longtext') {
|
||||
let out = `${casing(name)}: longtext(${dbColumnName({ name, casing: rawCasing })})`;
|
||||
out += defaultValue
|
||||
? `.default(${mapColumnDefault(defaultValue, isExpression)})`
|
||||
: '';
|
||||
return out;
|
||||
}
|
||||
|
||||
if (lowered === 'year') {
|
||||
let out = `${casing(name)}: year(${dbColumnName({ name, casing: rawCasing })})`;
|
||||
out += defaultValue
|
||||
? `.default(${mapColumnDefault(defaultValue, isExpression)})`
|
||||
: '';
|
||||
return out;
|
||||
}
|
||||
|
||||
// in singlestore json can't have default value. Will leave it in case smth ;)
|
||||
if (lowered === 'json') {
|
||||
let out = `${casing(name)}: json(${dbColumnName({ name, casing: rawCasing })})`;
|
||||
|
||||
out += defaultValue
|
||||
? `.default(${mapColumnDefaultForJson(defaultValue)})`
|
||||
: '';
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
if (lowered.startsWith('varchar')) {
|
||||
let out: string = `${
|
||||
casing(
|
||||
name,
|
||||
)
|
||||
}: varchar(${dbColumnName({ name, casing: rawCasing, withMode: true })}{ length: ${
|
||||
lowered.substring(
|
||||
'varchar'.length + 1,
|
||||
lowered.length - 1,
|
||||
)
|
||||
} })`;
|
||||
|
||||
out += defaultValue
|
||||
? `.default(${mapColumnDefault(defaultValue, isExpression)})`
|
||||
: '';
|
||||
return out;
|
||||
}
|
||||
|
||||
if (lowered.startsWith('char')) {
|
||||
let out: string = `${
|
||||
casing(
|
||||
name,
|
||||
)
|
||||
}: char(${dbColumnName({ name, casing: rawCasing, withMode: true })}{ length: ${
|
||||
lowered.substring(
|
||||
'char'.length + 1,
|
||||
lowered.length - 1,
|
||||
)
|
||||
} })`;
|
||||
|
||||
out += defaultValue
|
||||
? `.default(${mapColumnDefault(defaultValue, isExpression)})`
|
||||
: '';
|
||||
return out;
|
||||
}
|
||||
|
||||
if (lowered.startsWith('datetime')) {
|
||||
let out = `// you can use { mode: 'date' }, if you want to have Date as type for this column\n\t`;
|
||||
|
||||
const fsp = lowered.startsWith('datetime(')
|
||||
? lowered.substring('datetime'.length + 1, lowered.length - 1)
|
||||
: undefined;
|
||||
|
||||
out = fsp
|
||||
? `${
|
||||
casing(
|
||||
name,
|
||||
)
|
||||
}: datetime(${dbColumnName({ name, casing: rawCasing, withMode: true })}{ mode: 'string', fsp: ${
|
||||
lowered.substring(
|
||||
'datetime'.length + 1,
|
||||
lowered.length - 1,
|
||||
)
|
||||
} })`
|
||||
: `${casing(name)}: datetime(${dbColumnName({ name, casing: rawCasing, withMode: true })}{ mode: 'string'})`;
|
||||
|
||||
defaultValue = defaultValue === 'now()'
|
||||
? '.defaultNow()'
|
||||
: defaultValue
|
||||
? `.default(${mapColumnDefault(defaultValue, isExpression)})`
|
||||
: '';
|
||||
|
||||
out += defaultValue;
|
||||
return out;
|
||||
}
|
||||
|
||||
if (lowered.startsWith('decimal')) {
|
||||
let params:
|
||||
| { precision?: string; scale?: string; unsigned?: boolean }
|
||||
| undefined;
|
||||
|
||||
if (lowered.length > (lowered.includes('unsigned') ? 16 : 7)) {
|
||||
const [precision, scale] = lowered
|
||||
.slice(8, lowered.length - (1 + (lowered.includes('unsigned') ? 9 : 0)))
|
||||
.split(',');
|
||||
params = { precision, scale };
|
||||
}
|
||||
|
||||
if (lowered.includes('unsigned')) {
|
||||
params = { ...(params ?? {}), unsigned: true };
|
||||
}
|
||||
|
||||
const timeConfigParams = params ? timeConfig(params) : undefined;
|
||||
|
||||
let out = params
|
||||
? `${casing(name)}: decimal(${
|
||||
dbColumnName({ name, casing: rawCasing, withMode: timeConfigParams !== undefined })
|
||||
}${timeConfigParams})`
|
||||
: `${casing(name)}: decimal(${dbColumnName({ name, casing: rawCasing })})`;
|
||||
|
||||
defaultValue = typeof defaultValue !== 'undefined'
|
||||
? `.default(${mapColumnDefault(defaultValue, isExpression)})`
|
||||
: '';
|
||||
|
||||
out += defaultValue;
|
||||
return out;
|
||||
}
|
||||
|
||||
if (lowered.startsWith('binary')) {
|
||||
const keyLength = 'binary'.length + 1;
|
||||
let length = lowered.length > keyLength
|
||||
? Number(lowered.substring(keyLength, lowered.length - 1))
|
||||
: null;
|
||||
length = length ? length : null;
|
||||
|
||||
const params = binaryConfig({ length });
|
||||
|
||||
let out = params
|
||||
? `${casing(name)}: binary(${dbColumnName({ name, casing: rawCasing, withMode: params !== undefined })}${params})`
|
||||
: `${casing(name)}: binary(${dbColumnName({ name, casing: rawCasing })})`;
|
||||
|
||||
defaultValue = defaultValue
|
||||
? `.default(${mapColumnDefault(defaultValue, isExpression)})`
|
||||
: '';
|
||||
|
||||
out += defaultValue;
|
||||
return out;
|
||||
}
|
||||
|
||||
if (lowered.startsWith('enum')) {
|
||||
const values = lowered.substring('enum'.length + 1, lowered.length - 1);
|
||||
let out = `${casing(name)}: singlestoreEnum(${
|
||||
dbColumnName({ name, casing: rawCasing, withMode: true })
|
||||
}[${values}])`;
|
||||
out += defaultValue
|
||||
? `.default(${mapColumnDefault(defaultValue, isExpression)})`
|
||||
: '';
|
||||
return out;
|
||||
}
|
||||
|
||||
if (lowered.startsWith('varbinary')) {
|
||||
const keyLength = 'varbinary'.length + 1;
|
||||
let length = lowered.length > keyLength
|
||||
? Number(lowered.substring(keyLength, lowered.length - 1))
|
||||
: null;
|
||||
length = length ? length : null;
|
||||
|
||||
const params = binaryConfig({ length });
|
||||
|
||||
let out = params
|
||||
? `${casing(name)}: varbinary(${
|
||||
dbColumnName({ name, casing: rawCasing, withMode: params !== undefined })
|
||||
}${params})`
|
||||
: `${casing(name)}: varbinary(${dbColumnName({ name, casing: rawCasing })})`;
|
||||
|
||||
defaultValue = defaultValue
|
||||
? `.default(${mapColumnDefault(defaultValue, isExpression)})`
|
||||
: '';
|
||||
|
||||
out += defaultValue;
|
||||
return out;
|
||||
}
|
||||
|
||||
if (lowered.startsWith('vector')) {
|
||||
const [dimensions, elementType] = lowered.substring('vector'.length + 1, lowered.length - 1).split(',');
|
||||
let out = `${casing(name)}: vector(${
|
||||
dbColumnName({ name, casing: rawCasing, withMode: true })
|
||||
}{ dimensions: ${dimensions}, elementType: ${elementType} })`;
|
||||
|
||||
out += defaultValue ? `.default(${mapColumnDefault(defaultValue, isExpression)})` : '';
|
||||
return out;
|
||||
}
|
||||
|
||||
console.log('uknown', type);
|
||||
return `// Warning: Can't parse ${type} from database\n\t// ${type}Type: ${type}("${name}")`;
|
||||
};
|
||||
|
||||
const createTableColumns = (
|
||||
columns: Column[],
|
||||
casing: (val: string) => string,
|
||||
rawCasing: Casing,
|
||||
tableName: string,
|
||||
schema: SingleStoreSchemaInternal,
|
||||
): string => {
|
||||
let statement = '';
|
||||
|
||||
columns.forEach((it) => {
|
||||
statement += '\t';
|
||||
statement += column(
|
||||
it.type,
|
||||
it.name,
|
||||
casing,
|
||||
rawCasing,
|
||||
it.default,
|
||||
it.autoincrement,
|
||||
it.onUpdate,
|
||||
schema.internal?.tables![tableName]?.columns[it.name]
|
||||
?.isDefaultAnExpression ?? false,
|
||||
);
|
||||
statement += it.primaryKey ? '.primaryKey()' : '';
|
||||
statement += it.notNull ? '.notNull()' : '';
|
||||
|
||||
statement += it.generated
|
||||
? `.generatedAlwaysAs(sql\`${
|
||||
it.generated.as.replace(
|
||||
/`/g,
|
||||
'\\`',
|
||||
)
|
||||
}\`, { mode: "${it.generated.type}" })`
|
||||
: '';
|
||||
|
||||
statement += ',\n';
|
||||
});
|
||||
|
||||
return statement;
|
||||
};
|
||||
|
||||
const createTableIndexes = (
|
||||
tableName: string,
|
||||
idxs: Index[],
|
||||
casing: (value: string) => string,
|
||||
): string => {
|
||||
let statement = '';
|
||||
|
||||
idxs.forEach((it) => {
|
||||
let idxKey = it.name.startsWith(tableName) && it.name !== tableName
|
||||
? it.name.slice(tableName.length + 1)
|
||||
: it.name;
|
||||
idxKey = idxKey.endsWith('_index')
|
||||
? idxKey.slice(0, -'_index'.length) + '_idx'
|
||||
: idxKey;
|
||||
|
||||
idxKey = casing(idxKey);
|
||||
|
||||
const indexGeneratedName = indexName(tableName, it.columns);
|
||||
const escapedIndexName = indexGeneratedName === it.name ? '' : `"${it.name}"`;
|
||||
|
||||
statement += `\n\t`;
|
||||
statement += it.isUnique ? 'uniqueIndex(' : 'index(';
|
||||
statement += `${escapedIndexName})`;
|
||||
statement += `.on(${
|
||||
it.columns
|
||||
.map((it) => `table.${casing(it)}`)
|
||||
.join(', ')
|
||||
}),`;
|
||||
});
|
||||
|
||||
return statement;
|
||||
};
|
||||
|
||||
const createTableUniques = (
|
||||
unqs: UniqueConstraint[],
|
||||
casing: (value: string) => string,
|
||||
): string => {
|
||||
let statement = '';
|
||||
|
||||
unqs.forEach((it) => {
|
||||
statement += `\n\t`;
|
||||
statement += 'unique(';
|
||||
statement += `"${it.name}")`;
|
||||
statement += `.on(${
|
||||
it.columns
|
||||
.map((it) => `table.${casing(it)}`)
|
||||
.join(', ')
|
||||
}),`;
|
||||
});
|
||||
|
||||
return statement;
|
||||
};
|
||||
|
||||
const createTablePKs = (
|
||||
pks: PrimaryKey[],
|
||||
casing: (value: string) => string,
|
||||
): string => {
|
||||
let statement = '';
|
||||
|
||||
pks.forEach((it) => {
|
||||
let idxKey = casing(it.name);
|
||||
|
||||
statement += `\n\t`;
|
||||
statement += 'primaryKey({ columns: [';
|
||||
statement += `${
|
||||
it.columns
|
||||
.map((c) => {
|
||||
return `table.${casing(c)}`;
|
||||
})
|
||||
.join(', ')
|
||||
}]${it.name ? `, name: "${it.name}"` : ''}}`;
|
||||
statement += '),';
|
||||
});
|
||||
|
||||
return statement;
|
||||
};
|
||||
@@ -0,0 +1,533 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
import { toCamelCase } from 'drizzle-orm/casing';
|
||||
import './@types/utils';
|
||||
import type { Casing } from './cli/validations/common';
|
||||
import { assertUnreachable } from './global';
|
||||
import { CheckConstraint } from './serializer/mysqlSchema';
|
||||
import type {
|
||||
Column,
|
||||
ForeignKey,
|
||||
Index,
|
||||
PrimaryKey,
|
||||
SQLiteSchema,
|
||||
SQLiteSchemaInternal,
|
||||
UniqueConstraint,
|
||||
} from './serializer/sqliteSchema';
|
||||
|
||||
const sqliteImportsList = new Set([
|
||||
'sqliteTable',
|
||||
'integer',
|
||||
'real',
|
||||
'text',
|
||||
'numeric',
|
||||
'blob',
|
||||
]);
|
||||
|
||||
export const indexName = (tableName: string, columns: string[]) => {
|
||||
return `${tableName}_${columns.join('_')}_index`;
|
||||
};
|
||||
|
||||
const objToStatement2 = (json: any) => {
|
||||
json = Object.fromEntries(Object.entries(json).filter((it) => it[1]));
|
||||
|
||||
const keys = Object.keys(json);
|
||||
if (keys.length === 0) return;
|
||||
|
||||
let statement = '{ ';
|
||||
statement += keys.map((it) => `${it}: "${json[it]}"`).join(', '); // no "" for keys
|
||||
statement += ' }';
|
||||
return statement;
|
||||
};
|
||||
|
||||
const relations = new Set<string>();
|
||||
|
||||
const escapeColumnKey = (value: string) => {
|
||||
if (/^(?![a-zA-Z_$][a-zA-Z0-9_$]*$).+$/.test(value)) {
|
||||
return `"${value}"`;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const withCasing = (value: string, casing?: Casing) => {
|
||||
if (casing === 'preserve') {
|
||||
return escapeColumnKey(value);
|
||||
}
|
||||
if (casing === 'camel') {
|
||||
return escapeColumnKey(value.camelCase());
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
const dbColumnName = ({ name, casing, withMode = false }: { name: string; casing: Casing; withMode?: boolean }) => {
|
||||
if (casing === 'preserve') {
|
||||
return '';
|
||||
}
|
||||
if (casing === 'camel') {
|
||||
return toCamelCase(name) === name ? '' : withMode ? `"${name}", ` : `"${name}"`;
|
||||
}
|
||||
|
||||
assertUnreachable(casing);
|
||||
};
|
||||
|
||||
export const schemaToTypeScript = (
|
||||
schema: SQLiteSchemaInternal,
|
||||
casing: Casing,
|
||||
) => {
|
||||
// collectFKs
|
||||
Object.values(schema.tables).forEach((table) => {
|
||||
Object.values(table.foreignKeys).forEach((fk) => {
|
||||
const relation = `${fk.tableFrom}-${fk.tableTo}`;
|
||||
relations.add(relation);
|
||||
});
|
||||
});
|
||||
|
||||
const imports = Object.values(schema.tables).reduce(
|
||||
(res, it) => {
|
||||
const idxImports = Object.values(it.indexes).map((idx) => idx.isUnique ? 'uniqueIndex' : 'index');
|
||||
const fkImpots = Object.values(it.foreignKeys).map((it) => 'foreignKey');
|
||||
const pkImports = Object.values(it.compositePrimaryKeys).map(
|
||||
(it) => 'primaryKey',
|
||||
);
|
||||
const uniqueImports = Object.values(it.uniqueConstraints).map(
|
||||
(it) => 'unique',
|
||||
);
|
||||
const checkImports = Object.values(it.checkConstraints).map(
|
||||
(it) => 'check',
|
||||
);
|
||||
|
||||
res.sqlite.push(...idxImports);
|
||||
res.sqlite.push(...fkImpots);
|
||||
res.sqlite.push(...pkImports);
|
||||
res.sqlite.push(...uniqueImports);
|
||||
res.sqlite.push(...checkImports);
|
||||
|
||||
const columnImports = Object.values(it.columns)
|
||||
.map((col) => {
|
||||
return col.type;
|
||||
})
|
||||
.filter((type) => {
|
||||
return sqliteImportsList.has(type);
|
||||
});
|
||||
|
||||
res.sqlite.push(...columnImports);
|
||||
return res;
|
||||
},
|
||||
{ sqlite: [] as string[] },
|
||||
);
|
||||
|
||||
Object.values(schema.views).forEach((it) => {
|
||||
imports.sqlite.push('sqliteView');
|
||||
|
||||
const columnImports = Object.values(it.columns)
|
||||
.map((col) => {
|
||||
return col.type;
|
||||
})
|
||||
.filter((type) => {
|
||||
return sqliteImportsList.has(type);
|
||||
});
|
||||
|
||||
imports.sqlite.push(...columnImports);
|
||||
});
|
||||
|
||||
const tableStatements = Object.values(schema.tables).map((table) => {
|
||||
const func = 'sqliteTable';
|
||||
let statement = '';
|
||||
if (imports.sqlite.includes(withCasing(table.name, casing))) {
|
||||
statement = `// Table name is in conflict with ${
|
||||
withCasing(
|
||||
table.name,
|
||||
casing,
|
||||
)
|
||||
} import.\n// Please change to any other name, that is not in imports list\n`;
|
||||
}
|
||||
statement += `export const ${withCasing(table.name, casing)} = ${func}("${table.name}", {\n`;
|
||||
statement += createTableColumns(
|
||||
Object.values(table.columns),
|
||||
Object.values(table.foreignKeys),
|
||||
casing,
|
||||
);
|
||||
statement += '}';
|
||||
|
||||
// more than 2 fields or self reference or cyclic
|
||||
const filteredFKs = Object.values(table.foreignKeys).filter((it) => {
|
||||
return it.columnsFrom.length > 1 || isSelf(it);
|
||||
});
|
||||
|
||||
if (
|
||||
Object.keys(table.indexes).length > 0
|
||||
|| filteredFKs.length > 0
|
||||
|| Object.keys(table.compositePrimaryKeys).length > 0
|
||||
|| Object.keys(table.uniqueConstraints).length > 0
|
||||
|| Object.keys(table.checkConstraints).length > 0
|
||||
) {
|
||||
statement += ',\n';
|
||||
statement += '(table) => [';
|
||||
statement += createTableIndexes(
|
||||
table.name,
|
||||
Object.values(table.indexes),
|
||||
casing,
|
||||
);
|
||||
statement += createTableFKs(Object.values(filteredFKs), casing);
|
||||
statement += createTablePKs(
|
||||
Object.values(table.compositePrimaryKeys),
|
||||
casing,
|
||||
);
|
||||
statement += createTableUniques(
|
||||
Object.values(table.uniqueConstraints),
|
||||
casing,
|
||||
);
|
||||
statement += createTableChecks(
|
||||
Object.values(table.checkConstraints),
|
||||
casing,
|
||||
);
|
||||
statement += '\n]';
|
||||
}
|
||||
|
||||
statement += ');';
|
||||
return statement;
|
||||
});
|
||||
|
||||
const viewsStatements = Object.values(schema.views).map((view) => {
|
||||
const func = 'sqliteView';
|
||||
|
||||
let statement = '';
|
||||
if (imports.sqlite.includes(withCasing(view.name, casing))) {
|
||||
statement = `// Table name is in conflict with ${
|
||||
withCasing(
|
||||
view.name,
|
||||
casing,
|
||||
)
|
||||
} import.\n// Please change to any other name, that is not in imports list\n`;
|
||||
}
|
||||
statement += `export const ${withCasing(view.name, casing)} = ${func}("${view.name}", {\n`;
|
||||
statement += createTableColumns(
|
||||
Object.values(view.columns),
|
||||
[],
|
||||
casing,
|
||||
);
|
||||
statement += '})';
|
||||
statement += `.as(sql\`${view.definition?.replaceAll('`', '\\`')}\`);`;
|
||||
|
||||
return statement;
|
||||
});
|
||||
|
||||
const uniqueSqliteImports = [
|
||||
'sqliteTable',
|
||||
'AnySQLiteColumn',
|
||||
...new Set(imports.sqlite),
|
||||
];
|
||||
|
||||
const importsTs = `import { ${
|
||||
uniqueSqliteImports.join(
|
||||
', ',
|
||||
)
|
||||
} } from "drizzle-orm/sqlite-core"
|
||||
import { sql } from "drizzle-orm"\n\n`;
|
||||
|
||||
let decalrations = tableStatements.join('\n\n');
|
||||
decalrations += '\n\n';
|
||||
decalrations += viewsStatements.join('\n\n');
|
||||
|
||||
const file = importsTs + decalrations;
|
||||
|
||||
// for drizzle studio query runner
|
||||
const schemaEntry = `
|
||||
{
|
||||
${
|
||||
Object.values(schema.tables)
|
||||
.map((it) => withCasing(it.name, casing))
|
||||
.join(',')
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
return { file, imports: importsTs, decalrations, schemaEntry };
|
||||
};
|
||||
|
||||
const isCyclic = (fk: ForeignKey) => {
|
||||
const key = `${fk.tableFrom}-${fk.tableTo}`;
|
||||
const reverse = `${fk.tableTo}-${fk.tableFrom}`;
|
||||
return relations.has(key) && relations.has(reverse);
|
||||
};
|
||||
|
||||
const isSelf = (fk: ForeignKey) => {
|
||||
return fk.tableFrom === fk.tableTo;
|
||||
};
|
||||
|
||||
const mapColumnDefault = (defaultValue: any) => {
|
||||
if (
|
||||
typeof defaultValue === 'string'
|
||||
&& defaultValue.startsWith('(')
|
||||
&& defaultValue.endsWith(')')
|
||||
) {
|
||||
return `sql\`${defaultValue}\``;
|
||||
}
|
||||
// If default value is NULL as string it will come back from db as "'NULL'" and not just "NULL"
|
||||
if (defaultValue === 'NULL') {
|
||||
return `sql\`NULL\``;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof defaultValue === 'string'
|
||||
) {
|
||||
return defaultValue.substring(1, defaultValue.length - 1).replaceAll('"', '\\"').replaceAll("''", "'");
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
const column = (
|
||||
type: string,
|
||||
name: string,
|
||||
defaultValue?: any,
|
||||
autoincrement?: boolean,
|
||||
casing?: Casing,
|
||||
) => {
|
||||
let lowered = type;
|
||||
casing = casing!;
|
||||
|
||||
if (lowered === 'integer') {
|
||||
let out = `${withCasing(name, casing)}: integer(${dbColumnName({ name, casing })})`;
|
||||
// out += autoincrement ? `.autoincrement()` : "";
|
||||
out += typeof defaultValue !== 'undefined'
|
||||
? `.default(${mapColumnDefault(defaultValue)})`
|
||||
: '';
|
||||
return out;
|
||||
}
|
||||
|
||||
if (lowered === 'real') {
|
||||
let out = `${withCasing(name, casing)}: real(${dbColumnName({ name, casing })})`;
|
||||
out += defaultValue ? `.default(${mapColumnDefault(defaultValue)})` : '';
|
||||
return out;
|
||||
}
|
||||
|
||||
if (lowered.startsWith('text')) {
|
||||
const match = lowered.match(/\d+/);
|
||||
let out: string;
|
||||
|
||||
if (match) {
|
||||
out = `${withCasing(name, casing)}: text(${dbColumnName({ name, casing, withMode: true })}{ length: ${
|
||||
match[0]
|
||||
} })`;
|
||||
} else {
|
||||
out = `${withCasing(name, casing)}: text(${dbColumnName({ name, casing })})`;
|
||||
}
|
||||
|
||||
out += defaultValue ? `.default("${mapColumnDefault(defaultValue)}")` : '';
|
||||
return out;
|
||||
}
|
||||
|
||||
if (lowered === 'blob') {
|
||||
let out = `${withCasing(name, casing)}: blob(${dbColumnName({ name, casing })})`;
|
||||
out += defaultValue ? `.default(${mapColumnDefault(defaultValue)})` : '';
|
||||
return out;
|
||||
}
|
||||
|
||||
if (lowered === 'numeric') {
|
||||
let out = `${withCasing(name, casing)}: numeric(${dbColumnName({ name, casing })})`;
|
||||
out += defaultValue ? `.default(${mapColumnDefault(defaultValue)})` : '';
|
||||
return out;
|
||||
}
|
||||
|
||||
// console.log("uknown", type);
|
||||
return `// Warning: Can't parse ${type} from database\n\t// ${type}Type: ${type}("${name}")`;
|
||||
};
|
||||
|
||||
const createTableColumns = (
|
||||
columns: Column[],
|
||||
fks: ForeignKey[],
|
||||
casing: Casing,
|
||||
): string => {
|
||||
let statement = '';
|
||||
|
||||
// no self refs and no cyclic
|
||||
const oneColumnsFKs = Object.values(fks)
|
||||
.filter((it) => {
|
||||
return !isSelf(it);
|
||||
})
|
||||
.filter((it) => it.columnsFrom.length === 1);
|
||||
|
||||
const fkByColumnName = oneColumnsFKs.reduce((res, it) => {
|
||||
const arr = res[it.columnsFrom[0]] || [];
|
||||
arr.push(it);
|
||||
res[it.columnsFrom[0]] = arr;
|
||||
return res;
|
||||
}, {} as Record<string, ForeignKey[]>);
|
||||
|
||||
columns.forEach((it) => {
|
||||
statement += '\t';
|
||||
statement += column(it.type, it.name, it.default, it.autoincrement, casing);
|
||||
statement += it.primaryKey
|
||||
? `.primaryKey(${it.autoincrement ? '{ autoIncrement: true }' : ''})`
|
||||
: '';
|
||||
statement += it.notNull ? '.notNull()' : '';
|
||||
|
||||
statement += it.generated
|
||||
? `.generatedAlwaysAs(sql\`${
|
||||
it.generated.as
|
||||
.replace(/`/g, '\\`')
|
||||
.slice(1, -1)
|
||||
}\`, { mode: "${it.generated.type}" })`
|
||||
: '';
|
||||
|
||||
const fks = fkByColumnName[it.name];
|
||||
if (fks) {
|
||||
const fksStatement = fks
|
||||
.map((it) => {
|
||||
const onDelete = it.onDelete && it.onDelete !== 'no action' ? it.onDelete : null;
|
||||
const onUpdate = it.onUpdate && it.onUpdate !== 'no action' ? it.onUpdate : null;
|
||||
const params = { onDelete, onUpdate };
|
||||
|
||||
const typeSuffix = isCyclic(it) ? ': AnySQLiteColumn' : '';
|
||||
|
||||
const paramsStr = objToStatement2(params);
|
||||
if (paramsStr) {
|
||||
return `.references(()${typeSuffix} => ${
|
||||
withCasing(
|
||||
it.tableTo,
|
||||
casing,
|
||||
)
|
||||
}.${withCasing(it.columnsTo[0], casing)}, ${paramsStr} )`;
|
||||
}
|
||||
return `.references(()${typeSuffix} => ${
|
||||
withCasing(
|
||||
it.tableTo,
|
||||
casing,
|
||||
)
|
||||
}.${withCasing(it.columnsTo[0], casing)})`;
|
||||
})
|
||||
.join('');
|
||||
statement += fksStatement;
|
||||
}
|
||||
|
||||
statement += ',\n';
|
||||
});
|
||||
|
||||
return statement;
|
||||
};
|
||||
|
||||
const createTableIndexes = (
|
||||
tableName: string,
|
||||
idxs: Index[],
|
||||
casing: Casing,
|
||||
): string => {
|
||||
let statement = '';
|
||||
|
||||
idxs.forEach((it) => {
|
||||
let idxKey = it.name.startsWith(tableName) && it.name !== tableName
|
||||
? it.name.slice(tableName.length + 1)
|
||||
: it.name;
|
||||
idxKey = idxKey.endsWith('_index')
|
||||
? idxKey.slice(0, -'_index'.length) + '_idx'
|
||||
: idxKey;
|
||||
|
||||
idxKey = withCasing(idxKey, casing);
|
||||
|
||||
const indexGeneratedName = indexName(tableName, it.columns);
|
||||
const escapedIndexName = indexGeneratedName === it.name ? '' : `"${it.name}"`;
|
||||
|
||||
statement += `\n\t`;
|
||||
statement += it.isUnique ? 'uniqueIndex(' : 'index(';
|
||||
statement += `${escapedIndexName})`;
|
||||
statement += `.on(${
|
||||
it.columns
|
||||
.map((it) => `table.${withCasing(it, casing)}`)
|
||||
.join(', ')
|
||||
}),`;
|
||||
});
|
||||
|
||||
return statement;
|
||||
};
|
||||
|
||||
const createTableUniques = (
|
||||
unqs: UniqueConstraint[],
|
||||
casing: Casing,
|
||||
): string => {
|
||||
let statement = '';
|
||||
|
||||
unqs.forEach((it) => {
|
||||
const idxKey = withCasing(it.name, casing);
|
||||
|
||||
statement += `\n\t`;
|
||||
statement += 'unique(';
|
||||
statement += `"${it.name}")`;
|
||||
statement += `.on(${
|
||||
it.columns
|
||||
.map((it) => `table.${withCasing(it, casing)}`)
|
||||
.join(', ')
|
||||
}),`;
|
||||
});
|
||||
|
||||
return statement;
|
||||
};
|
||||
const createTableChecks = (
|
||||
checks: CheckConstraint[],
|
||||
casing: Casing,
|
||||
): string => {
|
||||
let statement = '';
|
||||
|
||||
checks.forEach((it) => {
|
||||
statement += `\n\t`;
|
||||
statement += 'check(';
|
||||
statement += `"${it.name}", `;
|
||||
statement += `sql\`${it.value}\`)`;
|
||||
statement += `,`;
|
||||
});
|
||||
|
||||
return statement;
|
||||
};
|
||||
|
||||
const createTablePKs = (pks: PrimaryKey[], casing: Casing): string => {
|
||||
let statement = '';
|
||||
|
||||
pks.forEach((it, i) => {
|
||||
statement += `\n\t`;
|
||||
statement += 'primaryKey({ columns: [';
|
||||
statement += `${
|
||||
it.columns
|
||||
.map((c) => {
|
||||
return `table.${withCasing(c, casing)}`;
|
||||
})
|
||||
.join(', ')
|
||||
}]${it.name ? `, name: "${it.name}"` : ''}}`;
|
||||
statement += ')';
|
||||
});
|
||||
|
||||
return statement;
|
||||
};
|
||||
|
||||
const createTableFKs = (fks: ForeignKey[], casing: Casing): string => {
|
||||
let statement = '';
|
||||
|
||||
fks.forEach((it) => {
|
||||
const isSelf = it.tableTo === it.tableFrom;
|
||||
const tableTo = isSelf ? 'table' : `${withCasing(it.tableTo, casing)}`;
|
||||
statement += `\n\t`;
|
||||
statement += `foreignKey(() => ({\n`;
|
||||
statement += `\t\t\tcolumns: [${
|
||||
it.columnsFrom
|
||||
.map((i) => `table.${withCasing(i, casing)}`)
|
||||
.join(', ')
|
||||
}],\n`;
|
||||
statement += `\t\t\tforeignColumns: [${
|
||||
it.columnsTo
|
||||
.map((i) => `${tableTo}.${withCasing(i, casing)}`)
|
||||
.join(', ')
|
||||
}],\n`;
|
||||
statement += `\t\t\tname: "${it.name}"\n`;
|
||||
statement += `\t\t}))`;
|
||||
|
||||
statement += it.onUpdate && it.onUpdate !== 'no action'
|
||||
? `.onUpdate("${it.onUpdate}")`
|
||||
: '';
|
||||
|
||||
statement += it.onDelete && it.onDelete !== 'no action'
|
||||
? `.onDelete("${it.onDelete}")`
|
||||
: '';
|
||||
|
||||
statement += `,`;
|
||||
});
|
||||
|
||||
return statement;
|
||||
};
|
||||
@@ -0,0 +1,870 @@
|
||||
'use-strict';
|
||||
import { diff } from 'json-diff';
|
||||
|
||||
export function diffForRenamedTables(pairs) {
|
||||
// raname table1 to name of table2, so we can apply diffs
|
||||
const renamed = pairs.map((it) => {
|
||||
const from = it.from;
|
||||
const to = it.to;
|
||||
const newFrom = { ...from, name: to.name };
|
||||
return [newFrom, to];
|
||||
});
|
||||
|
||||
// find any alternations made to a renamed table
|
||||
const altered = renamed.map((pair) => {
|
||||
return diffForRenamedTable(pair[0], pair[1]);
|
||||
});
|
||||
|
||||
return altered;
|
||||
}
|
||||
|
||||
function diffForRenamedTable(t1, t2) {
|
||||
t1.name = t2.name;
|
||||
const diffed = diff(t1, t2) || {};
|
||||
diffed.name = t2.name;
|
||||
|
||||
return findAlternationsInTable(diffed, t2.schema);
|
||||
}
|
||||
|
||||
export function diffForRenamedColumn(t1, t2) {
|
||||
const renamed = { ...t1, name: t2.name };
|
||||
const diffed = diff(renamed, t2) || {};
|
||||
diffed.name = t2.name;
|
||||
|
||||
return alternationsInColumn(diffed);
|
||||
}
|
||||
|
||||
const update1to2 = (json) => {
|
||||
Object.entries(json).forEach(([key, val]) => {
|
||||
if ('object' !== typeof val) return;
|
||||
|
||||
if (val.hasOwnProperty('references')) {
|
||||
const ref = val['references'];
|
||||
const fkName = ref['foreignKeyName'];
|
||||
const table = ref['table'];
|
||||
const column = ref['column'];
|
||||
const onDelete = ref['onDelete'];
|
||||
const onUpdate = ref['onUpdate'];
|
||||
const newRef = `${fkName};${table};${column};${onDelete ?? ''};${onUpdate ?? ''}`;
|
||||
val['references'] = newRef;
|
||||
} else {
|
||||
update1to2(val);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const mapArraysDiff = (source, diff) => {
|
||||
const sequence = [];
|
||||
let sourceIndex = 0;
|
||||
for (let i = 0; i < diff.length; i++) {
|
||||
const it = diff[i];
|
||||
if (it.length === 1) {
|
||||
sequence.push({ type: 'same', value: source[sourceIndex] });
|
||||
sourceIndex += 1;
|
||||
} else {
|
||||
if (it[0] === '-') {
|
||||
sequence.push({ type: 'removed', value: it[1] });
|
||||
} else {
|
||||
sequence.push({ type: 'added', value: it[1], before: '' });
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = sequence.reverse().reduce(
|
||||
(acc, it) => {
|
||||
if (it.type === 'same') {
|
||||
acc.prev = it.value;
|
||||
}
|
||||
|
||||
if (it.type === 'added' && acc.prev) {
|
||||
it.before = acc.prev;
|
||||
}
|
||||
acc.result.push(it);
|
||||
return acc;
|
||||
},
|
||||
{ result: [] },
|
||||
);
|
||||
|
||||
return result.result.reverse();
|
||||
};
|
||||
|
||||
export function diffSchemasOrTables(left, right) {
|
||||
left = JSON.parse(JSON.stringify(left));
|
||||
right = JSON.parse(JSON.stringify(right));
|
||||
|
||||
const result = Object.entries(diff(left, right) ?? {});
|
||||
|
||||
const added = result
|
||||
.filter((it) => it[0].endsWith('__added'))
|
||||
.map((it) => it[1]);
|
||||
const deleted = result
|
||||
.filter((it) => it[0].endsWith('__deleted'))
|
||||
.map((it) => it[1]);
|
||||
|
||||
return { added, deleted };
|
||||
}
|
||||
|
||||
export function diffIndPolicies(left, right) {
|
||||
left = JSON.parse(JSON.stringify(left));
|
||||
right = JSON.parse(JSON.stringify(right));
|
||||
|
||||
const result = Object.entries(diff(left, right) ?? {});
|
||||
|
||||
const added = result
|
||||
.filter((it) => it[0].endsWith('__added'))
|
||||
.map((it) => it[1]);
|
||||
const deleted = result
|
||||
.filter((it) => it[0].endsWith('__deleted'))
|
||||
.map((it) => it[1]);
|
||||
|
||||
return { added, deleted };
|
||||
}
|
||||
|
||||
export function diffColumns(left, right) {
|
||||
left = JSON.parse(JSON.stringify(left));
|
||||
right = JSON.parse(JSON.stringify(right));
|
||||
const result = diff(left, right) ?? {};
|
||||
|
||||
const alteredTables = Object.fromEntries(
|
||||
Object.entries(result)
|
||||
.filter((it) => {
|
||||
return !(it[0].includes('__added') || it[0].includes('__deleted'));
|
||||
})
|
||||
.map((tableEntry) => {
|
||||
// const entry = { name: it, ...result[it] }
|
||||
const deletedColumns = Object.entries(tableEntry[1].columns ?? {})
|
||||
.filter((it) => {
|
||||
return it[0].endsWith('__deleted');
|
||||
})
|
||||
.map((it) => {
|
||||
return it[1];
|
||||
});
|
||||
|
||||
const addedColumns = Object.entries(tableEntry[1].columns ?? {})
|
||||
.filter((it) => {
|
||||
return it[0].endsWith('__added');
|
||||
})
|
||||
.map((it) => {
|
||||
return it[1];
|
||||
});
|
||||
|
||||
tableEntry[1].columns = {
|
||||
added: addedColumns,
|
||||
deleted: deletedColumns,
|
||||
};
|
||||
const table = left[tableEntry[0]];
|
||||
return [
|
||||
tableEntry[0],
|
||||
{ name: table.name, schema: table.schema, ...tableEntry[1] },
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
return alteredTables;
|
||||
}
|
||||
|
||||
export function diffPolicies(left, right) {
|
||||
left = JSON.parse(JSON.stringify(left));
|
||||
right = JSON.parse(JSON.stringify(right));
|
||||
const result = diff(left, right) ?? {};
|
||||
|
||||
const alteredTables = Object.fromEntries(
|
||||
Object.entries(result)
|
||||
.filter((it) => {
|
||||
return !(it[0].includes('__added') || it[0].includes('__deleted'));
|
||||
})
|
||||
.map((tableEntry) => {
|
||||
// const entry = { name: it, ...result[it] }
|
||||
const deletedPolicies = Object.entries(tableEntry[1].policies ?? {})
|
||||
.filter((it) => {
|
||||
return it[0].endsWith('__deleted');
|
||||
})
|
||||
.map((it) => {
|
||||
return it[1];
|
||||
});
|
||||
|
||||
const addedPolicies = Object.entries(tableEntry[1].policies ?? {})
|
||||
.filter((it) => {
|
||||
return it[0].endsWith('__added');
|
||||
})
|
||||
.map((it) => {
|
||||
return it[1];
|
||||
});
|
||||
|
||||
tableEntry[1].policies = {
|
||||
added: addedPolicies,
|
||||
deleted: deletedPolicies,
|
||||
};
|
||||
const table = left[tableEntry[0]];
|
||||
return [
|
||||
tableEntry[0],
|
||||
{ name: table.name, schema: table.schema, ...tableEntry[1] },
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
return alteredTables;
|
||||
}
|
||||
|
||||
export function applyJsonDiff(json1, json2) {
|
||||
json1 = JSON.parse(JSON.stringify(json1));
|
||||
json2 = JSON.parse(JSON.stringify(json2));
|
||||
|
||||
// deep copy, needed because of the bug in diff library
|
||||
const rawDiff = diff(json1, json2);
|
||||
|
||||
const difference = JSON.parse(JSON.stringify(rawDiff || {}));
|
||||
difference.schemas = difference.schemas || {};
|
||||
difference.tables = difference.tables || {};
|
||||
difference.enums = difference.enums || {};
|
||||
difference.sequences = difference.sequences || {};
|
||||
difference.roles = difference.roles || {};
|
||||
difference.policies = difference.policies || {};
|
||||
difference.views = difference.views || {};
|
||||
|
||||
// remove added/deleted schemas
|
||||
const schemaKeys = Object.keys(difference.schemas);
|
||||
for (let key of schemaKeys) {
|
||||
if (key.endsWith('__added') || key.endsWith('__deleted')) {
|
||||
delete difference.schemas[key];
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// remove added/deleted tables
|
||||
const tableKeys = Object.keys(difference.tables);
|
||||
for (let key of tableKeys) {
|
||||
if (key.endsWith('__added') || key.endsWith('__deleted')) {
|
||||
delete difference.tables[key];
|
||||
continue;
|
||||
}
|
||||
|
||||
// supply table name and schema for altered tables
|
||||
const table = json1.tables[key];
|
||||
difference.tables[key] = {
|
||||
name: table.name,
|
||||
schema: table.schema,
|
||||
...difference.tables[key],
|
||||
};
|
||||
}
|
||||
|
||||
for (let [tableKey, tableValue] of Object.entries(difference.tables)) {
|
||||
const table = difference.tables[tableKey];
|
||||
const columns = tableValue.columns || {};
|
||||
const columnKeys = Object.keys(columns);
|
||||
for (let key of columnKeys) {
|
||||
if (key.endsWith('__added') || key.endsWith('__deleted')) {
|
||||
delete table.columns[key];
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(columns).length === 0) {
|
||||
delete table['columns'];
|
||||
}
|
||||
|
||||
if (
|
||||
'name' in table
|
||||
&& 'schema' in table
|
||||
&& Object.keys(table).length === 2
|
||||
) {
|
||||
delete difference.tables[tableKey];
|
||||
}
|
||||
}
|
||||
|
||||
const enumsEntries = Object.entries(difference.enums);
|
||||
const alteredEnums = enumsEntries
|
||||
.filter((it) => !(it[0].includes('__added') || it[0].includes('__deleted')))
|
||||
.map((it) => {
|
||||
const enumEntry = json1.enums[it[0]];
|
||||
const { name, schema, values } = enumEntry;
|
||||
|
||||
const sequence = mapArraysDiff(values, it[1].values);
|
||||
const addedValues = sequence
|
||||
.filter((it) => it.type === 'added')
|
||||
.map((it) => {
|
||||
return {
|
||||
before: it.before,
|
||||
value: it.value,
|
||||
};
|
||||
});
|
||||
const deletedValues = sequence
|
||||
.filter((it) => it.type === 'removed')
|
||||
.map((it) => it.value);
|
||||
|
||||
return { name, schema, addedValues, deletedValues };
|
||||
});
|
||||
|
||||
const sequencesEntries = Object.entries(difference.sequences);
|
||||
const alteredSequences = sequencesEntries
|
||||
.filter((it) => !(it[0].includes('__added') || it[0].includes('__deleted')) && 'values' in it[1])
|
||||
.map((it) => {
|
||||
return json2.sequences[it[0]];
|
||||
});
|
||||
|
||||
const rolesEntries = Object.entries(difference.roles);
|
||||
const alteredRoles = rolesEntries
|
||||
.filter((it) => !(it[0].includes('__added') || it[0].includes('__deleted')))
|
||||
.map((it) => {
|
||||
return json2.roles[it[0]];
|
||||
});
|
||||
|
||||
const policiesEntries = Object.entries(difference.policies);
|
||||
const alteredPolicies = policiesEntries
|
||||
.filter((it) => !(it[0].includes('__added') || it[0].includes('__deleted')))
|
||||
.map((it) => {
|
||||
return json2.policies[it[0]];
|
||||
});
|
||||
|
||||
const viewsEntries = Object.entries(difference.views);
|
||||
|
||||
const alteredViews = viewsEntries.filter((it) => !(it[0].includes('__added') || it[0].includes('__deleted'))).map(
|
||||
([nameWithSchema, view]) => {
|
||||
const deletedWithOption = view.with__deleted;
|
||||
|
||||
const addedWithOption = view.with__added;
|
||||
|
||||
const deletedWith = Object.fromEntries(
|
||||
Object.entries(view.with || {}).filter((it) => it[0].endsWith('__deleted')).map(([key, value]) => {
|
||||
return [key.replace('__deleted', ''), value];
|
||||
}),
|
||||
);
|
||||
|
||||
const addedWith = Object.fromEntries(
|
||||
Object.entries(view.with || {}).filter((it) => it[0].endsWith('__added')).map(([key, value]) => {
|
||||
return [key.replace('__added', ''), value];
|
||||
}),
|
||||
);
|
||||
|
||||
const alterWith = Object.fromEntries(
|
||||
Object.entries(view.with || {}).filter((it) =>
|
||||
typeof it[1].__old !== 'undefined' && typeof it[1].__new !== 'undefined'
|
||||
).map(
|
||||
(it) => {
|
||||
return [it[0], it[1].__new];
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const alteredSchema = view.schema;
|
||||
|
||||
const alteredDefinition = view.definition;
|
||||
|
||||
const alteredExisting = view.isExisting;
|
||||
|
||||
const addedTablespace = view.tablespace__added;
|
||||
const droppedTablespace = view.tablespace__deleted;
|
||||
const alterTablespaceTo = view.tablespace;
|
||||
|
||||
let alteredTablespace;
|
||||
if (addedTablespace) alteredTablespace = { __new: addedTablespace, __old: 'pg_default' };
|
||||
if (droppedTablespace) alteredTablespace = { __new: 'pg_default', __old: droppedTablespace };
|
||||
if (alterTablespaceTo) alteredTablespace = alterTablespaceTo;
|
||||
|
||||
const addedUsing = view.using__added;
|
||||
const droppedUsing = view.using__deleted;
|
||||
const alterUsingTo = view.using;
|
||||
|
||||
let alteredUsing;
|
||||
if (addedUsing) alteredUsing = { __new: addedUsing, __old: 'heap' };
|
||||
if (droppedUsing) alteredUsing = { __new: 'heap', __old: droppedUsing };
|
||||
if (alterUsingTo) alteredUsing = alterUsingTo;
|
||||
|
||||
const alteredMeta = view.meta;
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries({
|
||||
name: json2.views[nameWithSchema].name,
|
||||
schema: json2.views[nameWithSchema].schema,
|
||||
// pg
|
||||
deletedWithOption: deletedWithOption,
|
||||
addedWithOption: addedWithOption,
|
||||
deletedWith: Object.keys(deletedWith).length ? deletedWith : undefined,
|
||||
addedWith: Object.keys(addedWith).length ? addedWith : undefined,
|
||||
alteredWith: Object.keys(alterWith).length ? alterWith : undefined,
|
||||
alteredSchema,
|
||||
alteredTablespace,
|
||||
alteredUsing,
|
||||
// mysql
|
||||
alteredMeta,
|
||||
// common
|
||||
alteredDefinition,
|
||||
alteredExisting,
|
||||
}).filter(([_, value]) => value !== undefined),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const alteredTablesWithColumns = Object.values(difference.tables).map(
|
||||
(table) => {
|
||||
return findAlternationsInTable(table);
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
alteredTablesWithColumns,
|
||||
alteredEnums,
|
||||
alteredSequences,
|
||||
alteredRoles,
|
||||
alteredViews,
|
||||
alteredPolicies,
|
||||
};
|
||||
}
|
||||
|
||||
const findAlternationsInTable = (table) => {
|
||||
// map each table to have altered, deleted or renamed columns
|
||||
|
||||
// in case no columns were altered, but indexes were
|
||||
const columns = table.columns ?? {};
|
||||
|
||||
const altered = Object.keys(columns)
|
||||
.filter((it) => !(it.includes('__deleted') || it.includes('__added')))
|
||||
.map((it) => {
|
||||
return { name: it, ...columns[it] };
|
||||
});
|
||||
|
||||
const deletedIndexes = Object.fromEntries(
|
||||
Object.entries(table.indexes__deleted || {})
|
||||
.concat(
|
||||
Object.entries(table.indexes || {}).filter((it) => it[0].includes('__deleted')),
|
||||
)
|
||||
.map((entry) => [entry[0].replace('__deleted', ''), entry[1]]),
|
||||
);
|
||||
|
||||
const addedIndexes = Object.fromEntries(
|
||||
Object.entries(table.indexes__added || {})
|
||||
.concat(
|
||||
Object.entries(table.indexes || {}).filter((it) => it[0].includes('__added')),
|
||||
)
|
||||
.map((entry) => [entry[0].replace('__added', ''), entry[1]]),
|
||||
);
|
||||
|
||||
const alteredIndexes = Object.fromEntries(
|
||||
Object.entries(table.indexes || {}).filter((it) => {
|
||||
return !it[0].endsWith('__deleted') && !it[0].endsWith('__added');
|
||||
}),
|
||||
);
|
||||
|
||||
const deletedPolicies = Object.fromEntries(
|
||||
Object.entries(table.policies__deleted || {})
|
||||
.concat(
|
||||
Object.entries(table.policies || {}).filter((it) => it[0].includes('__deleted')),
|
||||
)
|
||||
.map((entry) => [entry[0].replace('__deleted', ''), entry[1]]),
|
||||
);
|
||||
|
||||
const addedPolicies = Object.fromEntries(
|
||||
Object.entries(table.policies__added || {})
|
||||
.concat(
|
||||
Object.entries(table.policies || {}).filter((it) => it[0].includes('__added')),
|
||||
)
|
||||
.map((entry) => [entry[0].replace('__added', ''), entry[1]]),
|
||||
);
|
||||
|
||||
const alteredPolicies = Object.fromEntries(
|
||||
Object.entries(table.policies || {}).filter((it) => {
|
||||
return !it[0].endsWith('__deleted') && !it[0].endsWith('__added');
|
||||
}),
|
||||
);
|
||||
|
||||
const deletedForeignKeys = Object.fromEntries(
|
||||
Object.entries(table.foreignKeys__deleted || {})
|
||||
.concat(
|
||||
Object.entries(table.foreignKeys || {}).filter((it) => it[0].includes('__deleted')),
|
||||
)
|
||||
.map((entry) => [entry[0].replace('__deleted', ''), entry[1]]),
|
||||
);
|
||||
|
||||
const addedForeignKeys = Object.fromEntries(
|
||||
Object.entries(table.foreignKeys__added || {})
|
||||
.concat(
|
||||
Object.entries(table.foreignKeys || {}).filter((it) => it[0].includes('__added')),
|
||||
)
|
||||
.map((entry) => [entry[0].replace('__added', ''), entry[1]]),
|
||||
);
|
||||
|
||||
const alteredForeignKeys = Object.fromEntries(
|
||||
Object.entries(table.foreignKeys || {})
|
||||
.filter(
|
||||
(it) => !it[0].endsWith('__added') && !it[0].endsWith('__deleted'),
|
||||
)
|
||||
.map((entry) => [entry[0], entry[1]]),
|
||||
);
|
||||
|
||||
const addedCompositePKs = Object.fromEntries(
|
||||
Object.entries(table.compositePrimaryKeys || {}).filter((it) => {
|
||||
return it[0].endsWith('__added');
|
||||
}),
|
||||
);
|
||||
|
||||
const deletedCompositePKs = Object.fromEntries(
|
||||
Object.entries(table.compositePrimaryKeys || {}).filter((it) => {
|
||||
return it[0].endsWith('__deleted');
|
||||
}),
|
||||
);
|
||||
|
||||
const alteredCompositePKs = Object.fromEntries(
|
||||
Object.entries(table.compositePrimaryKeys || {}).filter((it) => {
|
||||
return !it[0].endsWith('__deleted') && !it[0].endsWith('__added');
|
||||
}),
|
||||
);
|
||||
|
||||
const addedUniqueConstraints = Object.fromEntries(
|
||||
Object.entries(table.uniqueConstraints || {}).filter((it) => {
|
||||
return it[0].endsWith('__added');
|
||||
}),
|
||||
);
|
||||
|
||||
const deletedUniqueConstraints = Object.fromEntries(
|
||||
Object.entries(table.uniqueConstraints || {}).filter((it) => {
|
||||
return it[0].endsWith('__deleted');
|
||||
}),
|
||||
);
|
||||
|
||||
const alteredUniqueConstraints = Object.fromEntries(
|
||||
Object.entries(table.uniqueConstraints || {}).filter((it) => {
|
||||
return !it[0].endsWith('__deleted') && !it[0].endsWith('__added');
|
||||
}),
|
||||
);
|
||||
|
||||
const addedCheckConstraints = Object.fromEntries(
|
||||
Object.entries(table.checkConstraints || {}).filter((it) => {
|
||||
return it[0].endsWith('__added');
|
||||
}),
|
||||
);
|
||||
|
||||
const deletedCheckConstraints = Object.fromEntries(
|
||||
Object.entries(table.checkConstraints || {}).filter((it) => {
|
||||
return it[0].endsWith('__deleted');
|
||||
}),
|
||||
);
|
||||
|
||||
const alteredCheckConstraints = Object.fromEntries(
|
||||
Object.entries(table.checkConstraints || {}).filter((it) => {
|
||||
return !it[0].endsWith('__deleted') && !it[0].endsWith('__added');
|
||||
}),
|
||||
);
|
||||
|
||||
const mappedAltered = altered.map((it) => alternationsInColumn(it)).filter(Boolean);
|
||||
|
||||
return {
|
||||
name: table.name,
|
||||
schema: table.schema || '',
|
||||
altered: mappedAltered,
|
||||
addedIndexes,
|
||||
deletedIndexes,
|
||||
alteredIndexes,
|
||||
addedForeignKeys,
|
||||
deletedForeignKeys,
|
||||
alteredForeignKeys,
|
||||
addedCompositePKs,
|
||||
deletedCompositePKs,
|
||||
alteredCompositePKs,
|
||||
addedUniqueConstraints,
|
||||
deletedUniqueConstraints,
|
||||
alteredUniqueConstraints,
|
||||
deletedPolicies,
|
||||
addedPolicies,
|
||||
alteredPolicies,
|
||||
addedCheckConstraints,
|
||||
deletedCheckConstraints,
|
||||
alteredCheckConstraints,
|
||||
};
|
||||
};
|
||||
|
||||
const alternationsInColumn = (column) => {
|
||||
const altered = [column];
|
||||
|
||||
const result = altered
|
||||
.filter((it) => {
|
||||
if ('type' in it && it.type.__old.replace(' (', '(') === it.type.__new.replace(' (', '(')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((it) => {
|
||||
if (typeof it.name !== 'string' && '__old' in it.name) {
|
||||
// rename
|
||||
return {
|
||||
...it,
|
||||
name: { type: 'changed', old: it.name.__old, new: it.name.__new },
|
||||
};
|
||||
}
|
||||
return it;
|
||||
})
|
||||
.map((it) => {
|
||||
if ('type' in it) {
|
||||
// type change
|
||||
return {
|
||||
...it,
|
||||
type: { type: 'changed', old: it.type.__old, new: it.type.__new },
|
||||
};
|
||||
}
|
||||
return it;
|
||||
})
|
||||
.map((it) => {
|
||||
if ('default' in it) {
|
||||
return {
|
||||
...it,
|
||||
default: {
|
||||
type: 'changed',
|
||||
old: it.default.__old,
|
||||
new: it.default.__new,
|
||||
},
|
||||
};
|
||||
}
|
||||
if ('default__added' in it) {
|
||||
const { default__added, ...others } = it;
|
||||
return {
|
||||
...others,
|
||||
default: { type: 'added', value: it.default__added },
|
||||
};
|
||||
}
|
||||
if ('default__deleted' in it) {
|
||||
const { default__deleted, ...others } = it;
|
||||
return {
|
||||
...others,
|
||||
default: { type: 'deleted', value: it.default__deleted },
|
||||
};
|
||||
}
|
||||
return it;
|
||||
})
|
||||
.map((it) => {
|
||||
if ('generated' in it) {
|
||||
if ('as' in it.generated && 'type' in it.generated) {
|
||||
return {
|
||||
...it,
|
||||
generated: {
|
||||
type: 'changed',
|
||||
old: { as: it.generated.as.__old, type: it.generated.type.__old },
|
||||
new: { as: it.generated.as.__new, type: it.generated.type.__new },
|
||||
},
|
||||
};
|
||||
} else if ('as' in it.generated) {
|
||||
return {
|
||||
...it,
|
||||
generated: {
|
||||
type: 'changed',
|
||||
old: { as: it.generated.as.__old },
|
||||
new: { as: it.generated.as.__new },
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...it,
|
||||
generated: {
|
||||
type: 'changed',
|
||||
old: { as: it.generated.type.__old },
|
||||
new: { as: it.generated.type.__new },
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
if ('generated__added' in it) {
|
||||
const { generated__added, ...others } = it;
|
||||
return {
|
||||
...others,
|
||||
generated: { type: 'added', value: it.generated__added },
|
||||
};
|
||||
}
|
||||
if ('generated__deleted' in it) {
|
||||
const { generated__deleted, ...others } = it;
|
||||
return {
|
||||
...others,
|
||||
generated: { type: 'deleted', value: it.generated__deleted },
|
||||
};
|
||||
}
|
||||
return it;
|
||||
})
|
||||
.map((it) => {
|
||||
if ('identity' in it) {
|
||||
return {
|
||||
...it,
|
||||
identity: {
|
||||
type: 'changed',
|
||||
old: it.identity.__old,
|
||||
new: it.identity.__new,
|
||||
},
|
||||
};
|
||||
}
|
||||
if ('identity__added' in it) {
|
||||
const { identity__added, ...others } = it;
|
||||
return {
|
||||
...others,
|
||||
identity: { type: 'added', value: it.identity__added },
|
||||
};
|
||||
}
|
||||
if ('identity__deleted' in it) {
|
||||
const { identity__deleted, ...others } = it;
|
||||
return {
|
||||
...others,
|
||||
identity: { type: 'deleted', value: it.identity__deleted },
|
||||
};
|
||||
}
|
||||
return it;
|
||||
})
|
||||
.map((it) => {
|
||||
if ('notNull' in it) {
|
||||
return {
|
||||
...it,
|
||||
notNull: {
|
||||
type: 'changed',
|
||||
old: it.notNull.__old,
|
||||
new: it.notNull.__new,
|
||||
},
|
||||
};
|
||||
}
|
||||
if ('notNull__added' in it) {
|
||||
const { notNull__added, ...others } = it;
|
||||
return {
|
||||
...others,
|
||||
notNull: { type: 'added', value: it.notNull__added },
|
||||
};
|
||||
}
|
||||
if ('notNull__deleted' in it) {
|
||||
const { notNull__deleted, ...others } = it;
|
||||
return {
|
||||
...others,
|
||||
notNull: { type: 'deleted', value: it.notNull__deleted },
|
||||
};
|
||||
}
|
||||
return it;
|
||||
})
|
||||
.map((it) => {
|
||||
if ('primaryKey' in it) {
|
||||
return {
|
||||
...it,
|
||||
primaryKey: {
|
||||
type: 'changed',
|
||||
old: it.primaryKey.__old,
|
||||
new: it.primaryKey.__new,
|
||||
},
|
||||
};
|
||||
}
|
||||
if ('primaryKey__added' in it) {
|
||||
const { notNull__added, ...others } = it;
|
||||
return {
|
||||
...others,
|
||||
primaryKey: { type: 'added', value: it.primaryKey__added },
|
||||
};
|
||||
}
|
||||
if ('primaryKey__deleted' in it) {
|
||||
const { notNull__deleted, ...others } = it;
|
||||
return {
|
||||
...others,
|
||||
primaryKey: { type: 'deleted', value: it.primaryKey__deleted },
|
||||
};
|
||||
}
|
||||
return it;
|
||||
})
|
||||
.map((it) => {
|
||||
if ('typeSchema' in it) {
|
||||
return {
|
||||
...it,
|
||||
typeSchema: {
|
||||
type: 'changed',
|
||||
old: it.typeSchema.__old,
|
||||
new: it.typeSchema.__new,
|
||||
},
|
||||
};
|
||||
}
|
||||
if ('typeSchema__added' in it) {
|
||||
const { typeSchema__added, ...others } = it;
|
||||
return {
|
||||
...others,
|
||||
typeSchema: { type: 'added', value: it.typeSchema__added },
|
||||
};
|
||||
}
|
||||
if ('typeSchema__deleted' in it) {
|
||||
const { typeSchema__deleted, ...others } = it;
|
||||
return {
|
||||
...others,
|
||||
typeSchema: { type: 'deleted', value: it.typeSchema__deleted },
|
||||
};
|
||||
}
|
||||
return it;
|
||||
})
|
||||
.map((it) => {
|
||||
if ('onUpdate' in it) {
|
||||
return {
|
||||
...it,
|
||||
onUpdate: {
|
||||
type: 'changed',
|
||||
old: it.onUpdate.__old,
|
||||
new: it.onUpdate.__new,
|
||||
},
|
||||
};
|
||||
}
|
||||
if ('onUpdate__added' in it) {
|
||||
const { onUpdate__added, ...others } = it;
|
||||
return {
|
||||
...others,
|
||||
onUpdate: { type: 'added', value: it.onUpdate__added },
|
||||
};
|
||||
}
|
||||
if ('onUpdate__deleted' in it) {
|
||||
const { onUpdate__deleted, ...others } = it;
|
||||
return {
|
||||
...others,
|
||||
onUpdate: { type: 'deleted', value: it.onUpdate__deleted },
|
||||
};
|
||||
}
|
||||
return it;
|
||||
})
|
||||
.map((it) => {
|
||||
if ('autoincrement' in it) {
|
||||
return {
|
||||
...it,
|
||||
autoincrement: {
|
||||
type: 'changed',
|
||||
old: it.autoincrement.__old,
|
||||
new: it.autoincrement.__new,
|
||||
},
|
||||
};
|
||||
}
|
||||
if ('autoincrement__added' in it) {
|
||||
const { autoincrement__added, ...others } = it;
|
||||
return {
|
||||
...others,
|
||||
autoincrement: { type: 'added', value: it.autoincrement__added },
|
||||
};
|
||||
}
|
||||
if ('autoincrement__deleted' in it) {
|
||||
const { autoincrement__deleted, ...others } = it;
|
||||
return {
|
||||
...others,
|
||||
autoincrement: { type: 'deleted', value: it.autoincrement__deleted },
|
||||
};
|
||||
}
|
||||
return it;
|
||||
})
|
||||
.map((it) => {
|
||||
if ('' in it) {
|
||||
return {
|
||||
...it,
|
||||
autoincrement: {
|
||||
type: 'changed',
|
||||
old: it.autoincrement.__old,
|
||||
new: it.autoincrement.__new,
|
||||
},
|
||||
};
|
||||
}
|
||||
if ('autoincrement__added' in it) {
|
||||
const { autoincrement__added, ...others } = it;
|
||||
return {
|
||||
...others,
|
||||
autoincrement: { type: 'added', value: it.autoincrement__added },
|
||||
};
|
||||
}
|
||||
if ('autoincrement__deleted' in it) {
|
||||
const { autoincrement__deleted, ...others } = it;
|
||||
return {
|
||||
...others,
|
||||
autoincrement: { type: 'deleted', value: it.autoincrement__deleted },
|
||||
};
|
||||
}
|
||||
return it;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return result[0];
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,57 @@
|
||||
import esbuild from 'esbuild';
|
||||
import { readFileSync } from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const parse = (it) => {
|
||||
if (!it) return { drizzle: false };
|
||||
|
||||
if (it.endsWith('__drizzle__')) {
|
||||
const offset = it.startsWith('file://') ? 'file://'.length : 0;
|
||||
const clean = it.slice(offset, -'__drizzle__'.length);
|
||||
return { drizzle: true, clean, original: it };
|
||||
}
|
||||
return { drizzle: false, clean: it };
|
||||
};
|
||||
|
||||
export function resolve(specifier, context, nextResolve) {
|
||||
const { drizzle, clean } = parse(specifier);
|
||||
if (drizzle && !clean.endsWith('.ts') && !clean.endsWith('.mts')) {
|
||||
return nextResolve(clean);
|
||||
}
|
||||
|
||||
if (drizzle) {
|
||||
return {
|
||||
shortCircuit: true,
|
||||
url: `file://${specifier}`,
|
||||
};
|
||||
}
|
||||
|
||||
const parsedParent = parse(context.parentURL);
|
||||
const parentURL = parsedParent.drizzle
|
||||
? new URL(`file://${path.resolve(parsedParent.clean)}`)
|
||||
: context.parentURL;
|
||||
|
||||
// Let Node.js handle all other specifiers.
|
||||
return nextResolve(specifier, { ...context, parentURL });
|
||||
}
|
||||
|
||||
export async function load(url, context, defaultLoad) {
|
||||
const { drizzle, clean } = parse(url);
|
||||
if (drizzle) {
|
||||
const file = readFileSync(clean, 'utf-8');
|
||||
if (clean.endsWith('.ts') || clean.endsWith('.mts')) {
|
||||
const source = esbuild.transformSync(file, {
|
||||
loader: 'ts',
|
||||
format: 'esm',
|
||||
});
|
||||
return {
|
||||
format: 'module',
|
||||
shortCircuit: true,
|
||||
source: source.code,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// let Node.js handle all other URLs
|
||||
return defaultLoad(url, context, defaultLoad);
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
import fs from 'fs';
|
||||
import { CasingType } from './cli/validations/common';
|
||||
import { serializeMySql, serializePg, serializeSingleStore, serializeSQLite } from './serializer';
|
||||
import { dryMySql, MySqlSchema, mysqlSchema } from './serializer/mysqlSchema';
|
||||
import { dryPg, PgSchema, pgSchema } from './serializer/pgSchema';
|
||||
import { drySingleStore, SingleStoreSchema, singlestoreSchema } from './serializer/singlestoreSchema';
|
||||
import { drySQLite, SQLiteSchema, sqliteSchema } from './serializer/sqliteSchema';
|
||||
|
||||
export const prepareMySqlDbPushSnapshot = async (
|
||||
prev: MySqlSchema,
|
||||
schemaPath: string | string[],
|
||||
casing: CasingType | undefined,
|
||||
): Promise<{ prev: MySqlSchema; cur: MySqlSchema }> => {
|
||||
const serialized = await serializeMySql(schemaPath, casing);
|
||||
|
||||
const id = randomUUID();
|
||||
const idPrev = prev.id;
|
||||
|
||||
const { version, dialect, ...rest } = serialized;
|
||||
const result: MySqlSchema = { version, dialect, id, prevId: idPrev, ...rest };
|
||||
|
||||
return { prev, cur: result };
|
||||
};
|
||||
|
||||
export const prepareSingleStoreDbPushSnapshot = async (
|
||||
prev: SingleStoreSchema,
|
||||
schemaPath: string | string[],
|
||||
casing: CasingType | undefined,
|
||||
): Promise<{ prev: SingleStoreSchema; cur: SingleStoreSchema }> => {
|
||||
const serialized = await serializeSingleStore(schemaPath, casing);
|
||||
|
||||
const id = randomUUID();
|
||||
const idPrev = prev.id;
|
||||
|
||||
const { version, dialect, ...rest } = serialized;
|
||||
const result: SingleStoreSchema = { version, dialect, id, prevId: idPrev, ...rest };
|
||||
|
||||
return { prev, cur: result };
|
||||
};
|
||||
|
||||
export const prepareSQLiteDbPushSnapshot = async (
|
||||
prev: SQLiteSchema,
|
||||
schemaPath: string | string[],
|
||||
casing: CasingType | undefined,
|
||||
): Promise<{ prev: SQLiteSchema; cur: SQLiteSchema }> => {
|
||||
const serialized = await serializeSQLite(schemaPath, casing);
|
||||
|
||||
const id = randomUUID();
|
||||
const idPrev = prev.id;
|
||||
|
||||
const { version, dialect, ...rest } = serialized;
|
||||
const result: SQLiteSchema = {
|
||||
version,
|
||||
dialect,
|
||||
id,
|
||||
prevId: idPrev,
|
||||
...rest,
|
||||
};
|
||||
|
||||
return { prev, cur: result };
|
||||
};
|
||||
|
||||
export const preparePgDbPushSnapshot = async (
|
||||
prev: PgSchema,
|
||||
schemaPath: string | string[],
|
||||
casing: CasingType | undefined,
|
||||
schemaFilter: string[] = ['public'],
|
||||
): Promise<{ prev: PgSchema; cur: PgSchema }> => {
|
||||
const serialized = await serializePg(schemaPath, casing, schemaFilter);
|
||||
|
||||
const id = randomUUID();
|
||||
const idPrev = prev.id;
|
||||
|
||||
const { version, dialect, ...rest } = serialized;
|
||||
const result: PgSchema = { version, dialect, id, prevId: idPrev, ...rest };
|
||||
|
||||
return { prev, cur: result };
|
||||
};
|
||||
|
||||
export const prepareMySqlMigrationSnapshot = async (
|
||||
migrationFolders: string[],
|
||||
schemaPath: string | string[],
|
||||
casing: CasingType | undefined,
|
||||
): Promise<{ prev: MySqlSchema; cur: MySqlSchema; custom: MySqlSchema }> => {
|
||||
const prevSnapshot = mysqlSchema.parse(
|
||||
preparePrevSnapshot(migrationFolders, dryMySql),
|
||||
);
|
||||
const serialized = await serializeMySql(schemaPath, casing);
|
||||
|
||||
const id = randomUUID();
|
||||
const idPrev = prevSnapshot.id;
|
||||
|
||||
const { version, dialect, ...rest } = serialized;
|
||||
const result: MySqlSchema = { version, dialect, id, prevId: idPrev, ...rest };
|
||||
|
||||
const { id: _ignoredId, prevId: _ignoredPrevId, ...prevRest } = prevSnapshot;
|
||||
|
||||
// that's for custom migrations, when we need new IDs, but old snapshot
|
||||
const custom: MySqlSchema = {
|
||||
id,
|
||||
prevId: idPrev,
|
||||
...prevRest,
|
||||
};
|
||||
|
||||
return { prev: prevSnapshot, cur: result, custom };
|
||||
};
|
||||
|
||||
export const prepareSingleStoreMigrationSnapshot = async (
|
||||
migrationFolders: string[],
|
||||
schemaPath: string | string[],
|
||||
casing: CasingType | undefined,
|
||||
): Promise<{ prev: SingleStoreSchema; cur: SingleStoreSchema; custom: SingleStoreSchema }> => {
|
||||
const prevSnapshot = singlestoreSchema.parse(
|
||||
preparePrevSnapshot(migrationFolders, drySingleStore),
|
||||
);
|
||||
const serialized = await serializeSingleStore(schemaPath, casing);
|
||||
|
||||
const id = randomUUID();
|
||||
const idPrev = prevSnapshot.id;
|
||||
|
||||
const { version, dialect, ...rest } = serialized;
|
||||
const result: SingleStoreSchema = { version, dialect, id, prevId: idPrev, ...rest };
|
||||
|
||||
const { id: _ignoredId, prevId: _ignoredPrevId, ...prevRest } = prevSnapshot;
|
||||
|
||||
// that's for custom migrations, when we need new IDs, but old snapshot
|
||||
const custom: SingleStoreSchema = {
|
||||
id,
|
||||
prevId: idPrev,
|
||||
...prevRest,
|
||||
};
|
||||
|
||||
return { prev: prevSnapshot, cur: result, custom };
|
||||
};
|
||||
|
||||
export const prepareSqliteMigrationSnapshot = async (
|
||||
snapshots: string[],
|
||||
schemaPath: string | string[],
|
||||
casing: CasingType | undefined,
|
||||
): Promise<{ prev: SQLiteSchema; cur: SQLiteSchema; custom: SQLiteSchema }> => {
|
||||
const prevSnapshot = sqliteSchema.parse(
|
||||
preparePrevSnapshot(snapshots, drySQLite),
|
||||
);
|
||||
const serialized = await serializeSQLite(schemaPath, casing);
|
||||
|
||||
const id = randomUUID();
|
||||
const idPrev = prevSnapshot.id;
|
||||
|
||||
const { version, dialect, ...rest } = serialized;
|
||||
const result: SQLiteSchema = {
|
||||
version,
|
||||
dialect,
|
||||
id,
|
||||
prevId: idPrev,
|
||||
...rest,
|
||||
};
|
||||
|
||||
const { id: _ignoredId, prevId: _ignoredPrevId, ...prevRest } = prevSnapshot;
|
||||
|
||||
// that's for custom migrations, when we need new IDs, but old snapshot
|
||||
const custom: SQLiteSchema = {
|
||||
id,
|
||||
prevId: idPrev,
|
||||
...prevRest,
|
||||
};
|
||||
|
||||
return { prev: prevSnapshot, cur: result, custom };
|
||||
};
|
||||
|
||||
export const preparePgMigrationSnapshot = async (
|
||||
snapshots: string[],
|
||||
schemaPath: string | string[],
|
||||
casing: CasingType | undefined,
|
||||
): Promise<{ prev: PgSchema; cur: PgSchema; custom: PgSchema }> => {
|
||||
const prevSnapshot = pgSchema.parse(preparePrevSnapshot(snapshots, dryPg));
|
||||
const serialized = await serializePg(schemaPath, casing);
|
||||
|
||||
const id = randomUUID();
|
||||
const idPrev = prevSnapshot.id;
|
||||
|
||||
// const { version, dialect, ...rest } = serialized;
|
||||
|
||||
const result: PgSchema = { id, prevId: idPrev, ...serialized };
|
||||
|
||||
const { id: _ignoredId, prevId: _ignoredPrevId, ...prevRest } = prevSnapshot;
|
||||
|
||||
// that's for custom migrations, when we need new IDs, but old snapshot
|
||||
const custom: PgSchema = {
|
||||
id,
|
||||
prevId: idPrev,
|
||||
...prevRest,
|
||||
};
|
||||
|
||||
return { prev: prevSnapshot, cur: result, custom };
|
||||
};
|
||||
|
||||
const preparePrevSnapshot = (snapshots: string[], defaultPrev: any) => {
|
||||
let prevSnapshot: any;
|
||||
|
||||
if (snapshots.length === 0) {
|
||||
prevSnapshot = defaultPrev;
|
||||
} else {
|
||||
const lastSnapshot = snapshots[snapshots.length - 1];
|
||||
prevSnapshot = JSON.parse(fs.readFileSync(lastSnapshot).toString());
|
||||
}
|
||||
return prevSnapshot;
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { enum as enumType, TypeOf, union } from 'zod';
|
||||
import { mysqlSchema, mysqlSchemaSquashed } from './serializer/mysqlSchema';
|
||||
import { pgSchema, pgSchemaSquashed } from './serializer/pgSchema';
|
||||
import { singlestoreSchema, singlestoreSchemaSquashed } from './serializer/singlestoreSchema';
|
||||
import { sqliteSchema, SQLiteSchemaSquashed } from './serializer/sqliteSchema';
|
||||
|
||||
export const dialects = ['postgresql', 'mysql', 'sqlite', 'turso', 'singlestore', 'gel'] as const;
|
||||
export const dialect = enumType(dialects);
|
||||
|
||||
export type Dialect = (typeof dialects)[number];
|
||||
const _: Dialect = '' as TypeOf<typeof dialect>;
|
||||
|
||||
const commonSquashedSchema = union([
|
||||
pgSchemaSquashed,
|
||||
mysqlSchemaSquashed,
|
||||
SQLiteSchemaSquashed,
|
||||
singlestoreSchemaSquashed,
|
||||
]);
|
||||
|
||||
const commonSchema = union([pgSchema, mysqlSchema, sqliteSchema, singlestoreSchema]);
|
||||
|
||||
export type CommonSquashedSchema = TypeOf<typeof commonSquashedSchema>;
|
||||
export type CommonSchema = TypeOf<typeof commonSchema>;
|
||||
@@ -0,0 +1,633 @@
|
||||
import { mapValues, originUUID, snapshotVersion } from '../global';
|
||||
|
||||
import { any, array, boolean, enum as enumType, literal, number, object, record, string, TypeOf, union } from 'zod';
|
||||
|
||||
const enumSchema = object({
|
||||
name: string(),
|
||||
schema: string(),
|
||||
values: string().array(),
|
||||
}).strict();
|
||||
|
||||
const enumSchemaV1 = object({
|
||||
name: string(),
|
||||
values: record(string(), string()),
|
||||
}).strict();
|
||||
|
||||
const indexColumn = object({
|
||||
expression: string(),
|
||||
isExpression: boolean(),
|
||||
asc: boolean(),
|
||||
nulls: string().optional(),
|
||||
opclass: string().optional(),
|
||||
});
|
||||
|
||||
export type IndexColumnType = TypeOf<typeof indexColumn>;
|
||||
|
||||
const index = object({
|
||||
name: string(),
|
||||
columns: indexColumn.array(),
|
||||
isUnique: boolean(),
|
||||
with: record(string(), any()).optional(),
|
||||
method: string().default('btree'),
|
||||
where: string().optional(),
|
||||
concurrently: boolean().default(false),
|
||||
}).strict();
|
||||
|
||||
const fk = object({
|
||||
name: string(),
|
||||
tableFrom: string(),
|
||||
columnsFrom: string().array(),
|
||||
tableTo: string(),
|
||||
schemaTo: string().optional(),
|
||||
columnsTo: string().array(),
|
||||
onUpdate: string().optional(),
|
||||
onDelete: string().optional(),
|
||||
}).strict();
|
||||
|
||||
export const sequenceSchema = object({
|
||||
name: string(),
|
||||
increment: string().optional(),
|
||||
minValue: string().optional(),
|
||||
maxValue: string().optional(),
|
||||
startWith: string().optional(),
|
||||
cache: string().optional(),
|
||||
cycle: boolean().optional(),
|
||||
schema: string(),
|
||||
}).strict();
|
||||
|
||||
export const roleSchema = object({
|
||||
name: string(),
|
||||
createDb: boolean().optional(),
|
||||
createRole: boolean().optional(),
|
||||
inherit: boolean().optional(),
|
||||
}).strict();
|
||||
|
||||
export const sequenceSquashed = object({
|
||||
name: string(),
|
||||
schema: string(),
|
||||
values: string(),
|
||||
}).strict();
|
||||
|
||||
const column = object({
|
||||
name: string(),
|
||||
type: string(),
|
||||
typeSchema: string().optional(),
|
||||
primaryKey: boolean(),
|
||||
notNull: boolean(),
|
||||
default: any().optional(),
|
||||
isUnique: any().optional(),
|
||||
uniqueName: string().optional(),
|
||||
nullsNotDistinct: boolean().optional(),
|
||||
generated: object({
|
||||
type: literal('stored'),
|
||||
as: string(),
|
||||
}).optional(),
|
||||
identity: sequenceSchema
|
||||
.merge(object({ type: enumType(['always', 'byDefault']) }))
|
||||
.optional(),
|
||||
}).strict();
|
||||
|
||||
const checkConstraint = object({
|
||||
name: string(),
|
||||
value: string(),
|
||||
}).strict();
|
||||
|
||||
const columnSquashed = object({
|
||||
name: string(),
|
||||
type: string(),
|
||||
typeSchema: string().optional(),
|
||||
primaryKey: boolean(),
|
||||
notNull: boolean(),
|
||||
default: any().optional(),
|
||||
isUnique: any().optional(),
|
||||
uniqueName: string().optional(),
|
||||
nullsNotDistinct: boolean().optional(),
|
||||
generated: object({
|
||||
type: literal('stored'),
|
||||
as: string(),
|
||||
}).optional(),
|
||||
identity: string().optional(),
|
||||
}).strict();
|
||||
|
||||
const compositePK = object({
|
||||
name: string(),
|
||||
columns: string().array(),
|
||||
}).strict();
|
||||
|
||||
const uniqueConstraint = object({
|
||||
name: string(),
|
||||
columns: string().array(),
|
||||
nullsNotDistinct: boolean(),
|
||||
}).strict();
|
||||
|
||||
export const policy = object({
|
||||
name: string(),
|
||||
as: enumType(['PERMISSIVE', 'RESTRICTIVE']).optional(),
|
||||
for: enumType(['ALL', 'SELECT', 'INSERT', 'UPDATE', 'DELETE']).optional(),
|
||||
to: string().array().optional(),
|
||||
using: string().optional(),
|
||||
withCheck: string().optional(),
|
||||
on: string().optional(),
|
||||
schema: string().optional(),
|
||||
}).strict();
|
||||
|
||||
export const policySquashed = object({
|
||||
name: string(),
|
||||
values: string(),
|
||||
}).strict();
|
||||
|
||||
const viewWithOption = object({
|
||||
checkOption: enumType(['local', 'cascaded']).optional(),
|
||||
securityBarrier: boolean().optional(),
|
||||
securityInvoker: boolean().optional(),
|
||||
}).strict();
|
||||
|
||||
const matViewWithOption = object({
|
||||
fillfactor: number().optional(),
|
||||
toastTupleTarget: number().optional(),
|
||||
parallelWorkers: number().optional(),
|
||||
autovacuumEnabled: boolean().optional(),
|
||||
vacuumIndexCleanup: enumType(['auto', 'off', 'on']).optional(),
|
||||
vacuumTruncate: boolean().optional(),
|
||||
autovacuumVacuumThreshold: number().optional(),
|
||||
autovacuumVacuumScaleFactor: number().optional(),
|
||||
autovacuumVacuumCostDelay: number().optional(),
|
||||
autovacuumVacuumCostLimit: number().optional(),
|
||||
autovacuumFreezeMinAge: number().optional(),
|
||||
autovacuumFreezeMaxAge: number().optional(),
|
||||
autovacuumFreezeTableAge: number().optional(),
|
||||
autovacuumMultixactFreezeMinAge: number().optional(),
|
||||
autovacuumMultixactFreezeMaxAge: number().optional(),
|
||||
autovacuumMultixactFreezeTableAge: number().optional(),
|
||||
logAutovacuumMinDuration: number().optional(),
|
||||
userCatalogTable: boolean().optional(),
|
||||
}).strict();
|
||||
|
||||
export const mergedViewWithOption = viewWithOption.merge(matViewWithOption).strict();
|
||||
|
||||
export const view = object({
|
||||
name: string(),
|
||||
schema: string(),
|
||||
columns: record(string(), column),
|
||||
definition: string().optional(),
|
||||
materialized: boolean(),
|
||||
with: mergedViewWithOption.optional(),
|
||||
isExisting: boolean(),
|
||||
withNoData: boolean().optional(),
|
||||
using: string().optional(),
|
||||
tablespace: string().optional(),
|
||||
}).strict();
|
||||
|
||||
const table = object({
|
||||
name: string(),
|
||||
schema: string(),
|
||||
columns: record(string(), column),
|
||||
indexes: record(string(), index),
|
||||
foreignKeys: record(string(), fk),
|
||||
compositePrimaryKeys: record(string(), compositePK),
|
||||
uniqueConstraints: record(string(), uniqueConstraint).default({}),
|
||||
policies: record(string(), policy).default({}),
|
||||
checkConstraints: record(string(), checkConstraint).default({}),
|
||||
isRLSEnabled: boolean().default(false),
|
||||
}).strict();
|
||||
|
||||
const schemaHash = object({
|
||||
id: string(),
|
||||
prevId: string(),
|
||||
});
|
||||
|
||||
export const kitInternals = object({
|
||||
tables: record(
|
||||
string(),
|
||||
object({
|
||||
columns: record(
|
||||
string(),
|
||||
object({
|
||||
isArray: boolean().optional(),
|
||||
dimensions: number().optional(),
|
||||
rawType: string().optional(),
|
||||
isDefaultAnExpression: boolean().optional(),
|
||||
}).optional(),
|
||||
),
|
||||
}).optional(),
|
||||
),
|
||||
}).optional();
|
||||
|
||||
export const gelSchemaExternal = object({
|
||||
version: literal('1'),
|
||||
dialect: literal('gel'),
|
||||
tables: array(table),
|
||||
enums: array(enumSchemaV1),
|
||||
schemas: array(object({ name: string() })),
|
||||
_meta: object({
|
||||
schemas: record(string(), string()),
|
||||
tables: record(string(), string()),
|
||||
columns: record(string(), string()),
|
||||
}),
|
||||
}).strict();
|
||||
|
||||
export const gelSchemaInternal = object({
|
||||
version: literal('1'),
|
||||
dialect: literal('gel'),
|
||||
tables: record(string(), table),
|
||||
enums: record(string(), enumSchema),
|
||||
schemas: record(string(), string()),
|
||||
views: record(string(), view).default({}),
|
||||
sequences: record(string(), sequenceSchema).default({}),
|
||||
roles: record(string(), roleSchema).default({}),
|
||||
policies: record(string(), policy).default({}),
|
||||
_meta: object({
|
||||
schemas: record(string(), string()),
|
||||
tables: record(string(), string()),
|
||||
columns: record(string(), string()),
|
||||
}),
|
||||
internal: kitInternals,
|
||||
}).strict();
|
||||
|
||||
const tableSquashed = object({
|
||||
name: string(),
|
||||
schema: string(),
|
||||
columns: record(string(), columnSquashed),
|
||||
indexes: record(string(), string()),
|
||||
foreignKeys: record(string(), string()),
|
||||
compositePrimaryKeys: record(string(), string()),
|
||||
uniqueConstraints: record(string(), string()),
|
||||
policies: record(string(), string()),
|
||||
checkConstraints: record(string(), string()),
|
||||
isRLSEnabled: boolean().default(false),
|
||||
}).strict();
|
||||
|
||||
export const gelSchemaSquashed = object({
|
||||
version: literal('1'),
|
||||
dialect: literal('gel'),
|
||||
tables: record(string(), tableSquashed),
|
||||
enums: record(string(), enumSchema),
|
||||
schemas: record(string(), string()),
|
||||
views: record(string(), view),
|
||||
sequences: record(string(), sequenceSquashed),
|
||||
roles: record(string(), roleSchema).default({}),
|
||||
policies: record(string(), policySquashed).default({}),
|
||||
}).strict();
|
||||
|
||||
export const gelSchema = gelSchemaInternal.merge(schemaHash);
|
||||
|
||||
export type Enum = TypeOf<typeof enumSchema>;
|
||||
export type Sequence = TypeOf<typeof sequenceSchema>;
|
||||
export type Role = TypeOf<typeof roleSchema>;
|
||||
export type Column = TypeOf<typeof column>;
|
||||
export type Table = TypeOf<typeof table>;
|
||||
export type GelSchema = TypeOf<typeof gelSchema>;
|
||||
export type GelSchemaInternal = TypeOf<typeof gelSchemaInternal>;
|
||||
export type GelSchemaExternal = TypeOf<typeof gelSchemaExternal>;
|
||||
export type GelSchemaSquashed = TypeOf<typeof gelSchemaSquashed>;
|
||||
export type Index = TypeOf<typeof index>;
|
||||
export type ForeignKey = TypeOf<typeof fk>;
|
||||
export type PrimaryKey = TypeOf<typeof compositePK>;
|
||||
export type UniqueConstraint = TypeOf<typeof uniqueConstraint>;
|
||||
export type Policy = TypeOf<typeof policy>;
|
||||
export type View = TypeOf<typeof view>;
|
||||
export type MatViewWithOption = TypeOf<typeof matViewWithOption>;
|
||||
export type ViewWithOption = TypeOf<typeof viewWithOption>;
|
||||
|
||||
export type GelKitInternals = TypeOf<typeof kitInternals>;
|
||||
export type CheckConstraint = TypeOf<typeof checkConstraint>;
|
||||
|
||||
// no prev version
|
||||
export const backwardCompatibleGelSchema = gelSchema;
|
||||
|
||||
export const GelSquasher = {
|
||||
squashIdx: (idx: Index) => {
|
||||
index.parse(idx);
|
||||
return `${idx.name};${
|
||||
idx.columns
|
||||
.map(
|
||||
(c) => `${c.expression}--${c.isExpression}--${c.asc}--${c.nulls}--${c.opclass ? c.opclass : ''}`,
|
||||
)
|
||||
.join(',,')
|
||||
};${idx.isUnique};${idx.concurrently};${idx.method};${idx.where};${JSON.stringify(idx.with)}`;
|
||||
},
|
||||
unsquashIdx: (input: string): Index => {
|
||||
const [
|
||||
name,
|
||||
columnsString,
|
||||
isUnique,
|
||||
concurrently,
|
||||
method,
|
||||
where,
|
||||
idxWith,
|
||||
] = input.split(';');
|
||||
|
||||
const columnString = columnsString.split(',,');
|
||||
const columns: IndexColumnType[] = [];
|
||||
|
||||
for (const column of columnString) {
|
||||
const [expression, isExpression, asc, nulls, opclass] = column.split('--');
|
||||
columns.push({
|
||||
nulls: nulls as IndexColumnType['nulls'],
|
||||
isExpression: isExpression === 'true',
|
||||
asc: asc === 'true',
|
||||
expression: expression,
|
||||
opclass: opclass === 'undefined' ? undefined : opclass,
|
||||
});
|
||||
}
|
||||
|
||||
const result: Index = index.parse({
|
||||
name,
|
||||
columns: columns,
|
||||
isUnique: isUnique === 'true',
|
||||
concurrently: concurrently === 'true',
|
||||
method,
|
||||
where: where === 'undefined' ? undefined : where,
|
||||
with: !idxWith || idxWith === 'undefined' ? undefined : JSON.parse(idxWith),
|
||||
});
|
||||
return result;
|
||||
},
|
||||
squashIdxPush: (idx: Index) => {
|
||||
index.parse(idx);
|
||||
return `${idx.name};${
|
||||
idx.columns
|
||||
.map((c) => `${c.isExpression ? '' : c.expression}--${c.asc}--${c.nulls}`)
|
||||
.join(',,')
|
||||
};${idx.isUnique};${idx.method};${JSON.stringify(idx.with)}`;
|
||||
},
|
||||
unsquashIdxPush: (input: string): Index => {
|
||||
const [name, columnsString, isUnique, method, idxWith] = input.split(';');
|
||||
|
||||
const columnString = columnsString.split('--');
|
||||
const columns: IndexColumnType[] = [];
|
||||
|
||||
for (const column of columnString) {
|
||||
const [expression, asc, nulls, opclass] = column.split(',');
|
||||
columns.push({
|
||||
nulls: nulls as IndexColumnType['nulls'],
|
||||
isExpression: expression === '',
|
||||
asc: asc === 'true',
|
||||
expression: expression,
|
||||
});
|
||||
}
|
||||
|
||||
const result: Index = index.parse({
|
||||
name,
|
||||
columns: columns,
|
||||
isUnique: isUnique === 'true',
|
||||
concurrently: false,
|
||||
method,
|
||||
with: idxWith === 'undefined' ? undefined : JSON.parse(idxWith),
|
||||
});
|
||||
return result;
|
||||
},
|
||||
squashFK: (fk: ForeignKey) => {
|
||||
return `${fk.name};${fk.tableFrom};${fk.columnsFrom.join(',')};${fk.tableTo};${fk.columnsTo.join(',')};${
|
||||
fk.onUpdate ?? ''
|
||||
};${fk.onDelete ?? ''};${fk.schemaTo || 'public'}`;
|
||||
},
|
||||
squashPolicy: (policy: Policy) => {
|
||||
return `${policy.name}--${policy.as}--${policy.for}--${
|
||||
policy.to?.join(',')
|
||||
}--${policy.using}--${policy.withCheck}--${policy.on}`;
|
||||
},
|
||||
unsquashPolicy: (policy: string): Policy => {
|
||||
const splitted = policy.split('--');
|
||||
return {
|
||||
name: splitted[0],
|
||||
as: splitted[1] as Policy['as'],
|
||||
for: splitted[2] as Policy['for'],
|
||||
to: splitted[3].split(','),
|
||||
using: splitted[4] !== 'undefined' ? splitted[4] : undefined,
|
||||
withCheck: splitted[5] !== 'undefined' ? splitted[5] : undefined,
|
||||
on: splitted[6] !== 'undefined' ? splitted[6] : undefined,
|
||||
};
|
||||
},
|
||||
squashPolicyPush: (policy: Policy) => {
|
||||
return `${policy.name}--${policy.as}--${policy.for}--${policy.to?.join(',')}--${policy.on}`;
|
||||
},
|
||||
unsquashPolicyPush: (policy: string): Policy => {
|
||||
const splitted = policy.split('--');
|
||||
return {
|
||||
name: splitted[0],
|
||||
as: splitted[1] as Policy['as'],
|
||||
for: splitted[2] as Policy['for'],
|
||||
to: splitted[3].split(','),
|
||||
on: splitted[4] !== 'undefined' ? splitted[4] : undefined,
|
||||
};
|
||||
},
|
||||
squashPK: (pk: PrimaryKey) => {
|
||||
return `${pk.columns.join(',')};${pk.name}`;
|
||||
},
|
||||
unsquashPK: (pk: string): PrimaryKey => {
|
||||
const splitted = pk.split(';');
|
||||
return { name: splitted[1], columns: splitted[0].split(',') };
|
||||
},
|
||||
squashUnique: (unq: UniqueConstraint) => {
|
||||
return `${unq.name};${unq.columns.join(',')};${unq.nullsNotDistinct}`;
|
||||
},
|
||||
unsquashUnique: (unq: string): UniqueConstraint => {
|
||||
const [name, columns, nullsNotDistinct] = unq.split(';');
|
||||
return {
|
||||
name,
|
||||
columns: columns.split(','),
|
||||
nullsNotDistinct: nullsNotDistinct === 'true',
|
||||
};
|
||||
},
|
||||
unsquashFK: (input: string): ForeignKey => {
|
||||
const [
|
||||
name,
|
||||
tableFrom,
|
||||
columnsFromStr,
|
||||
tableTo,
|
||||
columnsToStr,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
schemaTo,
|
||||
] = input.split(';');
|
||||
|
||||
const result: ForeignKey = fk.parse({
|
||||
name,
|
||||
tableFrom,
|
||||
columnsFrom: columnsFromStr.split(','),
|
||||
schemaTo: schemaTo,
|
||||
tableTo,
|
||||
columnsTo: columnsToStr.split(','),
|
||||
onUpdate,
|
||||
onDelete,
|
||||
});
|
||||
return result;
|
||||
},
|
||||
squashSequence: (seq: Omit<Sequence, 'name' | 'schema'>) => {
|
||||
return `${seq.minValue};${seq.maxValue};${seq.increment};${seq.startWith};${seq.cache};${seq.cycle ?? ''}`;
|
||||
},
|
||||
unsquashSequence: (seq: string): Omit<Sequence, 'name' | 'schema'> => {
|
||||
const splitted = seq.split(';');
|
||||
return {
|
||||
minValue: splitted[0] !== 'undefined' ? splitted[0] : undefined,
|
||||
maxValue: splitted[1] !== 'undefined' ? splitted[1] : undefined,
|
||||
increment: splitted[2] !== 'undefined' ? splitted[2] : undefined,
|
||||
startWith: splitted[3] !== 'undefined' ? splitted[3] : undefined,
|
||||
cache: splitted[4] !== 'undefined' ? splitted[4] : undefined,
|
||||
cycle: splitted[5] === 'true',
|
||||
};
|
||||
},
|
||||
squashIdentity: (
|
||||
seq: Omit<Sequence, 'schema'> & { type: 'always' | 'byDefault' },
|
||||
) => {
|
||||
return `${seq.name};${seq.type};${seq.minValue};${seq.maxValue};${seq.increment};${seq.startWith};${seq.cache};${
|
||||
seq.cycle ?? ''
|
||||
}`;
|
||||
},
|
||||
unsquashIdentity: (
|
||||
seq: string,
|
||||
): Omit<Sequence, 'schema'> & { type: 'always' | 'byDefault' } => {
|
||||
const splitted = seq.split(';');
|
||||
return {
|
||||
name: splitted[0],
|
||||
type: splitted[1] as 'always' | 'byDefault',
|
||||
minValue: splitted[2] !== 'undefined' ? splitted[2] : undefined,
|
||||
maxValue: splitted[3] !== 'undefined' ? splitted[3] : undefined,
|
||||
increment: splitted[4] !== 'undefined' ? splitted[4] : undefined,
|
||||
startWith: splitted[5] !== 'undefined' ? splitted[5] : undefined,
|
||||
cache: splitted[6] !== 'undefined' ? splitted[6] : undefined,
|
||||
cycle: splitted[7] === 'true',
|
||||
};
|
||||
},
|
||||
squashCheck: (check: CheckConstraint) => {
|
||||
return `${check.name};${check.value}`;
|
||||
},
|
||||
unsquashCheck: (input: string): CheckConstraint => {
|
||||
const [
|
||||
name,
|
||||
value,
|
||||
] = input.split(';');
|
||||
|
||||
return { name, value };
|
||||
},
|
||||
};
|
||||
|
||||
export const squashGelScheme = (
|
||||
json: GelSchema,
|
||||
action?: 'push' | undefined,
|
||||
): GelSchemaSquashed => {
|
||||
const mappedTables = Object.fromEntries(
|
||||
Object.entries(json.tables).map((it) => {
|
||||
const squashedIndexes = mapValues(it[1].indexes, (index) => {
|
||||
return action === 'push'
|
||||
? GelSquasher.squashIdxPush(index)
|
||||
: GelSquasher.squashIdx(index);
|
||||
});
|
||||
|
||||
const squashedFKs = mapValues(it[1].foreignKeys, (fk) => {
|
||||
return GelSquasher.squashFK(fk);
|
||||
});
|
||||
|
||||
const squashedPKs = mapValues(it[1].compositePrimaryKeys, (pk) => {
|
||||
return GelSquasher.squashPK(pk);
|
||||
});
|
||||
|
||||
const mappedColumns = Object.fromEntries(
|
||||
Object.entries(it[1].columns).map((it) => {
|
||||
const mappedIdentity = it[1].identity
|
||||
? GelSquasher.squashIdentity(it[1].identity)
|
||||
: undefined;
|
||||
return [
|
||||
it[0],
|
||||
{
|
||||
...it[1],
|
||||
identity: mappedIdentity,
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
const squashedUniqueConstraints = mapValues(
|
||||
it[1].uniqueConstraints,
|
||||
(unq) => {
|
||||
return GelSquasher.squashUnique(unq);
|
||||
},
|
||||
);
|
||||
|
||||
const squashedPolicies = mapValues(it[1].policies, (policy) => {
|
||||
return action === 'push'
|
||||
? GelSquasher.squashPolicyPush(policy)
|
||||
: GelSquasher.squashPolicy(policy);
|
||||
});
|
||||
const squashedChecksContraints = mapValues(
|
||||
it[1].checkConstraints,
|
||||
(check) => {
|
||||
return GelSquasher.squashCheck(check);
|
||||
},
|
||||
);
|
||||
|
||||
return [
|
||||
it[0],
|
||||
{
|
||||
name: it[1].name,
|
||||
schema: it[1].schema,
|
||||
columns: mappedColumns,
|
||||
indexes: squashedIndexes,
|
||||
foreignKeys: squashedFKs,
|
||||
compositePrimaryKeys: squashedPKs,
|
||||
uniqueConstraints: squashedUniqueConstraints,
|
||||
policies: squashedPolicies,
|
||||
checkConstraints: squashedChecksContraints,
|
||||
isRLSEnabled: it[1].isRLSEnabled ?? false,
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
const mappedSequences = Object.fromEntries(
|
||||
Object.entries(json.sequences).map((it) => {
|
||||
return [
|
||||
it[0],
|
||||
{
|
||||
name: it[1].name,
|
||||
schema: it[1].schema,
|
||||
values: GelSquasher.squashSequence(it[1]),
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
const mappedPolicies = Object.fromEntries(
|
||||
Object.entries(json.policies).map((it) => {
|
||||
return [
|
||||
it[0],
|
||||
{
|
||||
name: it[1].name,
|
||||
values: action === 'push'
|
||||
? GelSquasher.squashPolicyPush(it[1])
|
||||
: GelSquasher.squashPolicy(it[1]),
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
dialect: json.dialect,
|
||||
tables: mappedTables,
|
||||
enums: json.enums,
|
||||
schemas: json.schemas,
|
||||
views: json.views,
|
||||
policies: mappedPolicies,
|
||||
sequences: mappedSequences,
|
||||
roles: json.roles,
|
||||
};
|
||||
};
|
||||
|
||||
export const dryGel = gelSchema.parse({
|
||||
version: '1',
|
||||
dialect: 'gel',
|
||||
id: originUUID,
|
||||
prevId: '',
|
||||
tables: {},
|
||||
enums: {},
|
||||
schemas: {},
|
||||
policies: {},
|
||||
roles: {},
|
||||
sequences: {},
|
||||
_meta: {
|
||||
schemas: {},
|
||||
tables: {},
|
||||
columns: {},
|
||||
},
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,130 @@
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs';
|
||||
import * as glob from 'glob';
|
||||
import Path from 'path';
|
||||
import { CasingType } from 'src/cli/validations/common';
|
||||
import { error } from '../cli/views';
|
||||
import type { MySqlSchemaInternal } from './mysqlSchema';
|
||||
import type { PgSchemaInternal } from './pgSchema';
|
||||
import { SingleStoreSchemaInternal } from './singlestoreSchema';
|
||||
import type { SQLiteSchemaInternal } from './sqliteSchema';
|
||||
|
||||
export const serializeMySql = async (
|
||||
path: string | string[],
|
||||
casing: CasingType | undefined,
|
||||
): Promise<MySqlSchemaInternal> => {
|
||||
const filenames = prepareFilenames(path);
|
||||
|
||||
console.log(chalk.gray(`Reading schema files:\n${filenames.join('\n')}\n`));
|
||||
|
||||
const { prepareFromMySqlImports } = await import('./mysqlImports');
|
||||
const { generateMySqlSnapshot } = await import('./mysqlSerializer');
|
||||
|
||||
const { tables, views } = await prepareFromMySqlImports(filenames);
|
||||
|
||||
return generateMySqlSnapshot(tables, views, casing);
|
||||
};
|
||||
|
||||
export const serializePg = async (
|
||||
path: string | string[],
|
||||
casing: CasingType | undefined,
|
||||
schemaFilter?: string[],
|
||||
): Promise<PgSchemaInternal> => {
|
||||
const filenames = prepareFilenames(path);
|
||||
|
||||
const { prepareFromPgImports } = await import('./pgImports');
|
||||
const { generatePgSnapshot } = await import('./pgSerializer');
|
||||
|
||||
const { tables, enums, schemas, sequences, views, matViews, roles, policies } = await prepareFromPgImports(
|
||||
filenames,
|
||||
);
|
||||
|
||||
return generatePgSnapshot(tables, enums, schemas, sequences, roles, policies, views, matViews, casing, schemaFilter);
|
||||
};
|
||||
|
||||
export const serializeSQLite = async (
|
||||
path: string | string[],
|
||||
casing: CasingType | undefined,
|
||||
): Promise<SQLiteSchemaInternal> => {
|
||||
const filenames = prepareFilenames(path);
|
||||
|
||||
const { prepareFromSqliteImports } = await import('./sqliteImports');
|
||||
const { generateSqliteSnapshot } = await import('./sqliteSerializer');
|
||||
const { tables, views } = await prepareFromSqliteImports(filenames);
|
||||
return generateSqliteSnapshot(tables, views, casing);
|
||||
};
|
||||
|
||||
export const serializeSingleStore = async (
|
||||
path: string | string[],
|
||||
casing: CasingType | undefined,
|
||||
): Promise<SingleStoreSchemaInternal> => {
|
||||
const filenames = prepareFilenames(path);
|
||||
|
||||
console.log(chalk.gray(`Reading schema files:\n${filenames.join('\n')}\n`));
|
||||
|
||||
const { prepareFromSingleStoreImports } = await import('./singlestoreImports');
|
||||
const { generateSingleStoreSnapshot } = await import('./singlestoreSerializer');
|
||||
|
||||
const { tables /* views */ } = await prepareFromSingleStoreImports(filenames);
|
||||
|
||||
return generateSingleStoreSnapshot(tables, /* views, */ casing);
|
||||
};
|
||||
|
||||
export const prepareFilenames = (path: string | string[]) => {
|
||||
if (typeof path === 'string') {
|
||||
path = [path];
|
||||
}
|
||||
const prefix = process.env.TEST_CONFIG_PATH_PREFIX || '';
|
||||
|
||||
const result = path.reduce((result, cur) => {
|
||||
const globbed = glob.sync(`${prefix}${cur}`);
|
||||
|
||||
globbed.forEach((it) => {
|
||||
const fileName = fs.lstatSync(it).isDirectory() ? null : Path.resolve(it);
|
||||
|
||||
const filenames = fileName
|
||||
? [fileName!]
|
||||
: fs.readdirSync(it).map((file) => Path.join(Path.resolve(it), file));
|
||||
|
||||
filenames
|
||||
.filter((file) => !fs.lstatSync(file).isDirectory())
|
||||
.forEach((file) => result.add(file));
|
||||
});
|
||||
|
||||
return result;
|
||||
}, new Set<string>());
|
||||
const res = [...result];
|
||||
|
||||
// TODO: properly handle and test
|
||||
const errors = res.filter((it) => {
|
||||
return !(
|
||||
it.endsWith('.ts')
|
||||
|| it.endsWith('.js')
|
||||
|| it.endsWith('.cjs')
|
||||
|| it.endsWith('.mjs')
|
||||
|| it.endsWith('.mts')
|
||||
|| it.endsWith('.cts')
|
||||
);
|
||||
});
|
||||
|
||||
// when schema: "./schema" and not "./schema.ts"
|
||||
if (res.length === 0) {
|
||||
console.log(
|
||||
error(
|
||||
`No schema files found for path config [${
|
||||
path
|
||||
.map((it) => `'${it}'`)
|
||||
.join(', ')
|
||||
}]`,
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
error(
|
||||
`If path represents a file - please make sure to use .ts or other extension in the path`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import { is } from 'drizzle-orm';
|
||||
import type { AnyMySqlTable } from 'drizzle-orm/mysql-core';
|
||||
import { MySqlTable, MySqlView } from 'drizzle-orm/mysql-core';
|
||||
import { safeRegister } from '../cli/commands/utils';
|
||||
|
||||
export const prepareFromExports = (exports: Record<string, unknown>) => {
|
||||
const tables: AnyMySqlTable[] = [];
|
||||
const views: MySqlView[] = [];
|
||||
|
||||
const i0values = Object.values(exports);
|
||||
i0values.forEach((t) => {
|
||||
if (is(t, MySqlTable)) {
|
||||
tables.push(t);
|
||||
}
|
||||
|
||||
if (is(t, MySqlView)) {
|
||||
views.push(t);
|
||||
}
|
||||
});
|
||||
|
||||
return { tables, views };
|
||||
};
|
||||
|
||||
export const prepareFromMySqlImports = async (imports: string[]) => {
|
||||
const tables: AnyMySqlTable[] = [];
|
||||
const views: MySqlView[] = [];
|
||||
|
||||
await safeRegister(async () => {
|
||||
for (let i = 0; i < imports.length; i++) {
|
||||
const it = imports[i];
|
||||
const i0: Record<string, unknown> = require(`${it}`);
|
||||
const prepared = prepareFromExports(i0);
|
||||
|
||||
tables.push(...prepared.tables);
|
||||
views.push(...prepared.views);
|
||||
}
|
||||
});
|
||||
|
||||
return { tables: Array.from(new Set(tables)), views };
|
||||
};
|
||||
@@ -0,0 +1,423 @@
|
||||
import { any, boolean, enum as enumType, literal, object, record, string, TypeOf, union } from 'zod';
|
||||
import { mapValues, originUUID } from '../global';
|
||||
|
||||
// ------- V3 --------
|
||||
const index = object({
|
||||
name: string(),
|
||||
columns: string().array(),
|
||||
isUnique: boolean(),
|
||||
using: enumType(['btree', 'hash']).optional(),
|
||||
algorithm: enumType(['default', 'inplace', 'copy']).optional(),
|
||||
lock: enumType(['default', 'none', 'shared', 'exclusive']).optional(),
|
||||
}).strict();
|
||||
|
||||
const fk = object({
|
||||
name: string(),
|
||||
tableFrom: string(),
|
||||
columnsFrom: string().array(),
|
||||
tableTo: string(),
|
||||
columnsTo: string().array(),
|
||||
onUpdate: string().optional(),
|
||||
onDelete: string().optional(),
|
||||
}).strict();
|
||||
|
||||
const column = object({
|
||||
name: string(),
|
||||
type: string(),
|
||||
primaryKey: boolean(),
|
||||
notNull: boolean(),
|
||||
autoincrement: boolean().optional(),
|
||||
default: any().optional(),
|
||||
onUpdate: any().optional(),
|
||||
generated: object({
|
||||
type: enumType(['stored', 'virtual']),
|
||||
as: string(),
|
||||
}).optional(),
|
||||
}).strict();
|
||||
|
||||
const tableV3 = object({
|
||||
name: string(),
|
||||
columns: record(string(), column),
|
||||
indexes: record(string(), index),
|
||||
foreignKeys: record(string(), fk),
|
||||
}).strict();
|
||||
|
||||
const compositePK = object({
|
||||
name: string(),
|
||||
columns: string().array(),
|
||||
}).strict();
|
||||
|
||||
const uniqueConstraint = object({
|
||||
name: string(),
|
||||
columns: string().array(),
|
||||
}).strict();
|
||||
|
||||
const checkConstraint = object({
|
||||
name: string(),
|
||||
value: string(),
|
||||
}).strict();
|
||||
|
||||
const tableV4 = object({
|
||||
name: string(),
|
||||
schema: string().optional(),
|
||||
columns: record(string(), column),
|
||||
indexes: record(string(), index),
|
||||
foreignKeys: record(string(), fk),
|
||||
}).strict();
|
||||
|
||||
const table = object({
|
||||
name: string(),
|
||||
columns: record(string(), column),
|
||||
indexes: record(string(), index),
|
||||
foreignKeys: record(string(), fk),
|
||||
compositePrimaryKeys: record(string(), compositePK),
|
||||
uniqueConstraints: record(string(), uniqueConstraint).default({}),
|
||||
checkConstraint: record(string(), checkConstraint).default({}),
|
||||
}).strict();
|
||||
|
||||
const viewMeta = object({
|
||||
algorithm: enumType(['undefined', 'merge', 'temptable']),
|
||||
sqlSecurity: enumType(['definer', 'invoker']),
|
||||
withCheckOption: enumType(['local', 'cascaded']).optional(),
|
||||
}).strict();
|
||||
|
||||
export const view = object({
|
||||
name: string(),
|
||||
columns: record(string(), column),
|
||||
definition: string().optional(),
|
||||
isExisting: boolean(),
|
||||
}).strict().merge(viewMeta);
|
||||
type SquasherViewMeta = Omit<TypeOf<typeof viewMeta>, 'definer'>;
|
||||
|
||||
export const kitInternals = object({
|
||||
tables: record(
|
||||
string(),
|
||||
object({
|
||||
columns: record(
|
||||
string(),
|
||||
object({ isDefaultAnExpression: boolean().optional() }).optional(),
|
||||
),
|
||||
}).optional(),
|
||||
).optional(),
|
||||
indexes: record(
|
||||
string(),
|
||||
object({
|
||||
columns: record(
|
||||
string(),
|
||||
object({ isExpression: boolean().optional() }).optional(),
|
||||
),
|
||||
}).optional(),
|
||||
).optional(),
|
||||
}).optional();
|
||||
|
||||
// use main dialect
|
||||
const dialect = literal('mysql');
|
||||
|
||||
const schemaHash = object({
|
||||
id: string(),
|
||||
prevId: string(),
|
||||
});
|
||||
|
||||
export const schemaInternalV3 = object({
|
||||
version: literal('3'),
|
||||
dialect: dialect,
|
||||
tables: record(string(), tableV3),
|
||||
}).strict();
|
||||
|
||||
export const schemaInternalV4 = object({
|
||||
version: literal('4'),
|
||||
dialect: dialect,
|
||||
tables: record(string(), tableV4),
|
||||
schemas: record(string(), string()),
|
||||
}).strict();
|
||||
|
||||
export const schemaInternalV5 = object({
|
||||
version: literal('5'),
|
||||
dialect: dialect,
|
||||
tables: record(string(), table),
|
||||
schemas: record(string(), string()),
|
||||
_meta: object({
|
||||
schemas: record(string(), string()),
|
||||
tables: record(string(), string()),
|
||||
columns: record(string(), string()),
|
||||
}),
|
||||
internal: kitInternals,
|
||||
}).strict();
|
||||
|
||||
export const schemaInternal = object({
|
||||
version: literal('5'),
|
||||
dialect: dialect,
|
||||
tables: record(string(), table),
|
||||
views: record(string(), view).default({}),
|
||||
_meta: object({
|
||||
tables: record(string(), string()),
|
||||
columns: record(string(), string()),
|
||||
}),
|
||||
internal: kitInternals,
|
||||
}).strict();
|
||||
|
||||
export const schemaV3 = schemaInternalV3.merge(schemaHash);
|
||||
export const schemaV4 = schemaInternalV4.merge(schemaHash);
|
||||
export const schemaV5 = schemaInternalV5.merge(schemaHash);
|
||||
export const schema = schemaInternal.merge(schemaHash);
|
||||
|
||||
const tableSquashedV4 = object({
|
||||
name: string(),
|
||||
schema: string().optional(),
|
||||
columns: record(string(), column),
|
||||
indexes: record(string(), string()),
|
||||
foreignKeys: record(string(), string()),
|
||||
}).strict();
|
||||
|
||||
const tableSquashed = object({
|
||||
name: string(),
|
||||
columns: record(string(), column),
|
||||
indexes: record(string(), string()),
|
||||
foreignKeys: record(string(), string()),
|
||||
compositePrimaryKeys: record(string(), string()),
|
||||
uniqueConstraints: record(string(), string()).default({}),
|
||||
checkConstraints: record(string(), string()).default({}),
|
||||
}).strict();
|
||||
|
||||
const viewSquashed = view.omit({
|
||||
algorithm: true,
|
||||
sqlSecurity: true,
|
||||
withCheckOption: true,
|
||||
}).extend({ meta: string() });
|
||||
|
||||
export const schemaSquashed = object({
|
||||
version: literal('5'),
|
||||
dialect: dialect,
|
||||
tables: record(string(), tableSquashed),
|
||||
views: record(string(), viewSquashed),
|
||||
}).strict();
|
||||
|
||||
export const schemaSquashedV4 = object({
|
||||
version: literal('4'),
|
||||
dialect: dialect,
|
||||
tables: record(string(), tableSquashedV4),
|
||||
schemas: record(string(), string()),
|
||||
}).strict();
|
||||
|
||||
export type Dialect = TypeOf<typeof dialect>;
|
||||
export type Column = TypeOf<typeof column>;
|
||||
export type Table = TypeOf<typeof table>;
|
||||
export type TableV4 = TypeOf<typeof tableV4>;
|
||||
export type MySqlSchema = TypeOf<typeof schema>;
|
||||
export type MySqlSchemaV3 = TypeOf<typeof schemaV3>;
|
||||
export type MySqlSchemaV4 = TypeOf<typeof schemaV4>;
|
||||
export type MySqlSchemaV5 = TypeOf<typeof schemaV5>;
|
||||
export type MySqlSchemaInternal = TypeOf<typeof schemaInternal>;
|
||||
export type MySqlKitInternals = TypeOf<typeof kitInternals>;
|
||||
export type MySqlSchemaSquashed = TypeOf<typeof schemaSquashed>;
|
||||
export type MySqlSchemaSquashedV4 = TypeOf<typeof schemaSquashedV4>;
|
||||
export type Index = TypeOf<typeof index>;
|
||||
export type ForeignKey = TypeOf<typeof fk>;
|
||||
export type PrimaryKey = TypeOf<typeof compositePK>;
|
||||
export type UniqueConstraint = TypeOf<typeof uniqueConstraint>;
|
||||
export type CheckConstraint = TypeOf<typeof checkConstraint>;
|
||||
export type View = TypeOf<typeof view>;
|
||||
export type ViewSquashed = TypeOf<typeof viewSquashed>;
|
||||
|
||||
export const MySqlSquasher = {
|
||||
squashIdx: (idx: Index) => {
|
||||
index.parse(idx);
|
||||
return `${idx.name};${idx.columns.join(',')};${idx.isUnique};${idx.using ?? ''};${idx.algorithm ?? ''};${
|
||||
idx.lock ?? ''
|
||||
}`;
|
||||
},
|
||||
unsquashIdx: (input: string): Index => {
|
||||
const [name, columnsString, isUnique, using, algorithm, lock] = input.split(';');
|
||||
const destructed = {
|
||||
name,
|
||||
columns: columnsString.split(','),
|
||||
isUnique: isUnique === 'true',
|
||||
using: using ? using : undefined,
|
||||
algorithm: algorithm ? algorithm : undefined,
|
||||
lock: lock ? lock : undefined,
|
||||
};
|
||||
return index.parse(destructed);
|
||||
},
|
||||
squashPK: (pk: PrimaryKey) => {
|
||||
return `${pk.name};${pk.columns.join(',')}`;
|
||||
},
|
||||
unsquashPK: (pk: string): PrimaryKey => {
|
||||
const splitted = pk.split(';');
|
||||
return { name: splitted[0], columns: splitted[1].split(',') };
|
||||
},
|
||||
squashUnique: (unq: UniqueConstraint) => {
|
||||
return `${unq.name};${unq.columns.join(',')}`;
|
||||
},
|
||||
unsquashUnique: (unq: string): UniqueConstraint => {
|
||||
const [name, columns] = unq.split(';');
|
||||
return { name, columns: columns.split(',') };
|
||||
},
|
||||
squashFK: (fk: ForeignKey) => {
|
||||
return `${fk.name};${fk.tableFrom};${fk.columnsFrom.join(',')};${fk.tableTo};${fk.columnsTo.join(',')};${
|
||||
fk.onUpdate ?? ''
|
||||
};${fk.onDelete ?? ''}`;
|
||||
},
|
||||
unsquashFK: (input: string): ForeignKey => {
|
||||
const [
|
||||
name,
|
||||
tableFrom,
|
||||
columnsFromStr,
|
||||
tableTo,
|
||||
columnsToStr,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
] = input.split(';');
|
||||
|
||||
const result: ForeignKey = fk.parse({
|
||||
name,
|
||||
tableFrom,
|
||||
columnsFrom: columnsFromStr.split(','),
|
||||
tableTo,
|
||||
columnsTo: columnsToStr.split(','),
|
||||
onUpdate,
|
||||
onDelete,
|
||||
});
|
||||
return result;
|
||||
},
|
||||
squashCheck: (input: CheckConstraint): string => {
|
||||
return `${input.name};${input.value}`;
|
||||
},
|
||||
unsquashCheck: (input: string): CheckConstraint => {
|
||||
const [name, value] = input.split(';');
|
||||
|
||||
return { name, value };
|
||||
},
|
||||
squashView: (view: View): string => {
|
||||
return `${view.algorithm};${view.sqlSecurity};${view.withCheckOption}`;
|
||||
},
|
||||
unsquashView: (meta: string): SquasherViewMeta => {
|
||||
const [algorithm, sqlSecurity, withCheckOption] = meta.split(';');
|
||||
const toReturn = {
|
||||
algorithm: algorithm,
|
||||
sqlSecurity: sqlSecurity,
|
||||
withCheckOption: withCheckOption !== 'undefined' ? withCheckOption : undefined,
|
||||
};
|
||||
|
||||
return viewMeta.parse(toReturn);
|
||||
},
|
||||
};
|
||||
|
||||
export const squashMysqlSchemeV4 = (
|
||||
json: MySqlSchemaV4,
|
||||
): MySqlSchemaSquashedV4 => {
|
||||
const mappedTables = Object.fromEntries(
|
||||
Object.entries(json.tables).map((it) => {
|
||||
const squashedIndexes = mapValues(it[1].indexes, (index) => {
|
||||
return MySqlSquasher.squashIdx(index);
|
||||
});
|
||||
|
||||
const squashedFKs = mapValues(it[1].foreignKeys, (fk) => {
|
||||
return MySqlSquasher.squashFK(fk);
|
||||
});
|
||||
|
||||
return [
|
||||
it[0],
|
||||
{
|
||||
name: it[1].name,
|
||||
schema: it[1].schema,
|
||||
columns: it[1].columns,
|
||||
indexes: squashedIndexes,
|
||||
foreignKeys: squashedFKs,
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
return {
|
||||
version: '4',
|
||||
dialect: json.dialect,
|
||||
tables: mappedTables,
|
||||
schemas: json.schemas,
|
||||
};
|
||||
};
|
||||
|
||||
export const squashMysqlScheme = (json: MySqlSchema): MySqlSchemaSquashed => {
|
||||
const mappedTables = Object.fromEntries(
|
||||
Object.entries(json.tables).map((it) => {
|
||||
const squashedIndexes = mapValues(it[1].indexes, (index) => {
|
||||
return MySqlSquasher.squashIdx(index);
|
||||
});
|
||||
|
||||
const squashedFKs = mapValues(it[1].foreignKeys, (fk) => {
|
||||
return MySqlSquasher.squashFK(fk);
|
||||
});
|
||||
|
||||
const squashedPKs = mapValues(it[1].compositePrimaryKeys, (pk) => {
|
||||
return MySqlSquasher.squashPK(pk);
|
||||
});
|
||||
|
||||
const squashedUniqueConstraints = mapValues(
|
||||
it[1].uniqueConstraints,
|
||||
(unq) => {
|
||||
return MySqlSquasher.squashUnique(unq);
|
||||
},
|
||||
);
|
||||
|
||||
const squashedCheckConstraints = mapValues(it[1].checkConstraint, (check) => {
|
||||
return MySqlSquasher.squashCheck(check);
|
||||
});
|
||||
|
||||
return [
|
||||
it[0],
|
||||
{
|
||||
name: it[1].name,
|
||||
columns: it[1].columns,
|
||||
indexes: squashedIndexes,
|
||||
foreignKeys: squashedFKs,
|
||||
compositePrimaryKeys: squashedPKs,
|
||||
uniqueConstraints: squashedUniqueConstraints,
|
||||
checkConstraints: squashedCheckConstraints,
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
const mappedViews = Object.fromEntries(
|
||||
Object.entries(json.views).map(([key, value]) => {
|
||||
const meta = MySqlSquasher.squashView(value);
|
||||
|
||||
return [key, {
|
||||
name: value.name,
|
||||
isExisting: value.isExisting,
|
||||
columns: value.columns,
|
||||
definition: value.definition,
|
||||
meta,
|
||||
}];
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
version: '5',
|
||||
dialect: json.dialect,
|
||||
tables: mappedTables,
|
||||
views: mappedViews,
|
||||
};
|
||||
};
|
||||
|
||||
export const mysqlSchema = schema;
|
||||
export const mysqlSchemaV3 = schemaV3;
|
||||
export const mysqlSchemaV4 = schemaV4;
|
||||
export const mysqlSchemaV5 = schemaV5;
|
||||
export const mysqlSchemaSquashed = schemaSquashed;
|
||||
|
||||
// no prev version
|
||||
export const backwardCompatibleMysqlSchema = union([mysqlSchemaV5, schema]);
|
||||
|
||||
export const dryMySql = mysqlSchema.parse({
|
||||
version: '5',
|
||||
dialect: 'mysql',
|
||||
id: originUUID,
|
||||
prevId: '',
|
||||
tables: {},
|
||||
schemas: {},
|
||||
views: {},
|
||||
_meta: {
|
||||
schemas: {},
|
||||
tables: {},
|
||||
columns: {},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,999 @@
|
||||
import chalk from 'chalk';
|
||||
import { getTableName, is, SQL } from 'drizzle-orm';
|
||||
import {
|
||||
AnyMySqlTable,
|
||||
getTableConfig,
|
||||
getViewConfig,
|
||||
MySqlColumn,
|
||||
MySqlDialect,
|
||||
MySqlView,
|
||||
type PrimaryKey as PrimaryKeyORM,
|
||||
uniqueKeyName,
|
||||
} from 'drizzle-orm/mysql-core';
|
||||
import { RowDataPacket } from 'mysql2/promise';
|
||||
import { CasingType } from 'src/cli/validations/common';
|
||||
import { withStyle } from '../cli/validations/outputs';
|
||||
import { IntrospectStage, IntrospectStatus } from '../cli/views';
|
||||
import {
|
||||
CheckConstraint,
|
||||
Column,
|
||||
ForeignKey,
|
||||
Index,
|
||||
MySqlKitInternals,
|
||||
MySqlSchemaInternal,
|
||||
PrimaryKey,
|
||||
Table,
|
||||
UniqueConstraint,
|
||||
View,
|
||||
} from '../serializer/mysqlSchema';
|
||||
import { type DB, escapeSingleQuotes } from '../utils';
|
||||
import { getColumnCasing, sqlToStr } from './utils';
|
||||
|
||||
export const indexName = (tableName: string, columns: string[]) => {
|
||||
return `${tableName}_${columns.join('_')}_index`;
|
||||
};
|
||||
|
||||
const handleEnumType = (type: string) => {
|
||||
let str = type.split('(')[1];
|
||||
str = str.substring(0, str.length - 1);
|
||||
const values = str.split(',').map((v) => `'${escapeSingleQuotes(v.substring(1, v.length - 1))}'`);
|
||||
return `enum(${values.join(',')})`;
|
||||
};
|
||||
|
||||
export const generateMySqlSnapshot = (
|
||||
tables: AnyMySqlTable[],
|
||||
views: MySqlView[],
|
||||
casing: CasingType | undefined,
|
||||
): MySqlSchemaInternal => {
|
||||
const dialect = new MySqlDialect({ casing });
|
||||
const result: Record<string, Table> = {};
|
||||
const resultViews: Record<string, View> = {};
|
||||
const internal: MySqlKitInternals = { tables: {}, indexes: {} };
|
||||
|
||||
for (const table of tables) {
|
||||
const {
|
||||
name: tableName,
|
||||
columns,
|
||||
indexes,
|
||||
foreignKeys,
|
||||
schema,
|
||||
checks,
|
||||
primaryKeys,
|
||||
uniqueConstraints,
|
||||
} = getTableConfig(table);
|
||||
|
||||
const columnsObject: Record<string, Column> = {};
|
||||
const indexesObject: Record<string, Index> = {};
|
||||
const foreignKeysObject: Record<string, ForeignKey> = {};
|
||||
const primaryKeysObject: Record<string, PrimaryKey> = {};
|
||||
const uniqueConstraintObject: Record<string, UniqueConstraint> = {};
|
||||
const checkConstraintObject: Record<string, CheckConstraint> = {};
|
||||
|
||||
// this object will help to identify same check names
|
||||
let checksInTable: Record<string, string[]> = {};
|
||||
|
||||
columns.forEach((column) => {
|
||||
const name = getColumnCasing(column, casing);
|
||||
const notNull: boolean = column.notNull;
|
||||
const sqlType = column.getSQLType();
|
||||
const sqlTypeLowered = sqlType.toLowerCase();
|
||||
const autoIncrement = typeof (column as any).autoIncrement === 'undefined'
|
||||
? false
|
||||
: (column as any).autoIncrement;
|
||||
|
||||
const generated = column.generated;
|
||||
|
||||
const columnToSet: Column = {
|
||||
name,
|
||||
type: sqlType.startsWith('enum') ? handleEnumType(sqlType) : sqlType,
|
||||
primaryKey: false,
|
||||
// If field is autoincrement it's notNull by default
|
||||
// notNull: autoIncrement ? true : notNull,
|
||||
notNull,
|
||||
autoincrement: autoIncrement,
|
||||
onUpdate: (column as any).hasOnUpdateNow,
|
||||
generated: generated
|
||||
? {
|
||||
as: is(generated.as, SQL)
|
||||
? dialect.sqlToQuery(generated.as as SQL).sql
|
||||
: typeof generated.as === 'function'
|
||||
? dialect.sqlToQuery(generated.as() as SQL).sql
|
||||
: (generated.as as any),
|
||||
type: generated.mode ?? 'stored',
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
if (column.primary) {
|
||||
primaryKeysObject[`${tableName}_${name}`] = {
|
||||
name: `${tableName}_${name}`,
|
||||
columns: [name],
|
||||
};
|
||||
}
|
||||
|
||||
if (column.isUnique) {
|
||||
const existingUnique = uniqueConstraintObject[column.uniqueName!];
|
||||
if (typeof existingUnique !== 'undefined') {
|
||||
console.log(
|
||||
`\n${
|
||||
withStyle.errorWarning(`We\'ve found duplicated unique constraint names in ${
|
||||
chalk.underline.blue(
|
||||
tableName,
|
||||
)
|
||||
} table.
|
||||
The unique constraint ${
|
||||
chalk.underline.blue(
|
||||
column.uniqueName,
|
||||
)
|
||||
} on the ${
|
||||
chalk.underline.blue(
|
||||
name,
|
||||
)
|
||||
} column is confilcting with a unique constraint name already defined for ${
|
||||
chalk.underline.blue(
|
||||
existingUnique.columns.join(','),
|
||||
)
|
||||
} columns\n`)
|
||||
}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
uniqueConstraintObject[column.uniqueName!] = {
|
||||
name: column.uniqueName!,
|
||||
columns: [columnToSet.name],
|
||||
};
|
||||
}
|
||||
|
||||
if (column.default !== undefined) {
|
||||
if (is(column.default, SQL)) {
|
||||
columnToSet.default = sqlToStr(column.default, casing);
|
||||
} else {
|
||||
if (typeof column.default === 'string') {
|
||||
columnToSet.default = `'${escapeSingleQuotes(column.default)}'`;
|
||||
} else {
|
||||
if (sqlTypeLowered === 'json') {
|
||||
columnToSet.default = `'${JSON.stringify(column.default)}'`;
|
||||
} else if (column.default instanceof Date) {
|
||||
if (sqlTypeLowered === 'date') {
|
||||
columnToSet.default = `'${column.default.toISOString().split('T')[0]}'`;
|
||||
} else if (
|
||||
sqlTypeLowered.startsWith('datetime')
|
||||
|| sqlTypeLowered.startsWith('timestamp')
|
||||
) {
|
||||
columnToSet.default = `'${
|
||||
column.default
|
||||
.toISOString()
|
||||
.replace('T', ' ')
|
||||
.slice(0, 23)
|
||||
}'`;
|
||||
}
|
||||
} else {
|
||||
columnToSet.default = column.default;
|
||||
}
|
||||
}
|
||||
if (['blob', 'text', 'json'].includes(column.getSQLType())) {
|
||||
columnToSet.default = `(${columnToSet.default})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
columnsObject[name] = columnToSet;
|
||||
});
|
||||
|
||||
primaryKeys.map((pk: PrimaryKeyORM) => {
|
||||
const originalColumnNames = pk.columns.map((c) => c.name);
|
||||
const columnNames = pk.columns.map((c: any) => getColumnCasing(c, casing));
|
||||
|
||||
let name = pk.getName();
|
||||
if (casing !== undefined) {
|
||||
for (let i = 0; i < originalColumnNames.length; i++) {
|
||||
name = name.replace(originalColumnNames[i], columnNames[i]);
|
||||
}
|
||||
}
|
||||
|
||||
primaryKeysObject[name] = {
|
||||
name,
|
||||
columns: columnNames,
|
||||
};
|
||||
|
||||
// all composite pk's should be treated as notNull
|
||||
for (const column of pk.columns) {
|
||||
columnsObject[getColumnCasing(column, casing)].notNull = true;
|
||||
}
|
||||
});
|
||||
|
||||
uniqueConstraints?.map((unq) => {
|
||||
const columnNames = unq.columns.map((c) => getColumnCasing(c, casing));
|
||||
|
||||
const name = unq.name ?? uniqueKeyName(table, columnNames);
|
||||
|
||||
const existingUnique = uniqueConstraintObject[name];
|
||||
if (typeof existingUnique !== 'undefined') {
|
||||
console.log(
|
||||
`\n${
|
||||
withStyle.errorWarning(
|
||||
`We\'ve found duplicated unique constraint names in ${
|
||||
chalk.underline.blue(
|
||||
tableName,
|
||||
)
|
||||
} table. \nThe unique constraint ${
|
||||
chalk.underline.blue(
|
||||
name,
|
||||
)
|
||||
} on the ${
|
||||
chalk.underline.blue(
|
||||
columnNames.join(','),
|
||||
)
|
||||
} columns is confilcting with a unique constraint name already defined for ${
|
||||
chalk.underline.blue(
|
||||
existingUnique.columns.join(','),
|
||||
)
|
||||
} columns\n`,
|
||||
)
|
||||
}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
uniqueConstraintObject[name] = {
|
||||
name: unq.name!,
|
||||
columns: columnNames,
|
||||
};
|
||||
});
|
||||
|
||||
const fks: ForeignKey[] = foreignKeys.map((fk) => {
|
||||
const tableFrom = tableName;
|
||||
const onDelete = fk.onDelete ?? 'no action';
|
||||
const onUpdate = fk.onUpdate ?? 'no action';
|
||||
const reference = fk.reference();
|
||||
|
||||
const referenceFT = reference.foreignTable;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
const tableTo = getTableName(referenceFT);
|
||||
|
||||
const originalColumnsFrom = reference.columns.map((it) => it.name);
|
||||
const columnsFrom = reference.columns.map((it) => getColumnCasing(it, casing));
|
||||
const originalColumnsTo = reference.foreignColumns.map((it) => it.name);
|
||||
const columnsTo = reference.foreignColumns.map((it) => getColumnCasing(it, casing));
|
||||
|
||||
let name = fk.getName();
|
||||
if (casing !== undefined) {
|
||||
for (let i = 0; i < originalColumnsFrom.length; i++) {
|
||||
name = name.replace(originalColumnsFrom[i], columnsFrom[i]);
|
||||
}
|
||||
for (let i = 0; i < originalColumnsTo.length; i++) {
|
||||
name = name.replace(originalColumnsTo[i], columnsTo[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
tableFrom,
|
||||
tableTo,
|
||||
columnsFrom,
|
||||
columnsTo,
|
||||
onDelete,
|
||||
onUpdate,
|
||||
} as ForeignKey;
|
||||
});
|
||||
|
||||
fks.forEach((it) => {
|
||||
foreignKeysObject[it.name] = it;
|
||||
});
|
||||
|
||||
indexes.forEach((value) => {
|
||||
const columns = value.config.columns;
|
||||
const name = value.config.name;
|
||||
|
||||
let indexColumns = columns.map((it) => {
|
||||
if (is(it, SQL)) {
|
||||
const sql = dialect.sqlToQuery(it, 'indexes').sql;
|
||||
if (typeof internal!.indexes![name] === 'undefined') {
|
||||
internal!.indexes![name] = {
|
||||
columns: {
|
||||
[sql]: {
|
||||
isExpression: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
if (typeof internal!.indexes![name]?.columns[sql] === 'undefined') {
|
||||
internal!.indexes![name]!.columns[sql] = {
|
||||
isExpression: true,
|
||||
};
|
||||
} else {
|
||||
internal!.indexes![name]!.columns[sql]!.isExpression = true;
|
||||
}
|
||||
}
|
||||
return sql;
|
||||
} else {
|
||||
return `${getColumnCasing(it, casing)}`;
|
||||
}
|
||||
});
|
||||
|
||||
if (value.config.unique) {
|
||||
if (typeof uniqueConstraintObject[name] !== 'undefined') {
|
||||
console.log(
|
||||
`\n${
|
||||
withStyle.errorWarning(
|
||||
`We\'ve found duplicated unique constraint names in ${
|
||||
chalk.underline.blue(
|
||||
tableName,
|
||||
)
|
||||
} table. \nThe unique index ${
|
||||
chalk.underline.blue(
|
||||
name,
|
||||
)
|
||||
} on the ${
|
||||
chalk.underline.blue(
|
||||
indexColumns.join(','),
|
||||
)
|
||||
} columns is confilcting with a unique constraint name already defined for ${
|
||||
chalk.underline.blue(
|
||||
uniqueConstraintObject[name].columns.join(','),
|
||||
)
|
||||
} columns\n`,
|
||||
)
|
||||
}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
if (typeof foreignKeysObject[name] !== 'undefined') {
|
||||
console.log(
|
||||
`\n${
|
||||
withStyle.errorWarning(
|
||||
`In MySQL, when creating a foreign key, an index is automatically generated with the same name as the foreign key constraint.\n\nWe have encountered a collision between the index name on columns ${
|
||||
chalk.underline.blue(
|
||||
indexColumns.join(','),
|
||||
)
|
||||
} and the foreign key on columns ${
|
||||
chalk.underline.blue(
|
||||
foreignKeysObject[name].columnsFrom.join(','),
|
||||
)
|
||||
}. Please change either the index name or the foreign key name. For more information, please refer to https://dev.mysql.com/doc/refman/8.0/en/constraint-foreign-key.html\n
|
||||
`,
|
||||
)
|
||||
}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
indexesObject[name] = {
|
||||
name,
|
||||
columns: indexColumns,
|
||||
isUnique: value.config.unique ?? false,
|
||||
using: value.config.using,
|
||||
algorithm: value.config.algorithm,
|
||||
lock: value.config.lock,
|
||||
};
|
||||
});
|
||||
|
||||
checks.forEach((check) => {
|
||||
check;
|
||||
const checkName = check.name;
|
||||
if (typeof checksInTable[tableName] !== 'undefined') {
|
||||
if (checksInTable[tableName].includes(check.name)) {
|
||||
console.log(
|
||||
`\n${
|
||||
withStyle.errorWarning(
|
||||
`We\'ve found duplicated check constraint name in ${
|
||||
chalk.underline.blue(
|
||||
tableName,
|
||||
)
|
||||
}. Please rename your check constraint in the ${
|
||||
chalk.underline.blue(
|
||||
tableName,
|
||||
)
|
||||
} table`,
|
||||
)
|
||||
}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
checksInTable[tableName].push(checkName);
|
||||
} else {
|
||||
checksInTable[tableName] = [check.name];
|
||||
}
|
||||
|
||||
checkConstraintObject[checkName] = {
|
||||
name: checkName,
|
||||
value: dialect.sqlToQuery(check.value).sql,
|
||||
};
|
||||
});
|
||||
|
||||
// only handle tables without schemas
|
||||
if (!schema) {
|
||||
result[tableName] = {
|
||||
name: tableName,
|
||||
columns: columnsObject,
|
||||
indexes: indexesObject,
|
||||
foreignKeys: foreignKeysObject,
|
||||
compositePrimaryKeys: primaryKeysObject,
|
||||
uniqueConstraints: uniqueConstraintObject,
|
||||
checkConstraint: checkConstraintObject,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
for (const view of views) {
|
||||
const {
|
||||
isExisting,
|
||||
name,
|
||||
query,
|
||||
schema,
|
||||
selectedFields,
|
||||
algorithm,
|
||||
sqlSecurity,
|
||||
withCheckOption,
|
||||
} = getViewConfig(view);
|
||||
|
||||
const columnsObject: Record<string, Column> = {};
|
||||
|
||||
const existingView = resultViews[name];
|
||||
if (typeof existingView !== 'undefined') {
|
||||
console.log(
|
||||
`\n${
|
||||
withStyle.errorWarning(
|
||||
`We\'ve found duplicated view name across ${
|
||||
chalk.underline.blue(
|
||||
schema ?? 'public',
|
||||
)
|
||||
} schema. Please rename your view`,
|
||||
)
|
||||
}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
for (const key in selectedFields) {
|
||||
if (is(selectedFields[key], MySqlColumn)) {
|
||||
const column = selectedFields[key];
|
||||
|
||||
const notNull: boolean = column.notNull;
|
||||
const sqlTypeLowered = column.getSQLType().toLowerCase();
|
||||
const autoIncrement = typeof (column as any).autoIncrement === 'undefined'
|
||||
? false
|
||||
: (column as any).autoIncrement;
|
||||
|
||||
const generated = column.generated;
|
||||
|
||||
const columnToSet: Column = {
|
||||
name: column.name,
|
||||
type: column.getSQLType(),
|
||||
primaryKey: false,
|
||||
// If field is autoincrement it's notNull by default
|
||||
// notNull: autoIncrement ? true : notNull,
|
||||
notNull,
|
||||
autoincrement: autoIncrement,
|
||||
onUpdate: (column as any).hasOnUpdateNow,
|
||||
generated: generated
|
||||
? {
|
||||
as: is(generated.as, SQL)
|
||||
? dialect.sqlToQuery(generated.as as SQL).sql
|
||||
: typeof generated.as === 'function'
|
||||
? dialect.sqlToQuery(generated.as() as SQL).sql
|
||||
: (generated.as as any),
|
||||
type: generated.mode ?? 'stored',
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
if (column.default !== undefined) {
|
||||
if (is(column.default, SQL)) {
|
||||
columnToSet.default = sqlToStr(column.default, casing);
|
||||
} else {
|
||||
if (typeof column.default === 'string') {
|
||||
columnToSet.default = `'${column.default}'`;
|
||||
} else {
|
||||
if (sqlTypeLowered === 'json') {
|
||||
columnToSet.default = `'${JSON.stringify(column.default)}'`;
|
||||
} else if (column.default instanceof Date) {
|
||||
if (sqlTypeLowered === 'date') {
|
||||
columnToSet.default = `'${column.default.toISOString().split('T')[0]}'`;
|
||||
} else if (
|
||||
sqlTypeLowered.startsWith('datetime')
|
||||
|| sqlTypeLowered.startsWith('timestamp')
|
||||
) {
|
||||
columnToSet.default = `'${
|
||||
column.default
|
||||
.toISOString()
|
||||
.replace('T', ' ')
|
||||
.slice(0, 23)
|
||||
}'`;
|
||||
}
|
||||
} else {
|
||||
columnToSet.default = column.default;
|
||||
}
|
||||
}
|
||||
if (['blob', 'text', 'json'].includes(column.getSQLType())) {
|
||||
columnToSet.default = `(${columnToSet.default})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
columnsObject[column.name] = columnToSet;
|
||||
}
|
||||
}
|
||||
|
||||
resultViews[name] = {
|
||||
columns: columnsObject,
|
||||
name,
|
||||
isExisting,
|
||||
definition: isExisting ? undefined : dialect.sqlToQuery(query!).sql,
|
||||
withCheckOption,
|
||||
algorithm: algorithm ?? 'undefined', // set default values
|
||||
sqlSecurity: sqlSecurity ?? 'definer', // set default values
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
version: '5',
|
||||
dialect: 'mysql',
|
||||
tables: result,
|
||||
views: resultViews,
|
||||
_meta: {
|
||||
tables: {},
|
||||
columns: {},
|
||||
},
|
||||
internal,
|
||||
};
|
||||
};
|
||||
|
||||
function clearDefaults(defaultValue: any, collate: string) {
|
||||
if (typeof collate === 'undefined' || collate === null) {
|
||||
collate = `utf8mb4`;
|
||||
}
|
||||
|
||||
let resultDefault = defaultValue;
|
||||
collate = `_${collate}`;
|
||||
if (defaultValue.startsWith(collate)) {
|
||||
resultDefault = resultDefault
|
||||
.substring(collate.length, defaultValue.length)
|
||||
.replace(/\\/g, '');
|
||||
if (resultDefault.startsWith("'") && resultDefault.endsWith("'")) {
|
||||
return `('${escapeSingleQuotes(resultDefault.substring(1, resultDefault.length - 1))}')`;
|
||||
} else {
|
||||
return `'${escapeSingleQuotes(resultDefault.substring(1, resultDefault.length - 1))}'`;
|
||||
}
|
||||
} else {
|
||||
return `(${resultDefault})`;
|
||||
}
|
||||
}
|
||||
|
||||
export const fromDatabase = async (
|
||||
db: DB,
|
||||
inputSchema: string,
|
||||
tablesFilter: (table: string) => boolean = (table) => true,
|
||||
progressCallback?: (
|
||||
stage: IntrospectStage,
|
||||
count: number,
|
||||
status: IntrospectStatus,
|
||||
) => void,
|
||||
): Promise<MySqlSchemaInternal> => {
|
||||
const result: Record<string, Table> = {};
|
||||
const internals: MySqlKitInternals = { tables: {}, indexes: {} };
|
||||
|
||||
const columns = await db.query(`select * from information_schema.columns
|
||||
where table_schema = '${inputSchema}' and table_name != '__drizzle_migrations'
|
||||
order by table_name, ordinal_position;`);
|
||||
|
||||
const response = columns as RowDataPacket[];
|
||||
|
||||
const schemas: string[] = [];
|
||||
|
||||
let columnsCount = 0;
|
||||
let tablesCount = new Set();
|
||||
let indexesCount = 0;
|
||||
let foreignKeysCount = 0;
|
||||
let checksCount = 0;
|
||||
let viewsCount = 0;
|
||||
|
||||
const idxs = await db.query(
|
||||
`select * from INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE INFORMATION_SCHEMA.STATISTICS.TABLE_SCHEMA = '${inputSchema}' and INFORMATION_SCHEMA.STATISTICS.INDEX_NAME != 'PRIMARY';`,
|
||||
);
|
||||
|
||||
const idxRows = idxs as RowDataPacket[];
|
||||
|
||||
for (const column of response) {
|
||||
if (!tablesFilter(column['TABLE_NAME'] as string)) continue;
|
||||
|
||||
columnsCount += 1;
|
||||
if (progressCallback) {
|
||||
progressCallback('columns', columnsCount, 'fetching');
|
||||
}
|
||||
const schema: string = column['TABLE_SCHEMA'];
|
||||
const tableName = column['TABLE_NAME'];
|
||||
|
||||
tablesCount.add(`${schema}.${tableName}`);
|
||||
if (progressCallback) {
|
||||
progressCallback('columns', tablesCount.size, 'fetching');
|
||||
}
|
||||
const columnName: string = column['COLUMN_NAME'];
|
||||
const isNullable = column['IS_NULLABLE'] === 'YES'; // 'YES', 'NO'
|
||||
const dataType = column['DATA_TYPE']; // varchar
|
||||
const columnType = column['COLUMN_TYPE']; // varchar(256)
|
||||
const isPrimary = column['COLUMN_KEY'] === 'PRI'; // 'PRI', ''
|
||||
const columnDefault: string = column['COLUMN_DEFAULT'];
|
||||
const collation: string = column['CHARACTER_SET_NAME'];
|
||||
const geenratedExpression: string = column['GENERATION_EXPRESSION'];
|
||||
|
||||
let columnExtra = column['EXTRA'];
|
||||
let isAutoincrement = false; // 'auto_increment', ''
|
||||
let isDefaultAnExpression = false; // 'auto_increment', ''
|
||||
|
||||
if (typeof column['EXTRA'] !== 'undefined') {
|
||||
columnExtra = column['EXTRA'];
|
||||
isAutoincrement = column['EXTRA'] === 'auto_increment'; // 'auto_increment', ''
|
||||
isDefaultAnExpression = column['EXTRA'].includes('DEFAULT_GENERATED'); // 'auto_increment', ''
|
||||
}
|
||||
|
||||
// if (isPrimary) {
|
||||
// if (typeof tableToPk[tableName] === "undefined") {
|
||||
// tableToPk[tableName] = [columnName];
|
||||
// } else {
|
||||
// tableToPk[tableName].push(columnName);
|
||||
// }
|
||||
// }
|
||||
|
||||
if (schema !== inputSchema) {
|
||||
schemas.push(schema);
|
||||
}
|
||||
|
||||
const table = result[tableName];
|
||||
|
||||
// let changedType = columnType.replace("bigint unsigned", "serial")
|
||||
let changedType = columnType;
|
||||
|
||||
if (columnType === 'bigint unsigned' && !isNullable && isAutoincrement) {
|
||||
// check unique here
|
||||
const uniqueIdx = idxRows.filter(
|
||||
(it) =>
|
||||
it['COLUMN_NAME'] === columnName
|
||||
&& it['TABLE_NAME'] === tableName
|
||||
&& it['NON_UNIQUE'] === 0,
|
||||
);
|
||||
if (uniqueIdx && uniqueIdx.length === 1) {
|
||||
changedType = columnType.replace('bigint unsigned', 'serial');
|
||||
}
|
||||
}
|
||||
|
||||
if (columnType.includes('decimal(10,0)')) {
|
||||
changedType = columnType.replace('decimal(10,0)', 'decimal');
|
||||
}
|
||||
|
||||
let onUpdate: boolean | undefined = undefined;
|
||||
if (
|
||||
columnType.startsWith('timestamp')
|
||||
&& typeof columnExtra !== 'undefined'
|
||||
&& columnExtra.includes('on update CURRENT_TIMESTAMP')
|
||||
) {
|
||||
onUpdate = true;
|
||||
}
|
||||
|
||||
const newColumn: Column = {
|
||||
default: columnDefault === null || columnDefault === undefined
|
||||
? undefined
|
||||
: /^-?[\d.]+(?:e-?\d+)?$/.test(columnDefault)
|
||||
&& !['decimal', 'char', 'varchar'].some((type) => columnType.startsWith(type))
|
||||
? Number(columnDefault)
|
||||
: isDefaultAnExpression
|
||||
? clearDefaults(columnDefault, collation)
|
||||
: `'${escapeSingleQuotes(columnDefault)}'`,
|
||||
autoincrement: isAutoincrement,
|
||||
name: columnName,
|
||||
type: changedType,
|
||||
primaryKey: false,
|
||||
notNull: !isNullable,
|
||||
onUpdate,
|
||||
generated: geenratedExpression
|
||||
? {
|
||||
as: geenratedExpression,
|
||||
type: columnExtra === 'VIRTUAL GENERATED' ? 'virtual' : 'stored',
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
// Set default to internal object
|
||||
if (isDefaultAnExpression) {
|
||||
if (typeof internals!.tables![tableName] === 'undefined') {
|
||||
internals!.tables![tableName] = {
|
||||
columns: {
|
||||
[columnName]: {
|
||||
isDefaultAnExpression: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
if (
|
||||
typeof internals!.tables![tableName]!.columns[columnName]
|
||||
=== 'undefined'
|
||||
) {
|
||||
internals!.tables![tableName]!.columns[columnName] = {
|
||||
isDefaultAnExpression: true,
|
||||
};
|
||||
} else {
|
||||
internals!.tables![tableName]!.columns[
|
||||
columnName
|
||||
]!.isDefaultAnExpression = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!table) {
|
||||
result[tableName] = {
|
||||
name: tableName,
|
||||
columns: {
|
||||
[columnName]: newColumn,
|
||||
},
|
||||
compositePrimaryKeys: {},
|
||||
indexes: {},
|
||||
foreignKeys: {},
|
||||
uniqueConstraints: {},
|
||||
checkConstraint: {},
|
||||
};
|
||||
} else {
|
||||
result[tableName]!.columns[columnName] = newColumn;
|
||||
}
|
||||
}
|
||||
|
||||
const tablePks = await db.query(
|
||||
`SELECT table_name, column_name, ordinal_position
|
||||
FROM information_schema.table_constraints t
|
||||
LEFT JOIN information_schema.key_column_usage k
|
||||
USING(constraint_name,table_schema,table_name)
|
||||
WHERE t.constraint_type='PRIMARY KEY'
|
||||
and table_name != '__drizzle_migrations'
|
||||
AND t.table_schema = '${inputSchema}'
|
||||
ORDER BY ordinal_position`,
|
||||
);
|
||||
|
||||
const tableToPk: { [tname: string]: string[] } = {};
|
||||
|
||||
const tableToPkRows = tablePks as RowDataPacket[];
|
||||
for (const tableToPkRow of tableToPkRows) {
|
||||
const tableName: string = tableToPkRow['TABLE_NAME'];
|
||||
const columnName: string = tableToPkRow['COLUMN_NAME'];
|
||||
const position: string = tableToPkRow['ordinal_position'];
|
||||
|
||||
if (typeof result[tableName] === 'undefined') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof tableToPk[tableName] === 'undefined') {
|
||||
tableToPk[tableName] = [columnName];
|
||||
} else {
|
||||
tableToPk[tableName].push(columnName);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(tableToPk)) {
|
||||
// if (value.length > 1) {
|
||||
result[key].compositePrimaryKeys = {
|
||||
[`${key}_${value.join('_')}`]: {
|
||||
name: `${key}_${value.join('_')}`,
|
||||
columns: value,
|
||||
},
|
||||
};
|
||||
// } else if (value.length === 1) {
|
||||
// result[key].columns[value[0]].primaryKey = true;
|
||||
// } else {
|
||||
// }
|
||||
}
|
||||
if (progressCallback) {
|
||||
progressCallback('columns', columnsCount, 'done');
|
||||
progressCallback('tables', tablesCount.size, 'done');
|
||||
}
|
||||
try {
|
||||
const fks = await db.query(
|
||||
`SELECT
|
||||
kcu.TABLE_SCHEMA,
|
||||
kcu.TABLE_NAME,
|
||||
kcu.CONSTRAINT_NAME,
|
||||
kcu.COLUMN_NAME,
|
||||
kcu.REFERENCED_TABLE_SCHEMA,
|
||||
kcu.REFERENCED_TABLE_NAME,
|
||||
kcu.REFERENCED_COLUMN_NAME,
|
||||
rc.UPDATE_RULE,
|
||||
rc.DELETE_RULE
|
||||
FROM
|
||||
INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
|
||||
LEFT JOIN
|
||||
information_schema.referential_constraints rc
|
||||
ON kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
|
||||
WHERE kcu.TABLE_SCHEMA = '${inputSchema}' AND kcu.CONSTRAINT_NAME != 'PRIMARY'
|
||||
AND kcu.REFERENCED_TABLE_NAME IS NOT NULL;`,
|
||||
);
|
||||
|
||||
const fkRows = fks as RowDataPacket[];
|
||||
|
||||
for (const fkRow of fkRows) {
|
||||
foreignKeysCount += 1;
|
||||
if (progressCallback) {
|
||||
progressCallback('fks', foreignKeysCount, 'fetching');
|
||||
}
|
||||
const tableSchema = fkRow['TABLE_SCHEMA'];
|
||||
const tableName: string = fkRow['TABLE_NAME'];
|
||||
const constraintName = fkRow['CONSTRAINT_NAME'];
|
||||
const columnName: string = fkRow['COLUMN_NAME'];
|
||||
const refTableSchema = fkRow['REFERENCED_TABLE_SCHEMA'];
|
||||
const refTableName = fkRow['REFERENCED_TABLE_NAME'];
|
||||
const refColumnName: string = fkRow['REFERENCED_COLUMN_NAME'];
|
||||
const updateRule: string = fkRow['UPDATE_RULE'];
|
||||
const deleteRule = fkRow['DELETE_RULE'];
|
||||
|
||||
const tableInResult = result[tableName];
|
||||
if (typeof tableInResult === 'undefined') continue;
|
||||
|
||||
if (typeof tableInResult.foreignKeys[constraintName] !== 'undefined') {
|
||||
tableInResult.foreignKeys[constraintName]!.columnsFrom.push(columnName);
|
||||
tableInResult.foreignKeys[constraintName]!.columnsTo.push(
|
||||
refColumnName,
|
||||
);
|
||||
} else {
|
||||
tableInResult.foreignKeys[constraintName] = {
|
||||
name: constraintName,
|
||||
tableFrom: tableName,
|
||||
tableTo: refTableName,
|
||||
columnsFrom: [columnName],
|
||||
columnsTo: [refColumnName],
|
||||
onDelete: deleteRule?.toLowerCase(),
|
||||
onUpdate: updateRule?.toLowerCase(),
|
||||
};
|
||||
}
|
||||
|
||||
tableInResult.foreignKeys[constraintName]!.columnsFrom = [
|
||||
...new Set(tableInResult.foreignKeys[constraintName]!.columnsFrom),
|
||||
];
|
||||
|
||||
tableInResult.foreignKeys[constraintName]!.columnsTo = [
|
||||
...new Set(tableInResult.foreignKeys[constraintName]!.columnsTo),
|
||||
];
|
||||
}
|
||||
} catch (e) {
|
||||
// console.log(`Can't proccess foreign keys`);
|
||||
}
|
||||
if (progressCallback) {
|
||||
progressCallback('fks', foreignKeysCount, 'done');
|
||||
}
|
||||
|
||||
for (const idxRow of idxRows) {
|
||||
const tableSchema = idxRow['TABLE_SCHEMA'];
|
||||
const tableName = idxRow['TABLE_NAME'];
|
||||
const constraintName = idxRow['INDEX_NAME'];
|
||||
const columnName: string = idxRow['COLUMN_NAME'];
|
||||
const isUnique = idxRow['NON_UNIQUE'] === 0;
|
||||
|
||||
const tableInResult = result[tableName];
|
||||
if (typeof tableInResult === 'undefined') continue;
|
||||
|
||||
// if (tableInResult.columns[columnName].type === "serial") continue;
|
||||
|
||||
indexesCount += 1;
|
||||
if (progressCallback) {
|
||||
progressCallback('indexes', indexesCount, 'fetching');
|
||||
}
|
||||
|
||||
if (isUnique) {
|
||||
if (
|
||||
typeof tableInResult.uniqueConstraints[constraintName] !== 'undefined'
|
||||
) {
|
||||
tableInResult.uniqueConstraints[constraintName]!.columns.push(
|
||||
columnName,
|
||||
);
|
||||
} else {
|
||||
tableInResult.uniqueConstraints[constraintName] = {
|
||||
name: constraintName,
|
||||
columns: [columnName],
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// in MySQL FK creates index by default. Name of index is the same as fk constraint name
|
||||
// so for introspect we will just skip it
|
||||
if (typeof tableInResult.foreignKeys[constraintName] === 'undefined') {
|
||||
if (typeof tableInResult.indexes[constraintName] !== 'undefined') {
|
||||
tableInResult.indexes[constraintName]!.columns.push(columnName);
|
||||
} else {
|
||||
tableInResult.indexes[constraintName] = {
|
||||
name: constraintName,
|
||||
columns: [columnName],
|
||||
isUnique: isUnique,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const views = await db.query(
|
||||
`select * from INFORMATION_SCHEMA.VIEWS WHERE table_schema = '${inputSchema}';`,
|
||||
);
|
||||
|
||||
const resultViews: Record<string, View> = {};
|
||||
|
||||
viewsCount = views.length;
|
||||
if (progressCallback) {
|
||||
progressCallback('views', viewsCount, 'fetching');
|
||||
}
|
||||
for await (const view of views) {
|
||||
const viewName = view['TABLE_NAME'];
|
||||
const definition = view['VIEW_DEFINITION'];
|
||||
|
||||
const withCheckOption = view['CHECK_OPTION'] === 'NONE' ? undefined : view['CHECK_OPTION'].toLowerCase();
|
||||
const sqlSecurity = view['SECURITY_TYPE'].toLowerCase();
|
||||
|
||||
const [createSqlStatement] = await db.query(`SHOW CREATE VIEW \`${viewName}\`;`);
|
||||
const algorithmMatch = createSqlStatement['Create View'].match(/ALGORITHM=([^ ]+)/);
|
||||
const algorithm = algorithmMatch ? algorithmMatch[1].toLowerCase() : undefined;
|
||||
|
||||
const columns = result[viewName].columns;
|
||||
delete result[viewName];
|
||||
|
||||
resultViews[viewName] = {
|
||||
columns: columns,
|
||||
isExisting: false,
|
||||
name: viewName,
|
||||
algorithm,
|
||||
definition,
|
||||
sqlSecurity,
|
||||
withCheckOption,
|
||||
};
|
||||
}
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback('indexes', indexesCount, 'done');
|
||||
// progressCallback("enums", 0, "fetching");
|
||||
progressCallback('enums', 0, 'done');
|
||||
progressCallback('views', viewsCount, 'done');
|
||||
}
|
||||
|
||||
const checkConstraints = await db.query(
|
||||
`SELECT
|
||||
tc.table_name,
|
||||
tc.constraint_name,
|
||||
cc.check_clause
|
||||
FROM
|
||||
information_schema.table_constraints tc
|
||||
JOIN
|
||||
information_schema.check_constraints cc
|
||||
ON tc.constraint_name = cc.constraint_name
|
||||
WHERE
|
||||
tc.constraint_schema = '${inputSchema}'
|
||||
AND
|
||||
tc.constraint_type = 'CHECK';`,
|
||||
);
|
||||
|
||||
checksCount += checkConstraints.length;
|
||||
if (progressCallback) {
|
||||
progressCallback('checks', checksCount, 'fetching');
|
||||
}
|
||||
for (const checkConstraintRow of checkConstraints) {
|
||||
const constraintName = checkConstraintRow['CONSTRAINT_NAME'];
|
||||
const constraintValue = checkConstraintRow['CHECK_CLAUSE'];
|
||||
const tableName = checkConstraintRow['TABLE_NAME'];
|
||||
|
||||
const tableInResult = result[tableName];
|
||||
// if (typeof tableInResult === 'undefined') continue;
|
||||
|
||||
tableInResult.checkConstraint[constraintName] = {
|
||||
name: constraintName,
|
||||
value: constraintValue,
|
||||
};
|
||||
}
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback('checks', checksCount, 'done');
|
||||
}
|
||||
|
||||
return {
|
||||
version: '5',
|
||||
dialect: 'mysql',
|
||||
tables: result,
|
||||
views: resultViews,
|
||||
_meta: {
|
||||
tables: {},
|
||||
columns: {},
|
||||
},
|
||||
internal: internals,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
import { is } from 'drizzle-orm';
|
||||
import type { AnyPgTable, PgEnum, PgMaterializedView, PgSequence, PgView } from 'drizzle-orm/pg-core';
|
||||
import {
|
||||
isPgEnum,
|
||||
isPgMaterializedView,
|
||||
isPgSequence,
|
||||
isPgView,
|
||||
PgPolicy,
|
||||
PgRole,
|
||||
PgSchema,
|
||||
PgTable,
|
||||
} from 'drizzle-orm/pg-core';
|
||||
import { Relations } from 'drizzle-orm/relations';
|
||||
import { safeRegister } from '../cli/commands/utils';
|
||||
|
||||
export const prepareFromExports = (exports: Record<string, unknown>) => {
|
||||
const tables: AnyPgTable[] = [];
|
||||
const enums: PgEnum<any>[] = [];
|
||||
const schemas: PgSchema[] = [];
|
||||
const sequences: PgSequence[] = [];
|
||||
const roles: PgRole[] = [];
|
||||
const policies: PgPolicy[] = [];
|
||||
const views: PgView[] = [];
|
||||
const matViews: PgMaterializedView[] = [];
|
||||
const relations: Relations[] = [];
|
||||
|
||||
const i0values = Object.values(exports);
|
||||
i0values.forEach((t) => {
|
||||
if (isPgEnum(t)) {
|
||||
enums.push(t);
|
||||
return;
|
||||
}
|
||||
if (is(t, PgTable)) {
|
||||
tables.push(t);
|
||||
}
|
||||
|
||||
if (is(t, PgSchema)) {
|
||||
schemas.push(t);
|
||||
}
|
||||
|
||||
if (isPgView(t)) {
|
||||
views.push(t);
|
||||
}
|
||||
|
||||
if (isPgMaterializedView(t)) {
|
||||
matViews.push(t);
|
||||
}
|
||||
|
||||
if (isPgSequence(t)) {
|
||||
sequences.push(t);
|
||||
}
|
||||
|
||||
if (is(t, PgRole)) {
|
||||
roles.push(t);
|
||||
}
|
||||
|
||||
if (is(t, PgPolicy)) {
|
||||
policies.push(t);
|
||||
}
|
||||
|
||||
if (is(t, Relations)) {
|
||||
relations.push(t);
|
||||
}
|
||||
});
|
||||
|
||||
return { tables, enums, schemas, sequences, views, matViews, roles, policies, relations };
|
||||
};
|
||||
|
||||
export const prepareFromPgImports = async (imports: string[]) => {
|
||||
const tables: AnyPgTable[] = [];
|
||||
const enums: PgEnum<any>[] = [];
|
||||
const schemas: PgSchema[] = [];
|
||||
const sequences: PgSequence[] = [];
|
||||
const views: PgView[] = [];
|
||||
const roles: PgRole[] = [];
|
||||
const policies: PgPolicy[] = [];
|
||||
const matViews: PgMaterializedView[] = [];
|
||||
const relations: Relations[] = [];
|
||||
|
||||
await safeRegister(async () => {
|
||||
for (let i = 0; i < imports.length; i++) {
|
||||
const it = imports[i];
|
||||
|
||||
const i0: Record<string, unknown> = require(`${it}`);
|
||||
const prepared = prepareFromExports(i0);
|
||||
|
||||
tables.push(...prepared.tables);
|
||||
enums.push(...prepared.enums);
|
||||
schemas.push(...prepared.schemas);
|
||||
sequences.push(...prepared.sequences);
|
||||
views.push(...prepared.views);
|
||||
matViews.push(...prepared.matViews);
|
||||
roles.push(...prepared.roles);
|
||||
policies.push(...prepared.policies);
|
||||
relations.push(...prepared.relations);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
tables: Array.from(new Set(tables)),
|
||||
enums,
|
||||
schemas,
|
||||
sequences,
|
||||
views,
|
||||
matViews,
|
||||
roles,
|
||||
policies,
|
||||
relations,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,886 @@
|
||||
import { mapValues, originUUID, snapshotVersion } from '../global';
|
||||
|
||||
import { any, array, boolean, enum as enumType, literal, number, object, record, string, TypeOf, union } from 'zod';
|
||||
|
||||
const indexV2 = object({
|
||||
name: string(),
|
||||
columns: record(
|
||||
string(),
|
||||
object({
|
||||
name: string(),
|
||||
}),
|
||||
),
|
||||
isUnique: boolean(),
|
||||
}).strict();
|
||||
|
||||
const columnV2 = object({
|
||||
name: string(),
|
||||
type: string(),
|
||||
primaryKey: boolean(),
|
||||
notNull: boolean(),
|
||||
default: any().optional(),
|
||||
references: string().optional(),
|
||||
}).strict();
|
||||
|
||||
const tableV2 = object({
|
||||
name: string(),
|
||||
columns: record(string(), columnV2),
|
||||
indexes: record(string(), indexV2),
|
||||
}).strict();
|
||||
|
||||
const enumSchemaV1 = object({
|
||||
name: string(),
|
||||
values: record(string(), string()),
|
||||
}).strict();
|
||||
|
||||
const enumSchema = object({
|
||||
name: string(),
|
||||
schema: string(),
|
||||
values: string().array(),
|
||||
}).strict();
|
||||
|
||||
export const pgSchemaV2 = object({
|
||||
version: literal('2'),
|
||||
tables: record(string(), tableV2),
|
||||
enums: record(string(), enumSchemaV1),
|
||||
}).strict();
|
||||
|
||||
// ------- V1 --------
|
||||
const references = object({
|
||||
foreignKeyName: string(),
|
||||
table: string(),
|
||||
column: string(),
|
||||
onDelete: string().optional(),
|
||||
onUpdate: string().optional(),
|
||||
}).strict();
|
||||
|
||||
const columnV1 = object({
|
||||
name: string(),
|
||||
type: string(),
|
||||
primaryKey: boolean(),
|
||||
notNull: boolean(),
|
||||
default: any().optional(),
|
||||
references: references.optional(),
|
||||
}).strict();
|
||||
|
||||
const tableV1 = object({
|
||||
name: string(),
|
||||
columns: record(string(), columnV1),
|
||||
indexes: record(string(), indexV2),
|
||||
}).strict();
|
||||
|
||||
export const pgSchemaV1 = object({
|
||||
version: literal('1'),
|
||||
tables: record(string(), tableV1),
|
||||
enums: record(string(), enumSchemaV1),
|
||||
}).strict();
|
||||
|
||||
const indexColumn = object({
|
||||
expression: string(),
|
||||
isExpression: boolean(),
|
||||
asc: boolean(),
|
||||
nulls: string().optional(),
|
||||
opclass: string().optional(),
|
||||
});
|
||||
|
||||
export type IndexColumnType = TypeOf<typeof indexColumn>;
|
||||
|
||||
const index = object({
|
||||
name: string(),
|
||||
columns: indexColumn.array(),
|
||||
isUnique: boolean(),
|
||||
with: record(string(), any()).optional(),
|
||||
method: string().default('btree'),
|
||||
where: string().optional(),
|
||||
concurrently: boolean().default(false),
|
||||
}).strict();
|
||||
|
||||
const indexV4 = object({
|
||||
name: string(),
|
||||
columns: string().array(),
|
||||
isUnique: boolean(),
|
||||
with: record(string(), string()).optional(),
|
||||
method: string().default('btree'),
|
||||
where: string().optional(),
|
||||
concurrently: boolean().default(false),
|
||||
}).strict();
|
||||
|
||||
const indexV5 = object({
|
||||
name: string(),
|
||||
columns: string().array(),
|
||||
isUnique: boolean(),
|
||||
with: record(string(), string()).optional(),
|
||||
method: string().default('btree'),
|
||||
where: string().optional(),
|
||||
concurrently: boolean().default(false),
|
||||
}).strict();
|
||||
|
||||
const indexV6 = object({
|
||||
name: string(),
|
||||
columns: string().array(),
|
||||
isUnique: boolean(),
|
||||
with: record(string(), string()).optional(),
|
||||
method: string().default('btree'),
|
||||
where: string().optional(),
|
||||
concurrently: boolean().default(false),
|
||||
}).strict();
|
||||
|
||||
const fk = object({
|
||||
name: string(),
|
||||
tableFrom: string(),
|
||||
columnsFrom: string().array(),
|
||||
tableTo: string(),
|
||||
schemaTo: string().optional(),
|
||||
columnsTo: string().array(),
|
||||
onUpdate: string().optional(),
|
||||
onDelete: string().optional(),
|
||||
}).strict();
|
||||
|
||||
export const sequenceSchema = object({
|
||||
name: string(),
|
||||
increment: string().optional(),
|
||||
minValue: string().optional(),
|
||||
maxValue: string().optional(),
|
||||
startWith: string().optional(),
|
||||
cache: string().optional(),
|
||||
cycle: boolean().optional(),
|
||||
schema: string(),
|
||||
}).strict();
|
||||
|
||||
export const roleSchema = object({
|
||||
name: string(),
|
||||
createDb: boolean().optional(),
|
||||
createRole: boolean().optional(),
|
||||
inherit: boolean().optional(),
|
||||
}).strict();
|
||||
|
||||
export const sequenceSquashed = object({
|
||||
name: string(),
|
||||
schema: string(),
|
||||
values: string(),
|
||||
}).strict();
|
||||
|
||||
const columnV7 = object({
|
||||
name: string(),
|
||||
type: string(),
|
||||
typeSchema: string().optional(),
|
||||
primaryKey: boolean(),
|
||||
notNull: boolean(),
|
||||
default: any().optional(),
|
||||
isUnique: any().optional(),
|
||||
uniqueName: string().optional(),
|
||||
nullsNotDistinct: boolean().optional(),
|
||||
}).strict();
|
||||
|
||||
const column = object({
|
||||
name: string(),
|
||||
type: string(),
|
||||
typeSchema: string().optional(),
|
||||
primaryKey: boolean(),
|
||||
notNull: boolean(),
|
||||
default: any().optional(),
|
||||
isUnique: any().optional(),
|
||||
uniqueName: string().optional(),
|
||||
nullsNotDistinct: boolean().optional(),
|
||||
generated: object({
|
||||
type: literal('stored'),
|
||||
as: string(),
|
||||
}).optional(),
|
||||
identity: sequenceSchema
|
||||
.merge(object({ type: enumType(['always', 'byDefault']) }))
|
||||
.optional(),
|
||||
}).strict();
|
||||
|
||||
const checkConstraint = object({
|
||||
name: string(),
|
||||
value: string(),
|
||||
}).strict();
|
||||
|
||||
const columnSquashed = object({
|
||||
name: string(),
|
||||
type: string(),
|
||||
typeSchema: string().optional(),
|
||||
primaryKey: boolean(),
|
||||
notNull: boolean(),
|
||||
default: any().optional(),
|
||||
isUnique: any().optional(),
|
||||
uniqueName: string().optional(),
|
||||
nullsNotDistinct: boolean().optional(),
|
||||
generated: object({
|
||||
type: literal('stored'),
|
||||
as: string(),
|
||||
}).optional(),
|
||||
identity: string().optional(),
|
||||
}).strict();
|
||||
|
||||
const tableV3 = object({
|
||||
name: string(),
|
||||
columns: record(string(), column),
|
||||
indexes: record(string(), index),
|
||||
foreignKeys: record(string(), fk),
|
||||
}).strict();
|
||||
|
||||
const compositePK = object({
|
||||
name: string(),
|
||||
columns: string().array(),
|
||||
}).strict();
|
||||
|
||||
const uniqueConstraint = object({
|
||||
name: string(),
|
||||
columns: string().array(),
|
||||
nullsNotDistinct: boolean(),
|
||||
}).strict();
|
||||
|
||||
export const policy = object({
|
||||
name: string(),
|
||||
as: enumType(['PERMISSIVE', 'RESTRICTIVE']).optional(),
|
||||
for: enumType(['ALL', 'SELECT', 'INSERT', 'UPDATE', 'DELETE']).optional(),
|
||||
to: string().array().optional(),
|
||||
using: string().optional(),
|
||||
withCheck: string().optional(),
|
||||
on: string().optional(),
|
||||
schema: string().optional(),
|
||||
}).strict();
|
||||
|
||||
export const policySquashed = object({
|
||||
name: string(),
|
||||
values: string(),
|
||||
}).strict();
|
||||
|
||||
const viewWithOption = object({
|
||||
checkOption: enumType(['local', 'cascaded']).optional(),
|
||||
securityBarrier: boolean().optional(),
|
||||
securityInvoker: boolean().optional(),
|
||||
}).strict();
|
||||
|
||||
const matViewWithOption = object({
|
||||
fillfactor: number().optional(),
|
||||
toastTupleTarget: number().optional(),
|
||||
parallelWorkers: number().optional(),
|
||||
autovacuumEnabled: boolean().optional(),
|
||||
vacuumIndexCleanup: enumType(['auto', 'off', 'on']).optional(),
|
||||
vacuumTruncate: boolean().optional(),
|
||||
autovacuumVacuumThreshold: number().optional(),
|
||||
autovacuumVacuumScaleFactor: number().optional(),
|
||||
autovacuumVacuumCostDelay: number().optional(),
|
||||
autovacuumVacuumCostLimit: number().optional(),
|
||||
autovacuumFreezeMinAge: number().optional(),
|
||||
autovacuumFreezeMaxAge: number().optional(),
|
||||
autovacuumFreezeTableAge: number().optional(),
|
||||
autovacuumMultixactFreezeMinAge: number().optional(),
|
||||
autovacuumMultixactFreezeMaxAge: number().optional(),
|
||||
autovacuumMultixactFreezeTableAge: number().optional(),
|
||||
logAutovacuumMinDuration: number().optional(),
|
||||
userCatalogTable: boolean().optional(),
|
||||
}).strict();
|
||||
|
||||
export const mergedViewWithOption = viewWithOption.merge(matViewWithOption).strict();
|
||||
|
||||
export const view = object({
|
||||
name: string(),
|
||||
schema: string(),
|
||||
columns: record(string(), column),
|
||||
definition: string().optional(),
|
||||
materialized: boolean(),
|
||||
with: mergedViewWithOption.optional(),
|
||||
isExisting: boolean(),
|
||||
withNoData: boolean().optional(),
|
||||
using: string().optional(),
|
||||
tablespace: string().optional(),
|
||||
}).strict();
|
||||
|
||||
const tableV4 = object({
|
||||
name: string(),
|
||||
schema: string(),
|
||||
columns: record(string(), column),
|
||||
indexes: record(string(), indexV4),
|
||||
foreignKeys: record(string(), fk),
|
||||
}).strict();
|
||||
|
||||
const tableV5 = object({
|
||||
name: string(),
|
||||
schema: string(),
|
||||
columns: record(string(), column),
|
||||
indexes: record(string(), indexV5),
|
||||
foreignKeys: record(string(), fk),
|
||||
compositePrimaryKeys: record(string(), compositePK),
|
||||
uniqueConstraints: record(string(), uniqueConstraint).default({}),
|
||||
}).strict();
|
||||
|
||||
const tableV6 = object({
|
||||
name: string(),
|
||||
schema: string(),
|
||||
columns: record(string(), column),
|
||||
indexes: record(string(), indexV6),
|
||||
foreignKeys: record(string(), fk),
|
||||
compositePrimaryKeys: record(string(), compositePK),
|
||||
uniqueConstraints: record(string(), uniqueConstraint).default({}),
|
||||
}).strict();
|
||||
|
||||
const tableV7 = object({
|
||||
name: string(),
|
||||
schema: string(),
|
||||
columns: record(string(), columnV7),
|
||||
indexes: record(string(), index),
|
||||
foreignKeys: record(string(), fk),
|
||||
compositePrimaryKeys: record(string(), compositePK),
|
||||
uniqueConstraints: record(string(), uniqueConstraint).default({}),
|
||||
}).strict();
|
||||
|
||||
const table = object({
|
||||
name: string(),
|
||||
schema: string(),
|
||||
columns: record(string(), column),
|
||||
indexes: record(string(), index),
|
||||
foreignKeys: record(string(), fk),
|
||||
compositePrimaryKeys: record(string(), compositePK),
|
||||
uniqueConstraints: record(string(), uniqueConstraint).default({}),
|
||||
policies: record(string(), policy).default({}),
|
||||
checkConstraints: record(string(), checkConstraint).default({}),
|
||||
isRLSEnabled: boolean().default(false),
|
||||
}).strict();
|
||||
|
||||
const schemaHash = object({
|
||||
id: string(),
|
||||
prevId: string(),
|
||||
});
|
||||
|
||||
export const kitInternals = object({
|
||||
tables: record(
|
||||
string(),
|
||||
object({
|
||||
columns: record(
|
||||
string(),
|
||||
object({
|
||||
isArray: boolean().optional(),
|
||||
dimensions: number().optional(),
|
||||
rawType: string().optional(),
|
||||
isDefaultAnExpression: boolean().optional(),
|
||||
}).optional(),
|
||||
),
|
||||
}).optional(),
|
||||
),
|
||||
}).optional();
|
||||
|
||||
export const pgSchemaInternalV3 = object({
|
||||
version: literal('3'),
|
||||
dialect: literal('pg'),
|
||||
tables: record(string(), tableV3),
|
||||
enums: record(string(), enumSchemaV1),
|
||||
}).strict();
|
||||
|
||||
export const pgSchemaInternalV4 = object({
|
||||
version: literal('4'),
|
||||
dialect: literal('pg'),
|
||||
tables: record(string(), tableV4),
|
||||
enums: record(string(), enumSchemaV1),
|
||||
schemas: record(string(), string()),
|
||||
}).strict();
|
||||
|
||||
// "table" -> "schema.table" for schema proper support
|
||||
export const pgSchemaInternalV5 = object({
|
||||
version: literal('5'),
|
||||
dialect: literal('pg'),
|
||||
tables: record(string(), tableV5),
|
||||
enums: record(string(), enumSchemaV1),
|
||||
schemas: record(string(), string()),
|
||||
_meta: object({
|
||||
schemas: record(string(), string()),
|
||||
tables: record(string(), string()),
|
||||
columns: record(string(), string()),
|
||||
}),
|
||||
internal: kitInternals,
|
||||
}).strict();
|
||||
|
||||
export const pgSchemaInternalV6 = object({
|
||||
version: literal('6'),
|
||||
dialect: literal('postgresql'),
|
||||
tables: record(string(), tableV6),
|
||||
enums: record(string(), enumSchema),
|
||||
schemas: record(string(), string()),
|
||||
_meta: object({
|
||||
schemas: record(string(), string()),
|
||||
tables: record(string(), string()),
|
||||
columns: record(string(), string()),
|
||||
}),
|
||||
internal: kitInternals,
|
||||
}).strict();
|
||||
|
||||
export const pgSchemaExternal = object({
|
||||
version: literal('5'),
|
||||
dialect: literal('pg'),
|
||||
tables: array(table),
|
||||
enums: array(enumSchemaV1),
|
||||
schemas: array(object({ name: string() })),
|
||||
_meta: object({
|
||||
schemas: record(string(), string()),
|
||||
tables: record(string(), string()),
|
||||
columns: record(string(), string()),
|
||||
}),
|
||||
}).strict();
|
||||
|
||||
export const pgSchemaInternalV7 = object({
|
||||
version: literal('7'),
|
||||
dialect: literal('postgresql'),
|
||||
tables: record(string(), tableV7),
|
||||
enums: record(string(), enumSchema),
|
||||
schemas: record(string(), string()),
|
||||
sequences: record(string(), sequenceSchema),
|
||||
_meta: object({
|
||||
schemas: record(string(), string()),
|
||||
tables: record(string(), string()),
|
||||
columns: record(string(), string()),
|
||||
}),
|
||||
internal: kitInternals,
|
||||
}).strict();
|
||||
|
||||
export const pgSchemaInternal = object({
|
||||
version: literal('7'),
|
||||
dialect: literal('postgresql'),
|
||||
tables: record(string(), table),
|
||||
enums: record(string(), enumSchema),
|
||||
schemas: record(string(), string()),
|
||||
views: record(string(), view).default({}),
|
||||
sequences: record(string(), sequenceSchema).default({}),
|
||||
roles: record(string(), roleSchema).default({}),
|
||||
policies: record(string(), policy).default({}),
|
||||
_meta: object({
|
||||
schemas: record(string(), string()),
|
||||
tables: record(string(), string()),
|
||||
columns: record(string(), string()),
|
||||
}),
|
||||
internal: kitInternals,
|
||||
}).strict();
|
||||
|
||||
const tableSquashed = object({
|
||||
name: string(),
|
||||
schema: string(),
|
||||
columns: record(string(), columnSquashed),
|
||||
indexes: record(string(), string()),
|
||||
foreignKeys: record(string(), string()),
|
||||
compositePrimaryKeys: record(string(), string()),
|
||||
uniqueConstraints: record(string(), string()),
|
||||
policies: record(string(), string()),
|
||||
checkConstraints: record(string(), string()),
|
||||
isRLSEnabled: boolean().default(false),
|
||||
}).strict();
|
||||
|
||||
const tableSquashedV4 = object({
|
||||
name: string(),
|
||||
schema: string(),
|
||||
columns: record(string(), column),
|
||||
indexes: record(string(), string()),
|
||||
foreignKeys: record(string(), string()),
|
||||
}).strict();
|
||||
|
||||
export const pgSchemaSquashedV4 = object({
|
||||
version: literal('4'),
|
||||
dialect: literal('pg'),
|
||||
tables: record(string(), tableSquashedV4),
|
||||
enums: record(string(), enumSchemaV1),
|
||||
schemas: record(string(), string()),
|
||||
}).strict();
|
||||
|
||||
export const pgSchemaSquashedV6 = object({
|
||||
version: literal('6'),
|
||||
dialect: literal('postgresql'),
|
||||
tables: record(string(), tableSquashed),
|
||||
enums: record(string(), enumSchema),
|
||||
schemas: record(string(), string()),
|
||||
}).strict();
|
||||
|
||||
export const pgSchemaSquashed = object({
|
||||
version: literal('7'),
|
||||
dialect: literal('postgresql'),
|
||||
tables: record(string(), tableSquashed),
|
||||
enums: record(string(), enumSchema),
|
||||
schemas: record(string(), string()),
|
||||
views: record(string(), view),
|
||||
sequences: record(string(), sequenceSquashed),
|
||||
roles: record(string(), roleSchema).default({}),
|
||||
policies: record(string(), policySquashed).default({}),
|
||||
}).strict();
|
||||
|
||||
export const pgSchemaV3 = pgSchemaInternalV3.merge(schemaHash);
|
||||
export const pgSchemaV4 = pgSchemaInternalV4.merge(schemaHash);
|
||||
export const pgSchemaV5 = pgSchemaInternalV5.merge(schemaHash);
|
||||
export const pgSchemaV6 = pgSchemaInternalV6.merge(schemaHash);
|
||||
export const pgSchemaV7 = pgSchemaInternalV7.merge(schemaHash);
|
||||
export const pgSchema = pgSchemaInternal.merge(schemaHash);
|
||||
|
||||
export type Enum = TypeOf<typeof enumSchema>;
|
||||
export type Sequence = TypeOf<typeof sequenceSchema>;
|
||||
export type Role = TypeOf<typeof roleSchema>;
|
||||
export type Column = TypeOf<typeof column>;
|
||||
export type TableV3 = TypeOf<typeof tableV3>;
|
||||
export type TableV4 = TypeOf<typeof tableV4>;
|
||||
export type TableV5 = TypeOf<typeof tableV5>;
|
||||
export type Table = TypeOf<typeof table>;
|
||||
export type PgSchema = TypeOf<typeof pgSchema>;
|
||||
export type PgSchemaInternal = TypeOf<typeof pgSchemaInternal>;
|
||||
export type PgSchemaV6Internal = TypeOf<typeof pgSchemaInternalV6>;
|
||||
export type PgSchemaExternal = TypeOf<typeof pgSchemaExternal>;
|
||||
export type PgSchemaSquashed = TypeOf<typeof pgSchemaSquashed>;
|
||||
export type PgSchemaSquashedV4 = TypeOf<typeof pgSchemaSquashedV4>;
|
||||
export type PgSchemaSquashedV6 = TypeOf<typeof pgSchemaSquashedV6>;
|
||||
export type Index = TypeOf<typeof index>;
|
||||
export type ForeignKey = TypeOf<typeof fk>;
|
||||
export type PrimaryKey = TypeOf<typeof compositePK>;
|
||||
export type UniqueConstraint = TypeOf<typeof uniqueConstraint>;
|
||||
export type Policy = TypeOf<typeof policy>;
|
||||
export type View = TypeOf<typeof view>;
|
||||
export type MatViewWithOption = TypeOf<typeof matViewWithOption>;
|
||||
export type ViewWithOption = TypeOf<typeof viewWithOption>;
|
||||
|
||||
export type PgKitInternals = TypeOf<typeof kitInternals>;
|
||||
export type CheckConstraint = TypeOf<typeof checkConstraint>;
|
||||
|
||||
export type PgSchemaV1 = TypeOf<typeof pgSchemaV1>;
|
||||
export type PgSchemaV2 = TypeOf<typeof pgSchemaV2>;
|
||||
export type PgSchemaV3 = TypeOf<typeof pgSchemaV3>;
|
||||
export type PgSchemaV4 = TypeOf<typeof pgSchemaV4>;
|
||||
export type PgSchemaV5 = TypeOf<typeof pgSchemaV5>;
|
||||
export type PgSchemaV6 = TypeOf<typeof pgSchemaV6>;
|
||||
|
||||
export const backwardCompatiblePgSchema = union([
|
||||
pgSchemaV5,
|
||||
pgSchemaV6,
|
||||
pgSchema,
|
||||
]);
|
||||
|
||||
export const PgSquasher = {
|
||||
squashIdx: (idx: Index) => {
|
||||
index.parse(idx);
|
||||
return `${idx.name};${
|
||||
idx.columns
|
||||
.map(
|
||||
(c) => `${c.expression}--${c.isExpression}--${c.asc}--${c.nulls}--${c.opclass ? c.opclass : ''}`,
|
||||
)
|
||||
.join(',,')
|
||||
};${idx.isUnique};${idx.concurrently};${idx.method};${idx.where};${JSON.stringify(idx.with)}`;
|
||||
},
|
||||
unsquashIdx: (input: string): Index => {
|
||||
const [
|
||||
name,
|
||||
columnsString,
|
||||
isUnique,
|
||||
concurrently,
|
||||
method,
|
||||
where,
|
||||
idxWith,
|
||||
] = input.split(';');
|
||||
|
||||
const columnString = columnsString.split(',,');
|
||||
const columns: IndexColumnType[] = [];
|
||||
|
||||
for (const column of columnString) {
|
||||
const [expression, isExpression, asc, nulls, opclass] = column.split('--');
|
||||
columns.push({
|
||||
nulls: nulls as IndexColumnType['nulls'],
|
||||
isExpression: isExpression === 'true',
|
||||
asc: asc === 'true',
|
||||
expression: expression,
|
||||
opclass: opclass === 'undefined' ? undefined : opclass,
|
||||
});
|
||||
}
|
||||
|
||||
const result: Index = index.parse({
|
||||
name,
|
||||
columns: columns,
|
||||
isUnique: isUnique === 'true',
|
||||
concurrently: concurrently === 'true',
|
||||
method,
|
||||
where: where === 'undefined' ? undefined : where,
|
||||
with: !idxWith || idxWith === 'undefined' ? undefined : JSON.parse(idxWith),
|
||||
});
|
||||
return result;
|
||||
},
|
||||
squashIdxPush: (idx: Index) => {
|
||||
index.parse(idx);
|
||||
return `${idx.name};${
|
||||
idx.columns
|
||||
.map((c) => `${c.isExpression ? '' : c.expression}--${c.asc}--${c.nulls}`)
|
||||
.join(',,')
|
||||
};${idx.isUnique};${idx.method};${JSON.stringify(idx.with)}`;
|
||||
},
|
||||
unsquashIdxPush: (input: string): Index => {
|
||||
const [name, columnsString, isUnique, method, idxWith] = input.split(';');
|
||||
|
||||
const columnString = columnsString.split('--');
|
||||
const columns: IndexColumnType[] = [];
|
||||
|
||||
for (const column of columnString) {
|
||||
const [expression, asc, nulls, opclass] = column.split(',');
|
||||
columns.push({
|
||||
nulls: nulls as IndexColumnType['nulls'],
|
||||
isExpression: expression === '',
|
||||
asc: asc === 'true',
|
||||
expression: expression,
|
||||
});
|
||||
}
|
||||
|
||||
const result: Index = index.parse({
|
||||
name,
|
||||
columns: columns,
|
||||
isUnique: isUnique === 'true',
|
||||
concurrently: false,
|
||||
method,
|
||||
with: idxWith === 'undefined' ? undefined : JSON.parse(idxWith),
|
||||
});
|
||||
return result;
|
||||
},
|
||||
squashFK: (fk: ForeignKey) => {
|
||||
return `${fk.name};${fk.tableFrom};${fk.columnsFrom.join(',')};${fk.tableTo};${fk.columnsTo.join(',')};${
|
||||
fk.onUpdate ?? ''
|
||||
};${fk.onDelete ?? ''};${fk.schemaTo || 'public'}`;
|
||||
},
|
||||
squashPolicy: (policy: Policy) => {
|
||||
return `${policy.name}--${policy.as}--${policy.for}--${
|
||||
policy.to?.join(',')
|
||||
}--${policy.using}--${policy.withCheck}--${policy.on}`;
|
||||
},
|
||||
unsquashPolicy: (policy: string): Policy => {
|
||||
const splitted = policy.split('--');
|
||||
return {
|
||||
name: splitted[0],
|
||||
as: splitted[1] as Policy['as'],
|
||||
for: splitted[2] as Policy['for'],
|
||||
to: splitted[3].split(','),
|
||||
using: splitted[4] !== 'undefined' ? splitted[4] : undefined,
|
||||
withCheck: splitted[5] !== 'undefined' ? splitted[5] : undefined,
|
||||
on: splitted[6] !== 'undefined' ? splitted[6] : undefined,
|
||||
};
|
||||
},
|
||||
squashPolicyPush: (policy: Policy) => {
|
||||
return `${policy.name}--${policy.as}--${policy.for}--${policy.to?.join(',')}--${policy.on}`;
|
||||
},
|
||||
unsquashPolicyPush: (policy: string): Policy => {
|
||||
const splitted = policy.split('--');
|
||||
return {
|
||||
name: splitted[0],
|
||||
as: splitted[1] as Policy['as'],
|
||||
for: splitted[2] as Policy['for'],
|
||||
to: splitted[3].split(','),
|
||||
on: splitted[4] !== 'undefined' ? splitted[4] : undefined,
|
||||
};
|
||||
},
|
||||
squashPK: (pk: PrimaryKey) => {
|
||||
return `${pk.columns.join(',')};${pk.name}`;
|
||||
},
|
||||
unsquashPK: (pk: string): PrimaryKey => {
|
||||
const splitted = pk.split(';');
|
||||
return { name: splitted[1], columns: splitted[0].split(',') };
|
||||
},
|
||||
squashUnique: (unq: UniqueConstraint) => {
|
||||
return `${unq.name};${unq.columns.join(',')};${unq.nullsNotDistinct}`;
|
||||
},
|
||||
unsquashUnique: (unq: string): UniqueConstraint => {
|
||||
const [name, columns, nullsNotDistinct] = unq.split(';');
|
||||
return {
|
||||
name,
|
||||
columns: columns.split(','),
|
||||
nullsNotDistinct: nullsNotDistinct === 'true',
|
||||
};
|
||||
},
|
||||
unsquashFK: (input: string): ForeignKey => {
|
||||
const [
|
||||
name,
|
||||
tableFrom,
|
||||
columnsFromStr,
|
||||
tableTo,
|
||||
columnsToStr,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
schemaTo,
|
||||
] = input.split(';');
|
||||
|
||||
const result: ForeignKey = fk.parse({
|
||||
name,
|
||||
tableFrom,
|
||||
columnsFrom: columnsFromStr.split(','),
|
||||
schemaTo: schemaTo,
|
||||
tableTo,
|
||||
columnsTo: columnsToStr.split(','),
|
||||
onUpdate,
|
||||
onDelete,
|
||||
});
|
||||
return result;
|
||||
},
|
||||
squashSequence: (seq: Omit<Sequence, 'name' | 'schema'>) => {
|
||||
return `${seq.minValue};${seq.maxValue};${seq.increment};${seq.startWith};${seq.cache};${seq.cycle ?? ''}`;
|
||||
},
|
||||
unsquashSequence: (seq: string): Omit<Sequence, 'name' | 'schema'> => {
|
||||
const splitted = seq.split(';');
|
||||
return {
|
||||
minValue: splitted[0] !== 'undefined' ? splitted[0] : undefined,
|
||||
maxValue: splitted[1] !== 'undefined' ? splitted[1] : undefined,
|
||||
increment: splitted[2] !== 'undefined' ? splitted[2] : undefined,
|
||||
startWith: splitted[3] !== 'undefined' ? splitted[3] : undefined,
|
||||
cache: splitted[4] !== 'undefined' ? splitted[4] : undefined,
|
||||
cycle: splitted[5] === 'true',
|
||||
};
|
||||
},
|
||||
squashIdentity: (
|
||||
seq: Omit<Sequence, 'schema'> & { type: 'always' | 'byDefault' },
|
||||
) => {
|
||||
return `${seq.name};${seq.type};${seq.minValue};${seq.maxValue};${seq.increment};${seq.startWith};${seq.cache};${
|
||||
seq.cycle ?? ''
|
||||
}`;
|
||||
},
|
||||
unsquashIdentity: (
|
||||
seq: string,
|
||||
): Omit<Sequence, 'schema'> & { type: 'always' | 'byDefault' } => {
|
||||
const splitted = seq.split(';');
|
||||
return {
|
||||
name: splitted[0],
|
||||
type: splitted[1] as 'always' | 'byDefault',
|
||||
minValue: splitted[2] !== 'undefined' ? splitted[2] : undefined,
|
||||
maxValue: splitted[3] !== 'undefined' ? splitted[3] : undefined,
|
||||
increment: splitted[4] !== 'undefined' ? splitted[4] : undefined,
|
||||
startWith: splitted[5] !== 'undefined' ? splitted[5] : undefined,
|
||||
cache: splitted[6] !== 'undefined' ? splitted[6] : undefined,
|
||||
cycle: splitted[7] === 'true',
|
||||
};
|
||||
},
|
||||
squashCheck: (check: CheckConstraint) => {
|
||||
return `${check.name};${check.value}`;
|
||||
},
|
||||
unsquashCheck: (input: string): CheckConstraint => {
|
||||
const [
|
||||
name,
|
||||
value,
|
||||
] = input.split(';');
|
||||
|
||||
return { name, value };
|
||||
},
|
||||
};
|
||||
|
||||
export const squashPgScheme = (
|
||||
json: PgSchema,
|
||||
action?: 'push' | undefined,
|
||||
): PgSchemaSquashed => {
|
||||
const mappedTables = Object.fromEntries(
|
||||
Object.entries(json.tables).map((it) => {
|
||||
const squashedIndexes = mapValues(it[1].indexes, (index) => {
|
||||
return action === 'push'
|
||||
? PgSquasher.squashIdxPush(index)
|
||||
: PgSquasher.squashIdx(index);
|
||||
});
|
||||
|
||||
const squashedFKs = mapValues(it[1].foreignKeys, (fk) => {
|
||||
return PgSquasher.squashFK(fk);
|
||||
});
|
||||
|
||||
const squashedPKs = mapValues(it[1].compositePrimaryKeys, (pk) => {
|
||||
return PgSquasher.squashPK(pk);
|
||||
});
|
||||
|
||||
const mappedColumns = Object.fromEntries(
|
||||
Object.entries(it[1].columns).map((it) => {
|
||||
const mappedIdentity = it[1].identity
|
||||
? PgSquasher.squashIdentity(it[1].identity)
|
||||
: undefined;
|
||||
return [
|
||||
it[0],
|
||||
{
|
||||
...it[1],
|
||||
identity: mappedIdentity,
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
const squashedUniqueConstraints = mapValues(
|
||||
it[1].uniqueConstraints,
|
||||
(unq) => {
|
||||
return PgSquasher.squashUnique(unq);
|
||||
},
|
||||
);
|
||||
|
||||
const squashedPolicies = mapValues(it[1].policies, (policy) => {
|
||||
return action === 'push'
|
||||
? PgSquasher.squashPolicyPush(policy)
|
||||
: PgSquasher.squashPolicy(policy);
|
||||
});
|
||||
const squashedChecksContraints = mapValues(
|
||||
it[1].checkConstraints,
|
||||
(check) => {
|
||||
return PgSquasher.squashCheck(check);
|
||||
},
|
||||
);
|
||||
|
||||
return [
|
||||
it[0],
|
||||
{
|
||||
name: it[1].name,
|
||||
schema: it[1].schema,
|
||||
columns: mappedColumns,
|
||||
indexes: squashedIndexes,
|
||||
foreignKeys: squashedFKs,
|
||||
compositePrimaryKeys: squashedPKs,
|
||||
uniqueConstraints: squashedUniqueConstraints,
|
||||
policies: squashedPolicies,
|
||||
checkConstraints: squashedChecksContraints,
|
||||
isRLSEnabled: it[1].isRLSEnabled ?? false,
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
const mappedSequences = Object.fromEntries(
|
||||
Object.entries(json.sequences).map((it) => {
|
||||
return [
|
||||
it[0],
|
||||
{
|
||||
name: it[1].name,
|
||||
schema: it[1].schema,
|
||||
values: PgSquasher.squashSequence(it[1]),
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
const mappedPolicies = Object.fromEntries(
|
||||
Object.entries(json.policies).map((it) => {
|
||||
return [
|
||||
it[0],
|
||||
{
|
||||
name: it[1].name,
|
||||
values: action === 'push'
|
||||
? PgSquasher.squashPolicyPush(it[1])
|
||||
: PgSquasher.squashPolicy(it[1]),
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
version: '7',
|
||||
dialect: json.dialect,
|
||||
tables: mappedTables,
|
||||
enums: json.enums,
|
||||
schemas: json.schemas,
|
||||
views: json.views,
|
||||
policies: mappedPolicies,
|
||||
sequences: mappedSequences,
|
||||
roles: json.roles,
|
||||
};
|
||||
};
|
||||
|
||||
export const dryPg = pgSchema.parse({
|
||||
version: snapshotVersion,
|
||||
dialect: 'postgresql',
|
||||
id: originUUID,
|
||||
prevId: '',
|
||||
tables: {},
|
||||
enums: {},
|
||||
schemas: {},
|
||||
policies: {},
|
||||
roles: {},
|
||||
sequences: {},
|
||||
_meta: {
|
||||
schemas: {},
|
||||
tables: {},
|
||||
columns: {},
|
||||
},
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
||||
import { is } from 'drizzle-orm';
|
||||
import type { AnySingleStoreTable } from 'drizzle-orm/singlestore-core';
|
||||
import { SingleStoreTable } from 'drizzle-orm/singlestore-core';
|
||||
import { safeRegister } from '../cli/commands/utils';
|
||||
|
||||
export const prepareFromExports = (exports: Record<string, unknown>) => {
|
||||
const tables: AnySingleStoreTable[] = [];
|
||||
/* const views: SingleStoreView[] = []; */
|
||||
|
||||
const i0values = Object.values(exports);
|
||||
i0values.forEach((t) => {
|
||||
if (is(t, SingleStoreTable)) {
|
||||
tables.push(t);
|
||||
}
|
||||
|
||||
/* if (is(t, SingleStoreView)) {
|
||||
views.push(t);
|
||||
} */
|
||||
});
|
||||
|
||||
return { tables /* views */ };
|
||||
};
|
||||
|
||||
export const prepareFromSingleStoreImports = async (imports: string[]) => {
|
||||
const tables: AnySingleStoreTable[] = [];
|
||||
/* const views: SingleStoreView[] = []; */
|
||||
|
||||
await safeRegister(async () => {
|
||||
for (let i = 0; i < imports.length; i++) {
|
||||
const it = imports[i];
|
||||
const i0: Record<string, unknown> = require(`${it}`);
|
||||
const prepared = prepareFromExports(i0);
|
||||
|
||||
tables.push(...prepared.tables);
|
||||
/* views.push(...prepared.views); */
|
||||
}
|
||||
});
|
||||
|
||||
return { tables: Array.from(new Set(tables)) /* , views */ };
|
||||
};
|
||||
@@ -0,0 +1,257 @@
|
||||
import { any, boolean, enum as enumType, literal, object, record, string, TypeOf, union } from 'zod';
|
||||
import { mapValues, originUUID, snapshotVersion } from '../global';
|
||||
|
||||
// ------- V3 --------
|
||||
const index = object({
|
||||
name: string(),
|
||||
columns: string().array(),
|
||||
isUnique: boolean(),
|
||||
using: enumType(['btree', 'hash']).optional(),
|
||||
algorithm: enumType(['default', 'inplace', 'copy']).optional(),
|
||||
lock: enumType(['default', 'none', 'shared', 'exclusive']).optional(),
|
||||
}).strict();
|
||||
|
||||
const column = object({
|
||||
name: string(),
|
||||
type: string(),
|
||||
primaryKey: boolean(),
|
||||
notNull: boolean(),
|
||||
autoincrement: boolean().optional(),
|
||||
default: any().optional(),
|
||||
onUpdate: any().optional(),
|
||||
generated: object({
|
||||
type: enumType(['stored', 'virtual']),
|
||||
as: string(),
|
||||
}).optional(),
|
||||
}).strict();
|
||||
|
||||
const compositePK = object({
|
||||
name: string(),
|
||||
columns: string().array(),
|
||||
}).strict();
|
||||
|
||||
const uniqueConstraint = object({
|
||||
name: string(),
|
||||
columns: string().array(),
|
||||
}).strict();
|
||||
|
||||
const table = object({
|
||||
name: string(),
|
||||
columns: record(string(), column),
|
||||
indexes: record(string(), index),
|
||||
compositePrimaryKeys: record(string(), compositePK),
|
||||
uniqueConstraints: record(string(), uniqueConstraint).default({}),
|
||||
}).strict();
|
||||
|
||||
const viewMeta = object({
|
||||
algorithm: enumType(['undefined', 'merge', 'temptable']),
|
||||
sqlSecurity: enumType(['definer', 'invoker']),
|
||||
withCheckOption: enumType(['local', 'cascaded']).optional(),
|
||||
}).strict();
|
||||
|
||||
/* export const view = object({
|
||||
name: string(),
|
||||
columns: record(string(), column),
|
||||
definition: string().optional(),
|
||||
isExisting: boolean(),
|
||||
}).strict().merge(viewMeta);
|
||||
type SquasherViewMeta = Omit<TypeOf<typeof viewMeta>, 'definer'>; */
|
||||
|
||||
export const kitInternals = object({
|
||||
tables: record(
|
||||
string(),
|
||||
object({
|
||||
columns: record(
|
||||
string(),
|
||||
object({ isDefaultAnExpression: boolean().optional() }).optional(),
|
||||
),
|
||||
}).optional(),
|
||||
).optional(),
|
||||
indexes: record(
|
||||
string(),
|
||||
object({
|
||||
columns: record(
|
||||
string(),
|
||||
object({ isExpression: boolean().optional() }).optional(),
|
||||
),
|
||||
}).optional(),
|
||||
).optional(),
|
||||
}).optional();
|
||||
|
||||
// use main dialect
|
||||
const dialect = literal('singlestore');
|
||||
|
||||
const schemaHash = object({
|
||||
id: string(),
|
||||
prevId: string(),
|
||||
});
|
||||
|
||||
export const schemaInternal = object({
|
||||
version: literal('1'),
|
||||
dialect: dialect,
|
||||
tables: record(string(), table),
|
||||
/* views: record(string(), view).default({}), */
|
||||
_meta: object({
|
||||
tables: record(string(), string()),
|
||||
columns: record(string(), string()),
|
||||
}),
|
||||
internal: kitInternals,
|
||||
}).strict();
|
||||
|
||||
export const schema = schemaInternal.merge(schemaHash);
|
||||
|
||||
const tableSquashed = object({
|
||||
name: string(),
|
||||
columns: record(string(), column),
|
||||
indexes: record(string(), string()),
|
||||
compositePrimaryKeys: record(string(), string()),
|
||||
uniqueConstraints: record(string(), string()).default({}),
|
||||
}).strict();
|
||||
|
||||
/* const viewSquashed = view.omit({
|
||||
algorithm: true,
|
||||
sqlSecurity: true,
|
||||
withCheckOption: true,
|
||||
}).extend({ meta: string() }); */
|
||||
|
||||
export const schemaSquashed = object({
|
||||
version: literal('1'),
|
||||
dialect: dialect,
|
||||
tables: record(string(), tableSquashed),
|
||||
/* views: record(string(), viewSquashed), */
|
||||
}).strict();
|
||||
|
||||
export type Dialect = TypeOf<typeof dialect>;
|
||||
export type Column = TypeOf<typeof column>;
|
||||
export type Table = TypeOf<typeof table>;
|
||||
export type SingleStoreSchema = TypeOf<typeof schema>;
|
||||
export type SingleStoreSchemaInternal = TypeOf<typeof schemaInternal>;
|
||||
export type SingleStoreKitInternals = TypeOf<typeof kitInternals>;
|
||||
export type SingleStoreSchemaSquashed = TypeOf<typeof schemaSquashed>;
|
||||
export type Index = TypeOf<typeof index>;
|
||||
export type PrimaryKey = TypeOf<typeof compositePK>;
|
||||
export type UniqueConstraint = TypeOf<typeof uniqueConstraint>;
|
||||
/* export type View = TypeOf<typeof view>; */
|
||||
/* export type ViewSquashed = TypeOf<typeof viewSquashed>; */
|
||||
|
||||
export const SingleStoreSquasher = {
|
||||
squashIdx: (idx: Index) => {
|
||||
index.parse(idx);
|
||||
return `${idx.name};${idx.columns.join(',')};${idx.isUnique};${idx.using ?? ''};${idx.algorithm ?? ''};${
|
||||
idx.lock ?? ''
|
||||
}`;
|
||||
},
|
||||
unsquashIdx: (input: string): Index => {
|
||||
const [name, columnsString, isUnique, using, algorithm, lock] = input.split(';');
|
||||
const destructed = {
|
||||
name,
|
||||
columns: columnsString.split(','),
|
||||
isUnique: isUnique === 'true',
|
||||
using: using ? using : undefined,
|
||||
algorithm: algorithm ? algorithm : undefined,
|
||||
lock: lock ? lock : undefined,
|
||||
};
|
||||
return index.parse(destructed);
|
||||
},
|
||||
squashPK: (pk: PrimaryKey) => {
|
||||
return `${pk.name};${pk.columns.join(',')}`;
|
||||
},
|
||||
unsquashPK: (pk: string): PrimaryKey => {
|
||||
const splitted = pk.split(';');
|
||||
return { name: splitted[0], columns: splitted[1].split(',') };
|
||||
},
|
||||
squashUnique: (unq: UniqueConstraint) => {
|
||||
return `${unq.name};${unq.columns.join(',')}`;
|
||||
},
|
||||
unsquashUnique: (unq: string): UniqueConstraint => {
|
||||
const [name, columns] = unq.split(';');
|
||||
return { name, columns: columns.split(',') };
|
||||
},
|
||||
/* squashView: (view: View): string => {
|
||||
return `${view.algorithm};${view.sqlSecurity};${view.withCheckOption}`;
|
||||
},
|
||||
unsquashView: (meta: string): SquasherViewMeta => {
|
||||
const [algorithm, sqlSecurity, withCheckOption] = meta.split(';');
|
||||
const toReturn = {
|
||||
algorithm: algorithm,
|
||||
sqlSecurity: sqlSecurity,
|
||||
withCheckOption: withCheckOption !== 'undefined' ? withCheckOption : undefined,
|
||||
};
|
||||
|
||||
return viewMeta.parse(toReturn);
|
||||
}, */
|
||||
};
|
||||
|
||||
export const squashSingleStoreScheme = (json: SingleStoreSchema): SingleStoreSchemaSquashed => {
|
||||
const mappedTables = Object.fromEntries(
|
||||
Object.entries(json.tables).map((it) => {
|
||||
const squashedIndexes = mapValues(it[1].indexes, (index) => {
|
||||
return SingleStoreSquasher.squashIdx(index);
|
||||
});
|
||||
|
||||
const squashedPKs = mapValues(it[1].compositePrimaryKeys, (pk) => {
|
||||
return SingleStoreSquasher.squashPK(pk);
|
||||
});
|
||||
|
||||
const squashedUniqueConstraints = mapValues(
|
||||
it[1].uniqueConstraints,
|
||||
(unq) => {
|
||||
return SingleStoreSquasher.squashUnique(unq);
|
||||
},
|
||||
);
|
||||
|
||||
return [
|
||||
it[0],
|
||||
{
|
||||
name: it[1].name,
|
||||
columns: it[1].columns,
|
||||
indexes: squashedIndexes,
|
||||
compositePrimaryKeys: squashedPKs,
|
||||
uniqueConstraints: squashedUniqueConstraints,
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
/* const mappedViews = Object.fromEntries(
|
||||
Object.entries(json.views).map(([key, value]) => {
|
||||
const meta = SingleStoreSquasher.squashView(value);
|
||||
|
||||
return [key, {
|
||||
name: value.name,
|
||||
isExisting: value.isExisting,
|
||||
columns: value.columns,
|
||||
definition: value.definition,
|
||||
meta,
|
||||
}];
|
||||
}),
|
||||
); */
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
dialect: json.dialect,
|
||||
tables: mappedTables,
|
||||
/* views: mappedViews, */
|
||||
};
|
||||
};
|
||||
|
||||
export const singlestoreSchema = schema;
|
||||
export const singlestoreSchemaSquashed = schemaSquashed;
|
||||
|
||||
// no prev version
|
||||
export const backwardCompatibleSingleStoreSchema = union([singlestoreSchema, schema]);
|
||||
|
||||
export const drySingleStore = singlestoreSchema.parse({
|
||||
version: '1',
|
||||
dialect: 'singlestore',
|
||||
id: originUUID,
|
||||
prevId: '',
|
||||
tables: {},
|
||||
schemas: {},
|
||||
/* views: {}, */
|
||||
_meta: {
|
||||
schemas: {},
|
||||
tables: {},
|
||||
columns: {},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,767 @@
|
||||
import chalk from 'chalk';
|
||||
import { is, SQL } from 'drizzle-orm';
|
||||
import {
|
||||
AnySingleStoreTable,
|
||||
getTableConfig,
|
||||
type PrimaryKey as PrimaryKeyORM,
|
||||
SingleStoreDialect,
|
||||
uniqueKeyName,
|
||||
} from 'drizzle-orm/singlestore-core';
|
||||
import { RowDataPacket } from 'mysql2/promise';
|
||||
import { withStyle } from '../cli/validations/outputs';
|
||||
import { IntrospectStage, IntrospectStatus } from '../cli/views';
|
||||
|
||||
import { CasingType } from 'src/cli/validations/common';
|
||||
import type { DB } from '../utils';
|
||||
import {
|
||||
Column,
|
||||
Index,
|
||||
PrimaryKey,
|
||||
SingleStoreKitInternals,
|
||||
SingleStoreSchemaInternal,
|
||||
Table,
|
||||
UniqueConstraint,
|
||||
} from './singlestoreSchema';
|
||||
import { sqlToStr } from './utils';
|
||||
|
||||
const dialect = new SingleStoreDialect();
|
||||
|
||||
export const indexName = (tableName: string, columns: string[]) => {
|
||||
return `${tableName}_${columns.join('_')}_index`;
|
||||
};
|
||||
|
||||
export const generateSingleStoreSnapshot = (
|
||||
tables: AnySingleStoreTable[],
|
||||
/* views: SingleStoreView[], */
|
||||
casing: CasingType | undefined,
|
||||
): SingleStoreSchemaInternal => {
|
||||
const dialect = new SingleStoreDialect({ casing });
|
||||
const result: Record<string, Table> = {};
|
||||
/* const resultViews: Record<string, View> = {}; */
|
||||
const internal: SingleStoreKitInternals = { tables: {}, indexes: {} };
|
||||
for (const table of tables) {
|
||||
const {
|
||||
name: tableName,
|
||||
columns,
|
||||
indexes,
|
||||
schema,
|
||||
primaryKeys,
|
||||
uniqueConstraints,
|
||||
} = getTableConfig(table);
|
||||
const columnsObject: Record<string, Column> = {};
|
||||
const indexesObject: Record<string, Index> = {};
|
||||
const primaryKeysObject: Record<string, PrimaryKey> = {};
|
||||
const uniqueConstraintObject: Record<string, UniqueConstraint> = {};
|
||||
|
||||
columns.forEach((column) => {
|
||||
const notNull: boolean = column.notNull;
|
||||
const sqlTypeLowered = column.getSQLType().toLowerCase();
|
||||
const autoIncrement = typeof (column as any).autoIncrement === 'undefined'
|
||||
? false
|
||||
: (column as any).autoIncrement;
|
||||
|
||||
const generated = column.generated;
|
||||
|
||||
const columnToSet: Column = {
|
||||
name: column.name,
|
||||
type: column.getSQLType(),
|
||||
primaryKey: false,
|
||||
// If field is autoincrement it's notNull by default
|
||||
// notNull: autoIncrement ? true : notNull,
|
||||
notNull,
|
||||
autoincrement: autoIncrement,
|
||||
onUpdate: (column as any).hasOnUpdateNow,
|
||||
generated: generated
|
||||
? {
|
||||
as: is(generated.as, SQL)
|
||||
? dialect.sqlToQuery(generated.as as SQL).sql
|
||||
: typeof generated.as === 'function'
|
||||
? dialect.sqlToQuery(generated.as() as SQL).sql
|
||||
: (generated.as as any),
|
||||
type: generated.mode ?? 'stored',
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
if (column.primary) {
|
||||
primaryKeysObject[`${tableName}_${column.name}`] = {
|
||||
name: `${tableName}_${column.name}`,
|
||||
columns: [column.name],
|
||||
};
|
||||
}
|
||||
|
||||
if (column.isUnique) {
|
||||
const existingUnique = uniqueConstraintObject[column.uniqueName!];
|
||||
if (typeof existingUnique !== 'undefined') {
|
||||
console.log(
|
||||
`\n${
|
||||
withStyle.errorWarning(`We\'ve found duplicated unique constraint names in ${
|
||||
chalk.underline.blue(
|
||||
tableName,
|
||||
)
|
||||
} table.
|
||||
The unique constraint ${
|
||||
chalk.underline.blue(
|
||||
column.uniqueName,
|
||||
)
|
||||
} on the ${
|
||||
chalk.underline.blue(
|
||||
column.name,
|
||||
)
|
||||
} column is confilcting with a unique constraint name already defined for ${
|
||||
chalk.underline.blue(
|
||||
existingUnique.columns.join(','),
|
||||
)
|
||||
} columns\n`)
|
||||
}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
uniqueConstraintObject[column.uniqueName!] = {
|
||||
name: column.uniqueName!,
|
||||
columns: [columnToSet.name],
|
||||
};
|
||||
}
|
||||
|
||||
if (column.default !== undefined) {
|
||||
if (is(column.default, SQL)) {
|
||||
columnToSet.default = sqlToStr(column.default, casing);
|
||||
} else {
|
||||
if (typeof column.default === 'string') {
|
||||
columnToSet.default = `'${column.default}'`;
|
||||
} else {
|
||||
if (sqlTypeLowered === 'json' || Array.isArray(column.default)) {
|
||||
columnToSet.default = `'${JSON.stringify(column.default)}'`;
|
||||
} else if (column.default instanceof Date) {
|
||||
if (sqlTypeLowered === 'date') {
|
||||
columnToSet.default = `'${column.default.toISOString().split('T')[0]}'`;
|
||||
} else if (
|
||||
sqlTypeLowered.startsWith('datetime')
|
||||
|| sqlTypeLowered.startsWith('timestamp')
|
||||
) {
|
||||
columnToSet.default = `'${
|
||||
column.default
|
||||
.toISOString()
|
||||
.replace('T', ' ')
|
||||
.slice(0, 23)
|
||||
}'`;
|
||||
}
|
||||
} else {
|
||||
columnToSet.default = column.default;
|
||||
}
|
||||
}
|
||||
// if (['blob', 'text', 'json'].includes(column.getSQLType())) {
|
||||
// columnToSet.default = `(${columnToSet.default})`;
|
||||
// }
|
||||
}
|
||||
}
|
||||
columnsObject[column.name] = columnToSet;
|
||||
});
|
||||
|
||||
primaryKeys.map((pk: PrimaryKeyORM) => {
|
||||
const columnNames = pk.columns.map((c: any) => c.name);
|
||||
primaryKeysObject[pk.getName()] = {
|
||||
name: pk.getName(),
|
||||
columns: columnNames,
|
||||
};
|
||||
|
||||
// all composite pk's should be treated as notNull
|
||||
for (const column of pk.columns) {
|
||||
columnsObject[column.name].notNull = true;
|
||||
}
|
||||
});
|
||||
|
||||
uniqueConstraints?.map((unq) => {
|
||||
const columnNames = unq.columns.map((c) => c.name);
|
||||
|
||||
const name = unq.name ?? uniqueKeyName(table, columnNames);
|
||||
|
||||
const existingUnique = uniqueConstraintObject[name];
|
||||
if (typeof existingUnique !== 'undefined') {
|
||||
console.log(
|
||||
`\n${
|
||||
withStyle.errorWarning(
|
||||
`We\'ve found duplicated unique constraint names in ${
|
||||
chalk.underline.blue(
|
||||
tableName,
|
||||
)
|
||||
} table. \nThe unique constraint ${
|
||||
chalk.underline.blue(
|
||||
name,
|
||||
)
|
||||
} on the ${
|
||||
chalk.underline.blue(
|
||||
columnNames.join(','),
|
||||
)
|
||||
} columns is confilcting with a unique constraint name already defined for ${
|
||||
chalk.underline.blue(
|
||||
existingUnique.columns.join(','),
|
||||
)
|
||||
} columns\n`,
|
||||
)
|
||||
}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
uniqueConstraintObject[name] = {
|
||||
name: unq.name!,
|
||||
columns: columnNames,
|
||||
};
|
||||
});
|
||||
|
||||
indexes.forEach((value) => {
|
||||
const columns = value.config.columns;
|
||||
const name = value.config.name;
|
||||
|
||||
let indexColumns = columns.map((it) => {
|
||||
if (is(it, SQL)) {
|
||||
const sql = dialect.sqlToQuery(it, 'indexes').sql;
|
||||
if (typeof internal!.indexes![name] === 'undefined') {
|
||||
internal!.indexes![name] = {
|
||||
columns: {
|
||||
[sql]: {
|
||||
isExpression: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
if (typeof internal!.indexes![name]?.columns[sql] === 'undefined') {
|
||||
internal!.indexes![name]!.columns[sql] = {
|
||||
isExpression: true,
|
||||
};
|
||||
} else {
|
||||
internal!.indexes![name]!.columns[sql]!.isExpression = true;
|
||||
}
|
||||
}
|
||||
return sql;
|
||||
} else {
|
||||
return `${it.name}`;
|
||||
}
|
||||
});
|
||||
|
||||
if (value.config.unique) {
|
||||
if (typeof uniqueConstraintObject[name] !== 'undefined') {
|
||||
console.log(
|
||||
`\n${
|
||||
withStyle.errorWarning(
|
||||
`We\'ve found duplicated unique constraint names in ${
|
||||
chalk.underline.blue(
|
||||
tableName,
|
||||
)
|
||||
} table. \nThe unique index ${
|
||||
chalk.underline.blue(
|
||||
name,
|
||||
)
|
||||
} on the ${
|
||||
chalk.underline.blue(
|
||||
indexColumns.join(','),
|
||||
)
|
||||
} columns is confilcting with a unique constraint name already defined for ${
|
||||
chalk.underline.blue(
|
||||
uniqueConstraintObject[name].columns.join(','),
|
||||
)
|
||||
} columns\n`,
|
||||
)
|
||||
}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
indexesObject[name] = {
|
||||
name,
|
||||
columns: indexColumns,
|
||||
isUnique: value.config.unique ?? false,
|
||||
using: value.config.using,
|
||||
algorithm: value.config.algorithm,
|
||||
lock: value.config.lock,
|
||||
};
|
||||
});
|
||||
|
||||
// only handle tables without schemas
|
||||
if (!schema) {
|
||||
result[tableName] = {
|
||||
name: tableName,
|
||||
columns: columnsObject,
|
||||
indexes: indexesObject,
|
||||
compositePrimaryKeys: primaryKeysObject,
|
||||
uniqueConstraints: uniqueConstraintObject,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/* for (const view of views) {
|
||||
const {
|
||||
isExisting,
|
||||
name,
|
||||
query,
|
||||
schema,
|
||||
selectedFields,
|
||||
algorithm,
|
||||
sqlSecurity,
|
||||
withCheckOption,
|
||||
} = getViewConfig(view);
|
||||
|
||||
const columnsObject: Record<string, Column> = {};
|
||||
|
||||
const existingView = resultViews[name];
|
||||
if (typeof existingView !== 'undefined') {
|
||||
console.log(
|
||||
`\n${
|
||||
withStyle.errorWarning(
|
||||
`We\'ve found duplicated view name across ${
|
||||
chalk.underline.blue(
|
||||
schema ?? 'public',
|
||||
)
|
||||
} schema. Please rename your view`,
|
||||
)
|
||||
}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
for (const key in selectedFields) {
|
||||
if (is(selectedFields[key], SingleStoreColumn)) {
|
||||
const column = selectedFields[key];
|
||||
|
||||
const notNull: boolean = column.notNull;
|
||||
const sqlTypeLowered = column.getSQLType().toLowerCase();
|
||||
const autoIncrement = typeof (column as any).autoIncrement === 'undefined'
|
||||
? false
|
||||
: (column as any).autoIncrement;
|
||||
|
||||
const generated = column.generated;
|
||||
|
||||
const columnToSet: Column = {
|
||||
name: column.name,
|
||||
type: column.getSQLType(),
|
||||
primaryKey: false,
|
||||
// If field is autoincrement it's notNull by default
|
||||
// notNull: autoIncrement ? true : notNull,
|
||||
notNull,
|
||||
autoincrement: autoIncrement,
|
||||
onUpdate: (column as any).hasOnUpdateNow,
|
||||
generated: generated
|
||||
? {
|
||||
as: is(generated.as, SQL)
|
||||
? dialect.sqlToQuery(generated.as as SQL).sql
|
||||
: typeof generated.as === 'function'
|
||||
? dialect.sqlToQuery(generated.as() as SQL).sql
|
||||
: (generated.as as any),
|
||||
type: generated.mode ?? 'stored',
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
if (column.default !== undefined) {
|
||||
if (is(column.default, SQL)) {
|
||||
columnToSet.default = sqlToStr(column.default, casing);
|
||||
} else {
|
||||
if (typeof column.default === 'string') {
|
||||
columnToSet.default = `'${column.default}'`;
|
||||
} else {
|
||||
if (sqlTypeLowered === 'json') {
|
||||
columnToSet.default = `'${JSON.stringify(column.default)}'`;
|
||||
} else if (column.default instanceof Date) {
|
||||
if (sqlTypeLowered === 'date') {
|
||||
columnToSet.default = `'${column.default.toISOString().split('T')[0]}'`;
|
||||
} else if (
|
||||
sqlTypeLowered.startsWith('datetime')
|
||||
|| sqlTypeLowered.startsWith('timestamp')
|
||||
) {
|
||||
columnToSet.default = `'${
|
||||
column.default
|
||||
.toISOString()
|
||||
.replace('T', ' ')
|
||||
.slice(0, 23)
|
||||
}'`;
|
||||
}
|
||||
} else {
|
||||
columnToSet.default = column.default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
columnsObject[column.name] = columnToSet;
|
||||
}
|
||||
}
|
||||
|
||||
resultViews[name] = {
|
||||
columns: columnsObject,
|
||||
name,
|
||||
isExisting,
|
||||
definition: isExisting ? undefined : dialect.sqlToQuery(query!).sql,
|
||||
withCheckOption,
|
||||
algorithm: algorithm ?? 'undefined', // set default values
|
||||
sqlSecurity: sqlSecurity ?? 'definer', // set default values
|
||||
};
|
||||
} */
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
dialect: 'singlestore',
|
||||
tables: result,
|
||||
/* views: resultViews, */
|
||||
_meta: {
|
||||
tables: {},
|
||||
columns: {},
|
||||
},
|
||||
internal,
|
||||
};
|
||||
};
|
||||
|
||||
function clearDefaults(defaultValue: any, collate: string) {
|
||||
if (typeof collate === 'undefined' || collate === null) {
|
||||
collate = `utf8mb4`;
|
||||
}
|
||||
|
||||
let resultDefault = defaultValue;
|
||||
collate = `_${collate}`;
|
||||
if (defaultValue.startsWith(collate)) {
|
||||
resultDefault = resultDefault
|
||||
.substring(collate.length, defaultValue.length)
|
||||
.replace(/\\/g, '');
|
||||
if (resultDefault.startsWith("'") && resultDefault.endsWith("'")) {
|
||||
return `('${resultDefault.substring(1, resultDefault.length - 1)}')`;
|
||||
} else {
|
||||
return `'${resultDefault}'`;
|
||||
}
|
||||
} else {
|
||||
return `(${resultDefault})`;
|
||||
}
|
||||
}
|
||||
|
||||
export const fromDatabase = async (
|
||||
db: DB,
|
||||
inputSchema: string,
|
||||
tablesFilter: (table: string) => boolean = (table) => true,
|
||||
progressCallback?: (
|
||||
stage: IntrospectStage,
|
||||
count: number,
|
||||
status: IntrospectStatus,
|
||||
) => void,
|
||||
): Promise<SingleStoreSchemaInternal> => {
|
||||
const result: Record<string, Table> = {};
|
||||
const internals: SingleStoreKitInternals = { tables: {}, indexes: {} };
|
||||
|
||||
const columns = await db.query(`select * from information_schema.columns
|
||||
where table_schema = '${inputSchema}' and table_name != '__drizzle_migrations'
|
||||
order by table_name, ordinal_position;`);
|
||||
|
||||
const response = columns as RowDataPacket[];
|
||||
|
||||
const schemas: string[] = [];
|
||||
|
||||
let columnsCount = 0;
|
||||
let tablesCount = new Set();
|
||||
let indexesCount = 0;
|
||||
/* let viewsCount = 0; */
|
||||
|
||||
const idxs = await db.query(
|
||||
`select * from INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE INFORMATION_SCHEMA.STATISTICS.TABLE_SCHEMA = '${inputSchema}' and INFORMATION_SCHEMA.STATISTICS.INDEX_NAME != 'PRIMARY';`,
|
||||
);
|
||||
|
||||
const idxRows = idxs as RowDataPacket[];
|
||||
|
||||
for (const column of response) {
|
||||
if (!tablesFilter(column['TABLE_NAME'] as string)) continue;
|
||||
|
||||
columnsCount += 1;
|
||||
if (progressCallback) {
|
||||
progressCallback('columns', columnsCount, 'fetching');
|
||||
}
|
||||
const schema: string = column['TABLE_SCHEMA'];
|
||||
const tableName = column['TABLE_NAME'];
|
||||
|
||||
tablesCount.add(`${schema}.${tableName}`);
|
||||
if (progressCallback) {
|
||||
progressCallback('columns', tablesCount.size, 'fetching');
|
||||
}
|
||||
const columnName: string = column['COLUMN_NAME'];
|
||||
const isNullable = column['IS_NULLABLE'] === 'YES'; // 'YES', 'NO'
|
||||
const dataType = column['DATA_TYPE']; // varchar
|
||||
const columnType = column['COLUMN_TYPE']; // varchar(256)
|
||||
// const columnType = column["DATA_TYPE"];
|
||||
const isPrimary = column['COLUMN_KEY'] === 'PRI'; // 'PRI', ''
|
||||
let columnDefault: string | null = column['COLUMN_DEFAULT'];
|
||||
const collation: string = column['CHARACTER_SET_NAME'];
|
||||
const geenratedExpression: string = column['GENERATION_EXPRESSION'];
|
||||
|
||||
let columnExtra = column['EXTRA'];
|
||||
let isAutoincrement = false; // 'auto_increment', ''
|
||||
let isDefaultAnExpression = false; // 'auto_increment', ''
|
||||
|
||||
if (typeof column['EXTRA'] !== 'undefined') {
|
||||
columnExtra = column['EXTRA'];
|
||||
isAutoincrement = column['EXTRA'] === 'auto_increment'; // 'auto_increment', ''
|
||||
isDefaultAnExpression = column['EXTRA'].includes('DEFAULT_GENERATED'); // 'auto_increment', ''
|
||||
}
|
||||
|
||||
// if (isPrimary) {
|
||||
// if (typeof tableToPk[tableName] === "undefined") {
|
||||
// tableToPk[tableName] = [columnName];
|
||||
// } else {
|
||||
// tableToPk[tableName].push(columnName);
|
||||
// }
|
||||
// }
|
||||
|
||||
if (schema !== inputSchema) {
|
||||
schemas.push(schema);
|
||||
}
|
||||
|
||||
const table = result[tableName];
|
||||
|
||||
// let changedType = columnType.replace("bigint unsigned", "serial")
|
||||
let changedType = columnType;
|
||||
|
||||
if (columnType === 'bigint unsigned' && !isNullable && isAutoincrement) {
|
||||
// check unique here
|
||||
const uniqueIdx = idxRows.filter(
|
||||
(it) =>
|
||||
it['COLUMN_NAME'] === columnName
|
||||
&& it['TABLE_NAME'] === tableName
|
||||
&& it['NON_UNIQUE'] === 0,
|
||||
);
|
||||
if (uniqueIdx && uniqueIdx.length === 1) {
|
||||
changedType = columnType.replace('bigint unsigned', 'serial');
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
columnType.startsWith('bigint(')
|
||||
|| columnType.startsWith('tinyint(')
|
||||
|| columnType.startsWith('date(')
|
||||
|| columnType.startsWith('int(')
|
||||
|| columnType.startsWith('mediumint(')
|
||||
|| columnType.startsWith('smallint(')
|
||||
|| columnType.startsWith('text(')
|
||||
|| columnType.startsWith('time(')
|
||||
|| columnType.startsWith('year(')
|
||||
) {
|
||||
changedType = columnType.replace(/\(\s*[^)]*\)$/, '');
|
||||
}
|
||||
|
||||
if (columnType.includes('decimal(10,0)')) {
|
||||
changedType = columnType.replace('decimal(10,0)', 'decimal');
|
||||
}
|
||||
|
||||
if (columnDefault?.endsWith('.')) {
|
||||
columnDefault = columnDefault.slice(0, -1);
|
||||
}
|
||||
|
||||
let onUpdate: boolean | undefined = undefined;
|
||||
if (
|
||||
columnType.startsWith('timestamp')
|
||||
&& typeof columnExtra !== 'undefined'
|
||||
&& columnExtra.includes('on update CURRENT_TIMESTAMP')
|
||||
) {
|
||||
onUpdate = true;
|
||||
}
|
||||
|
||||
const newColumn: Column = {
|
||||
default: columnDefault === null
|
||||
? undefined
|
||||
: /^-?[\d.]+(?:e-?\d+)?$/.test(columnDefault)
|
||||
&& !['decimal', 'char', 'varchar'].some((type) => columnType.startsWith(type))
|
||||
? Number(columnDefault)
|
||||
: isDefaultAnExpression
|
||||
? clearDefaults(columnDefault, collation)
|
||||
: columnDefault.startsWith('CURRENT_TIMESTAMP')
|
||||
? 'CURRENT_TIMESTAMP'
|
||||
: `'${columnDefault}'`,
|
||||
autoincrement: isAutoincrement,
|
||||
name: columnName,
|
||||
type: changedType,
|
||||
primaryKey: false,
|
||||
notNull: !isNullable,
|
||||
onUpdate,
|
||||
generated: geenratedExpression
|
||||
? {
|
||||
as: geenratedExpression,
|
||||
type: columnExtra === 'VIRTUAL GENERATED' ? 'virtual' : 'stored',
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
// Set default to internal object
|
||||
if (isDefaultAnExpression) {
|
||||
if (typeof internals!.tables![tableName] === 'undefined') {
|
||||
internals!.tables![tableName] = {
|
||||
columns: {
|
||||
[columnName]: {
|
||||
isDefaultAnExpression: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
if (
|
||||
typeof internals!.tables![tableName]!.columns[columnName]
|
||||
=== 'undefined'
|
||||
) {
|
||||
internals!.tables![tableName]!.columns[columnName] = {
|
||||
isDefaultAnExpression: true,
|
||||
};
|
||||
} else {
|
||||
internals!.tables![tableName]!.columns[
|
||||
columnName
|
||||
]!.isDefaultAnExpression = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!table) {
|
||||
result[tableName] = {
|
||||
name: tableName,
|
||||
columns: {
|
||||
[columnName]: newColumn,
|
||||
},
|
||||
compositePrimaryKeys: {},
|
||||
indexes: {},
|
||||
uniqueConstraints: {},
|
||||
};
|
||||
} else {
|
||||
result[tableName]!.columns[columnName] = newColumn;
|
||||
}
|
||||
}
|
||||
|
||||
const tablePks = await db.query(
|
||||
`SELECT table_name, column_name, ordinal_position
|
||||
FROM information_schema.table_constraints t
|
||||
LEFT JOIN information_schema.key_column_usage k
|
||||
USING(constraint_name,table_schema,table_name)
|
||||
WHERE t.constraint_type='UNIQUE'
|
||||
and table_name != '__drizzle_migrations'
|
||||
AND t.table_schema = '${inputSchema}'
|
||||
ORDER BY ordinal_position`,
|
||||
);
|
||||
|
||||
const tableToPk: { [tname: string]: string[] } = {};
|
||||
|
||||
const tableToPkRows = tablePks as RowDataPacket[];
|
||||
for (const tableToPkRow of tableToPkRows) {
|
||||
const tableName: string = tableToPkRow['table_name'];
|
||||
const columnName: string = tableToPkRow['column_name'];
|
||||
const position: string = tableToPkRow['ordinal_position'];
|
||||
|
||||
if (typeof result[tableName] === 'undefined') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof tableToPk[tableName] === 'undefined') {
|
||||
tableToPk[tableName] = [columnName];
|
||||
} else {
|
||||
tableToPk[tableName].push(columnName);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(tableToPk)) {
|
||||
// if (value.length > 1) {
|
||||
result[key].compositePrimaryKeys = {
|
||||
[`${key}_${value.join('_')}`]: {
|
||||
name: `${key}_${value.join('_')}`,
|
||||
columns: value,
|
||||
},
|
||||
};
|
||||
// } else if (value.length === 1) {
|
||||
// result[key].columns[value[0]].primaryKey = true;
|
||||
// } else {
|
||||
// }
|
||||
}
|
||||
if (progressCallback) {
|
||||
progressCallback('columns', columnsCount, 'done');
|
||||
progressCallback('tables', tablesCount.size, 'done');
|
||||
}
|
||||
|
||||
for (const idxRow of idxRows) {
|
||||
const tableSchema = idxRow['TABLE_SCHEMA'];
|
||||
const tableName = idxRow['TABLE_NAME'];
|
||||
const constraintName = idxRow['INDEX_NAME'];
|
||||
const columnName: string = idxRow['COLUMN_NAME'];
|
||||
const isUnique = idxRow['NON_UNIQUE'] === 0;
|
||||
|
||||
const tableInResult = result[tableName];
|
||||
if (typeof tableInResult === 'undefined') continue;
|
||||
|
||||
// if (tableInResult.columns[columnName].type === "serial") continue;
|
||||
|
||||
indexesCount += 1;
|
||||
if (progressCallback) {
|
||||
progressCallback('indexes', indexesCount, 'fetching');
|
||||
}
|
||||
|
||||
if (isUnique) {
|
||||
if (
|
||||
typeof tableInResult.uniqueConstraints[constraintName] !== 'undefined'
|
||||
) {
|
||||
tableInResult.uniqueConstraints[constraintName]!.columns.push(
|
||||
columnName,
|
||||
);
|
||||
} else {
|
||||
tableInResult.uniqueConstraints[constraintName] = {
|
||||
name: constraintName,
|
||||
columns: [columnName],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* const views = await db.query(
|
||||
`select * from INFORMATION_SCHEMA.VIEWS WHERE table_schema = '${inputSchema}';`,
|
||||
); */
|
||||
|
||||
/* const resultViews: Record<string, View> = {}; */
|
||||
|
||||
/* viewsCount = views.length;
|
||||
if (progressCallback) {
|
||||
progressCallback('views', viewsCount, 'fetching');
|
||||
}
|
||||
for await (const view of views) {
|
||||
const viewName = view['TABLE_NAME'];
|
||||
const definition = view['VIEW_DEFINITION'];
|
||||
|
||||
const withCheckOption = view['CHECK_OPTION'] === 'NONE'
|
||||
? undefined
|
||||
: view['CHECK_OPTION'].toLowerCase();
|
||||
const sqlSecurity = view['SECURITY_TYPE'].toLowerCase();
|
||||
|
||||
const [createSqlStatement] = await db.query(
|
||||
`SHOW CREATE VIEW \`${viewName}\`;`,
|
||||
);
|
||||
const algorithmMatch = createSqlStatement['Create View'].match(/ALGORITHM=([^ ]+)/);
|
||||
const algorithm = algorithmMatch
|
||||
? algorithmMatch[1].toLowerCase()
|
||||
: undefined;
|
||||
|
||||
const columns = result[viewName].columns;
|
||||
delete result[viewName];
|
||||
|
||||
resultViews[viewName] = {
|
||||
columns: columns,
|
||||
isExisting: false,
|
||||
name: viewName,
|
||||
algorithm,
|
||||
definition,
|
||||
sqlSecurity,
|
||||
withCheckOption,
|
||||
};
|
||||
} */
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback('indexes', indexesCount, 'done');
|
||||
// progressCallback("enums", 0, "fetching");
|
||||
progressCallback('enums', 0, 'done');
|
||||
}
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
dialect: 'singlestore',
|
||||
tables: result,
|
||||
/* views: resultViews, */
|
||||
_meta: {
|
||||
tables: {},
|
||||
columns: {},
|
||||
},
|
||||
internal: internals,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { is } from 'drizzle-orm';
|
||||
import type { AnySQLiteTable } from 'drizzle-orm/sqlite-core';
|
||||
import { SQLiteTable, SQLiteView } from 'drizzle-orm/sqlite-core';
|
||||
import { safeRegister } from '../cli/commands/utils';
|
||||
|
||||
export const prepareFromExports = (exports: Record<string, unknown>) => {
|
||||
const tables: AnySQLiteTable[] = [];
|
||||
const views: SQLiteView[] = [];
|
||||
|
||||
const i0values = Object.values(exports);
|
||||
i0values.forEach((t) => {
|
||||
if (is(t, SQLiteTable)) {
|
||||
tables.push(t);
|
||||
}
|
||||
|
||||
if (is(t, SQLiteView)) {
|
||||
views.push(t);
|
||||
}
|
||||
});
|
||||
|
||||
return { tables, views };
|
||||
};
|
||||
|
||||
export const prepareFromSqliteImports = async (imports: string[]) => {
|
||||
const tables: AnySQLiteTable[] = [];
|
||||
const views: SQLiteView[] = [];
|
||||
|
||||
await safeRegister(async () => {
|
||||
for (let i = 0; i < imports.length; i++) {
|
||||
const it = imports[i];
|
||||
|
||||
const i0: Record<string, unknown> = require(`${it}`);
|
||||
const prepared = prepareFromExports(i0);
|
||||
|
||||
tables.push(...prepared.tables);
|
||||
views.push(...prepared.views);
|
||||
}
|
||||
});
|
||||
|
||||
return { tables: Array.from(new Set(tables)), views };
|
||||
};
|
||||
@@ -0,0 +1,352 @@
|
||||
import { any, boolean, enum as enumType, literal, object, record, string, TypeOf, union } from 'zod';
|
||||
import { customMapEntries, mapValues, originUUID } from '../global';
|
||||
|
||||
// ------- V3 --------
|
||||
const index = object({
|
||||
name: string(),
|
||||
columns: string().array(),
|
||||
where: string().optional(),
|
||||
isUnique: boolean(),
|
||||
}).strict();
|
||||
|
||||
const fk = object({
|
||||
name: string(),
|
||||
tableFrom: string(),
|
||||
columnsFrom: string().array(),
|
||||
tableTo: string(),
|
||||
columnsTo: string().array(),
|
||||
onUpdate: string().optional(),
|
||||
onDelete: string().optional(),
|
||||
}).strict();
|
||||
|
||||
const compositePK = object({
|
||||
columns: string().array(),
|
||||
name: string().optional(),
|
||||
}).strict();
|
||||
|
||||
const column = object({
|
||||
name: string(),
|
||||
type: string(),
|
||||
primaryKey: boolean(),
|
||||
notNull: boolean(),
|
||||
autoincrement: boolean().optional(),
|
||||
default: any().optional(),
|
||||
generated: object({
|
||||
type: enumType(['stored', 'virtual']),
|
||||
as: string(),
|
||||
}).optional(),
|
||||
}).strict();
|
||||
|
||||
const tableV3 = object({
|
||||
name: string(),
|
||||
columns: record(string(), column),
|
||||
indexes: record(string(), index),
|
||||
foreignKeys: record(string(), fk),
|
||||
}).strict();
|
||||
|
||||
const uniqueConstraint = object({
|
||||
name: string(),
|
||||
columns: string().array(),
|
||||
}).strict();
|
||||
|
||||
const checkConstraint = object({
|
||||
name: string(),
|
||||
value: string(),
|
||||
}).strict();
|
||||
|
||||
const table = object({
|
||||
name: string(),
|
||||
columns: record(string(), column),
|
||||
indexes: record(string(), index),
|
||||
foreignKeys: record(string(), fk),
|
||||
compositePrimaryKeys: record(string(), compositePK),
|
||||
uniqueConstraints: record(string(), uniqueConstraint).default({}),
|
||||
checkConstraints: record(string(), checkConstraint).default({}),
|
||||
}).strict();
|
||||
|
||||
export const view = object({
|
||||
name: string(),
|
||||
columns: record(string(), column),
|
||||
definition: string().optional(),
|
||||
isExisting: boolean(),
|
||||
}).strict();
|
||||
|
||||
// use main dialect
|
||||
const dialect = enumType(['sqlite']);
|
||||
|
||||
const schemaHash = object({
|
||||
id: string(),
|
||||
prevId: string(),
|
||||
}).strict();
|
||||
|
||||
export const schemaInternalV3 = object({
|
||||
version: literal('3'),
|
||||
dialect: dialect,
|
||||
tables: record(string(), tableV3),
|
||||
enums: object({}),
|
||||
}).strict();
|
||||
|
||||
export const schemaInternalV4 = object({
|
||||
version: literal('4'),
|
||||
dialect: dialect,
|
||||
tables: record(string(), table),
|
||||
views: record(string(), view).default({}),
|
||||
enums: object({}),
|
||||
}).strict();
|
||||
|
||||
export const schemaInternalV5 = object({
|
||||
version: literal('5'),
|
||||
dialect: dialect,
|
||||
tables: record(string(), table),
|
||||
enums: object({}),
|
||||
_meta: object({
|
||||
tables: record(string(), string()),
|
||||
columns: record(string(), string()),
|
||||
}),
|
||||
}).strict();
|
||||
|
||||
export const kitInternals = object({
|
||||
indexes: record(
|
||||
string(),
|
||||
object({
|
||||
columns: record(
|
||||
string(),
|
||||
object({ isExpression: boolean().optional() }).optional(),
|
||||
),
|
||||
}).optional(),
|
||||
).optional(),
|
||||
}).optional();
|
||||
|
||||
const latestVersion = literal('6');
|
||||
export const schemaInternal = object({
|
||||
version: latestVersion,
|
||||
dialect: dialect,
|
||||
tables: record(string(), table),
|
||||
views: record(string(), view).default({}),
|
||||
enums: object({}),
|
||||
_meta: object({
|
||||
tables: record(string(), string()),
|
||||
columns: record(string(), string()),
|
||||
}),
|
||||
internal: kitInternals,
|
||||
}).strict();
|
||||
|
||||
export const schemaV3 = schemaInternalV3.merge(schemaHash).strict();
|
||||
export const schemaV4 = schemaInternalV4.merge(schemaHash).strict();
|
||||
export const schemaV5 = schemaInternalV5.merge(schemaHash).strict();
|
||||
export const schema = schemaInternal.merge(schemaHash).strict();
|
||||
|
||||
const tableSquashed = object({
|
||||
name: string(),
|
||||
columns: record(string(), column),
|
||||
indexes: record(string(), string()),
|
||||
foreignKeys: record(string(), string()),
|
||||
compositePrimaryKeys: record(string(), string()),
|
||||
uniqueConstraints: record(string(), string()).default({}),
|
||||
checkConstraints: record(string(), string()).default({}),
|
||||
}).strict();
|
||||
|
||||
export const schemaSquashed = object({
|
||||
version: latestVersion,
|
||||
dialect: dialect,
|
||||
tables: record(string(), tableSquashed),
|
||||
views: record(string(), view),
|
||||
enums: any(),
|
||||
}).strict();
|
||||
|
||||
export type Dialect = TypeOf<typeof dialect>;
|
||||
export type Column = TypeOf<typeof column>;
|
||||
export type Table = TypeOf<typeof table>;
|
||||
export type SQLiteSchema = TypeOf<typeof schema>;
|
||||
export type SQLiteSchemaV3 = TypeOf<typeof schemaV3>;
|
||||
export type SQLiteSchemaV4 = TypeOf<typeof schemaV4>;
|
||||
export type SQLiteSchemaInternal = TypeOf<typeof schemaInternal>;
|
||||
export type SQLiteSchemaSquashed = TypeOf<typeof schemaSquashed>;
|
||||
export type SQLiteKitInternals = TypeOf<typeof kitInternals>;
|
||||
export type Index = TypeOf<typeof index>;
|
||||
export type ForeignKey = TypeOf<typeof fk>;
|
||||
export type PrimaryKey = TypeOf<typeof compositePK>;
|
||||
export type UniqueConstraint = TypeOf<typeof uniqueConstraint>;
|
||||
export type CheckConstraint = TypeOf<typeof checkConstraint>;
|
||||
export type View = TypeOf<typeof view>;
|
||||
|
||||
export const SQLiteSquasher = {
|
||||
squashIdx: (idx: Index) => {
|
||||
index.parse(idx);
|
||||
return `${idx.name};${idx.columns.join(',')};${idx.isUnique};${idx.where ?? ''}`;
|
||||
},
|
||||
unsquashIdx: (input: string): Index => {
|
||||
const [name, columnsString, isUnique, where] = input.split(';');
|
||||
|
||||
const result: Index = index.parse({
|
||||
name,
|
||||
columns: columnsString.split(','),
|
||||
isUnique: isUnique === 'true',
|
||||
where: where ?? undefined,
|
||||
});
|
||||
return result;
|
||||
},
|
||||
squashUnique: (unq: UniqueConstraint) => {
|
||||
return `${unq.name};${unq.columns.join(',')}`;
|
||||
},
|
||||
unsquashUnique: (unq: string): UniqueConstraint => {
|
||||
const [name, columns] = unq.split(';');
|
||||
return { name, columns: columns.split(',') };
|
||||
},
|
||||
squashFK: (fk: ForeignKey) => {
|
||||
return `${fk.name};${fk.tableFrom};${fk.columnsFrom.join(',')};${fk.tableTo};${fk.columnsTo.join(',')};${
|
||||
fk.onUpdate ?? ''
|
||||
};${fk.onDelete ?? ''}`;
|
||||
},
|
||||
unsquashFK: (input: string): ForeignKey => {
|
||||
const [
|
||||
name,
|
||||
tableFrom,
|
||||
columnsFromStr,
|
||||
tableTo,
|
||||
columnsToStr,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
] = input.split(';');
|
||||
|
||||
const result: ForeignKey = fk.parse({
|
||||
name,
|
||||
tableFrom,
|
||||
columnsFrom: columnsFromStr.split(','),
|
||||
tableTo,
|
||||
columnsTo: columnsToStr.split(','),
|
||||
onUpdate,
|
||||
onDelete,
|
||||
});
|
||||
return result;
|
||||
},
|
||||
squashPushFK: (fk: ForeignKey) => {
|
||||
return `${fk.tableFrom};${fk.columnsFrom.join(',')};${fk.tableTo};${fk.columnsTo.join(',')};${fk.onUpdate ?? ''};${
|
||||
fk.onDelete ?? ''
|
||||
}`;
|
||||
},
|
||||
unsquashPushFK: (input: string): ForeignKey => {
|
||||
const [
|
||||
tableFrom,
|
||||
columnsFromStr,
|
||||
tableTo,
|
||||
columnsToStr,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
] = input.split(';');
|
||||
|
||||
const result: ForeignKey = fk.parse({
|
||||
name: '',
|
||||
tableFrom,
|
||||
columnsFrom: columnsFromStr.split(','),
|
||||
tableTo,
|
||||
columnsTo: columnsToStr.split(','),
|
||||
onUpdate,
|
||||
onDelete,
|
||||
});
|
||||
return result;
|
||||
},
|
||||
squashPK: (pk: PrimaryKey) => {
|
||||
return pk.columns.join(',');
|
||||
},
|
||||
unsquashPK: (pk: string) => {
|
||||
return pk.split(',');
|
||||
},
|
||||
squashCheck: (check: CheckConstraint) => {
|
||||
return `${check.name};${check.value}`;
|
||||
},
|
||||
unsquashCheck: (input: string): CheckConstraint => {
|
||||
const [
|
||||
name,
|
||||
value,
|
||||
] = input.split(';');
|
||||
|
||||
return { name, value };
|
||||
},
|
||||
};
|
||||
|
||||
export const squashSqliteScheme = (
|
||||
json: SQLiteSchema | SQLiteSchemaV4,
|
||||
action?: 'push' | undefined,
|
||||
): SQLiteSchemaSquashed => {
|
||||
const mappedTables = Object.fromEntries(
|
||||
Object.entries(json.tables).map((it) => {
|
||||
const squashedIndexes = mapValues(it[1].indexes, (index: Index) => {
|
||||
return SQLiteSquasher.squashIdx(index);
|
||||
});
|
||||
|
||||
const squashedFKs = customMapEntries<string, ForeignKey>(
|
||||
it[1].foreignKeys,
|
||||
(key, value) => {
|
||||
return action === 'push'
|
||||
? [
|
||||
SQLiteSquasher.squashPushFK(value),
|
||||
SQLiteSquasher.squashPushFK(value),
|
||||
]
|
||||
: [key, SQLiteSquasher.squashFK(value)];
|
||||
},
|
||||
);
|
||||
|
||||
const squashedPKs = mapValues(it[1].compositePrimaryKeys, (pk) => {
|
||||
return SQLiteSquasher.squashPK(pk);
|
||||
});
|
||||
|
||||
const squashedUniqueConstraints = mapValues(
|
||||
it[1].uniqueConstraints,
|
||||
(unq) => {
|
||||
return SQLiteSquasher.squashUnique(unq);
|
||||
},
|
||||
);
|
||||
|
||||
const squashedCheckConstraints = mapValues(
|
||||
it[1].checkConstraints,
|
||||
(check) => {
|
||||
return SQLiteSquasher.squashCheck(check);
|
||||
},
|
||||
);
|
||||
|
||||
return [
|
||||
it[0],
|
||||
{
|
||||
name: it[1].name,
|
||||
columns: it[1].columns,
|
||||
indexes: squashedIndexes,
|
||||
foreignKeys: squashedFKs,
|
||||
compositePrimaryKeys: squashedPKs,
|
||||
uniqueConstraints: squashedUniqueConstraints,
|
||||
checkConstraints: squashedCheckConstraints,
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
version: '6',
|
||||
dialect: json.dialect,
|
||||
tables: mappedTables,
|
||||
views: json.views,
|
||||
enums: json.enums,
|
||||
};
|
||||
};
|
||||
|
||||
export const drySQLite = schema.parse({
|
||||
version: '6',
|
||||
dialect: 'sqlite',
|
||||
id: originUUID,
|
||||
prevId: '',
|
||||
tables: {},
|
||||
views: {},
|
||||
enums: {},
|
||||
_meta: {
|
||||
tables: {},
|
||||
columns: {},
|
||||
},
|
||||
});
|
||||
|
||||
export const sqliteSchemaV3 = schemaV3;
|
||||
export const sqliteSchemaV4 = schemaV4;
|
||||
export const sqliteSchemaV5 = schemaV5;
|
||||
export const sqliteSchema = schema;
|
||||
export const SQLiteSchemaSquashed = schemaSquashed;
|
||||
|
||||
export const backwardCompatibleSqliteSchema = union([sqliteSchemaV5, schema]);
|
||||
@@ -0,0 +1,953 @@
|
||||
import chalk from 'chalk';
|
||||
import { getTableName, is, SQL } from 'drizzle-orm';
|
||||
import {
|
||||
AnySQLiteTable,
|
||||
getTableConfig,
|
||||
getViewConfig,
|
||||
SQLiteBaseInteger,
|
||||
SQLiteColumn,
|
||||
SQLiteSyncDialect,
|
||||
SQLiteView,
|
||||
uniqueKeyName,
|
||||
} from 'drizzle-orm/sqlite-core';
|
||||
import { CasingType } from 'src/cli/validations/common';
|
||||
import { withStyle } from '../cli/validations/outputs';
|
||||
import type { IntrospectStage, IntrospectStatus } from '../cli/views';
|
||||
import type {
|
||||
CheckConstraint,
|
||||
Column,
|
||||
ForeignKey,
|
||||
Index,
|
||||
PrimaryKey,
|
||||
SQLiteKitInternals,
|
||||
SQLiteSchemaInternal,
|
||||
Table,
|
||||
UniqueConstraint,
|
||||
View,
|
||||
} from '../serializer/sqliteSchema';
|
||||
import { escapeSingleQuotes, type SQLiteDB } from '../utils';
|
||||
import { getColumnCasing, sqlToStr } from './utils';
|
||||
|
||||
export const generateSqliteSnapshot = (
|
||||
tables: AnySQLiteTable[],
|
||||
views: SQLiteView[],
|
||||
casing: CasingType | undefined,
|
||||
): SQLiteSchemaInternal => {
|
||||
const dialect = new SQLiteSyncDialect({ casing });
|
||||
const result: Record<string, Table> = {};
|
||||
const resultViews: Record<string, View> = {};
|
||||
|
||||
const internal: SQLiteKitInternals = { indexes: {} };
|
||||
for (const table of tables) {
|
||||
// const tableName = getTableName(table);
|
||||
const columnsObject: Record<string, Column> = {};
|
||||
const indexesObject: Record<string, Index> = {};
|
||||
const foreignKeysObject: Record<string, ForeignKey> = {};
|
||||
const primaryKeysObject: Record<string, PrimaryKey> = {};
|
||||
const uniqueConstraintObject: Record<string, UniqueConstraint> = {};
|
||||
const checkConstraintObject: Record<string, CheckConstraint> = {};
|
||||
|
||||
const checksInTable: Record<string, string[]> = {};
|
||||
|
||||
const {
|
||||
name: tableName,
|
||||
columns,
|
||||
indexes,
|
||||
checks,
|
||||
foreignKeys: tableForeignKeys,
|
||||
primaryKeys,
|
||||
uniqueConstraints,
|
||||
} = getTableConfig(table);
|
||||
|
||||
columns.forEach((column) => {
|
||||
const name = getColumnCasing(column, casing);
|
||||
const notNull: boolean = column.notNull;
|
||||
const primaryKey: boolean = column.primary;
|
||||
const generated = column.generated;
|
||||
|
||||
const columnToSet: Column = {
|
||||
name,
|
||||
type: column.getSQLType(),
|
||||
primaryKey,
|
||||
notNull,
|
||||
autoincrement: is(column, SQLiteBaseInteger)
|
||||
? column.autoIncrement
|
||||
: false,
|
||||
generated: generated
|
||||
? {
|
||||
as: is(generated.as, SQL)
|
||||
? `(${dialect.sqlToQuery(generated.as as SQL, 'indexes').sql})`
|
||||
: typeof generated.as === 'function'
|
||||
? `(${dialect.sqlToQuery(generated.as() as SQL, 'indexes').sql})`
|
||||
: `(${generated.as as any})`,
|
||||
type: generated.mode ?? 'virtual',
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
if (column.default !== undefined) {
|
||||
if (is(column.default, SQL)) {
|
||||
columnToSet.default = sqlToStr(column.default, casing);
|
||||
} else {
|
||||
columnToSet.default = typeof column.default === 'string'
|
||||
? `'${escapeSingleQuotes(column.default)}'`
|
||||
: typeof column.default === 'object'
|
||||
|| Array.isArray(column.default)
|
||||
? `'${JSON.stringify(column.default)}'`
|
||||
: column.default;
|
||||
}
|
||||
}
|
||||
columnsObject[name] = columnToSet;
|
||||
|
||||
if (column.isUnique) {
|
||||
const existingUnique = indexesObject[column.uniqueName!];
|
||||
if (typeof existingUnique !== 'undefined') {
|
||||
console.log(
|
||||
`\n${
|
||||
withStyle.errorWarning(`We\'ve found duplicated unique constraint names in ${
|
||||
chalk.underline.blue(
|
||||
tableName,
|
||||
)
|
||||
} table.
|
||||
The unique constraint ${
|
||||
chalk.underline.blue(
|
||||
column.uniqueName,
|
||||
)
|
||||
} on the ${
|
||||
chalk.underline.blue(
|
||||
name,
|
||||
)
|
||||
} column is confilcting with a unique constraint name already defined for ${
|
||||
chalk.underline.blue(
|
||||
existingUnique.columns.join(','),
|
||||
)
|
||||
} columns\n`)
|
||||
}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
indexesObject[column.uniqueName!] = {
|
||||
name: column.uniqueName!,
|
||||
columns: [columnToSet.name],
|
||||
isUnique: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const foreignKeys: ForeignKey[] = tableForeignKeys.map((fk) => {
|
||||
const tableFrom = tableName;
|
||||
const onDelete = fk.onDelete ?? 'no action';
|
||||
const onUpdate = fk.onUpdate ?? 'no action';
|
||||
const reference = fk.reference();
|
||||
|
||||
const referenceFT = reference.foreignTable;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
const tableTo = getTableName(referenceFT);
|
||||
|
||||
const originalColumnsFrom = reference.columns.map((it) => it.name);
|
||||
const columnsFrom = reference.columns.map((it) => getColumnCasing(it, casing));
|
||||
const originalColumnsTo = reference.foreignColumns.map((it) => it.name);
|
||||
const columnsTo = reference.foreignColumns.map((it) => getColumnCasing(it, casing));
|
||||
|
||||
let name = fk.getName();
|
||||
if (casing !== undefined) {
|
||||
for (let i = 0; i < originalColumnsFrom.length; i++) {
|
||||
name = name.replace(originalColumnsFrom[i], columnsFrom[i]);
|
||||
}
|
||||
for (let i = 0; i < originalColumnsTo.length; i++) {
|
||||
name = name.replace(originalColumnsTo[i], columnsTo[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
tableFrom,
|
||||
tableTo,
|
||||
columnsFrom,
|
||||
columnsTo,
|
||||
onDelete,
|
||||
onUpdate,
|
||||
} as ForeignKey;
|
||||
});
|
||||
|
||||
foreignKeys.forEach((it) => {
|
||||
foreignKeysObject[it.name] = it;
|
||||
});
|
||||
|
||||
indexes.forEach((value) => {
|
||||
const columns = value.config.columns;
|
||||
const name = value.config.name;
|
||||
|
||||
let indexColumns = columns.map((it) => {
|
||||
if (is(it, SQL)) {
|
||||
const sql = dialect.sqlToQuery(it, 'indexes').sql;
|
||||
if (typeof internal!.indexes![name] === 'undefined') {
|
||||
internal!.indexes![name] = {
|
||||
columns: {
|
||||
[sql]: {
|
||||
isExpression: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
if (typeof internal!.indexes![name]?.columns[sql] === 'undefined') {
|
||||
internal!.indexes![name]!.columns[sql] = {
|
||||
isExpression: true,
|
||||
};
|
||||
} else {
|
||||
internal!.indexes![name]!.columns[sql]!.isExpression = true;
|
||||
}
|
||||
}
|
||||
return sql;
|
||||
} else {
|
||||
return getColumnCasing(it, casing);
|
||||
}
|
||||
});
|
||||
|
||||
let where: string | undefined = undefined;
|
||||
if (value.config.where !== undefined) {
|
||||
if (is(value.config.where, SQL)) {
|
||||
where = dialect.sqlToQuery(value.config.where).sql;
|
||||
}
|
||||
}
|
||||
|
||||
indexesObject[name] = {
|
||||
name,
|
||||
columns: indexColumns,
|
||||
isUnique: value.config.unique ?? false,
|
||||
where,
|
||||
};
|
||||
});
|
||||
|
||||
uniqueConstraints?.map((unq) => {
|
||||
const columnNames = unq.columns.map((c) => getColumnCasing(c, casing));
|
||||
|
||||
const name = unq.name ?? uniqueKeyName(table, columnNames);
|
||||
|
||||
const existingUnique = indexesObject[name];
|
||||
if (typeof existingUnique !== 'undefined') {
|
||||
console.log(
|
||||
`\n${
|
||||
withStyle.errorWarning(
|
||||
`We\'ve found duplicated unique constraint names in ${
|
||||
chalk.underline.blue(
|
||||
tableName,
|
||||
)
|
||||
} table. \nThe unique constraint ${
|
||||
chalk.underline.blue(
|
||||
name,
|
||||
)
|
||||
} on the ${
|
||||
chalk.underline.blue(
|
||||
columnNames.join(','),
|
||||
)
|
||||
} columns is confilcting with a unique constraint name already defined for ${
|
||||
chalk.underline.blue(
|
||||
existingUnique.columns.join(','),
|
||||
)
|
||||
} columns\n`,
|
||||
)
|
||||
}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
indexesObject[name] = {
|
||||
name: unq.name!,
|
||||
columns: columnNames,
|
||||
isUnique: true,
|
||||
};
|
||||
});
|
||||
|
||||
primaryKeys.forEach((it) => {
|
||||
if (it.columns.length > 1) {
|
||||
const originalColumnNames = it.columns.map((c) => c.name);
|
||||
const columnNames = it.columns.map((c) => getColumnCasing(c, casing));
|
||||
|
||||
let name = it.getName();
|
||||
if (casing !== undefined) {
|
||||
for (let i = 0; i < originalColumnNames.length; i++) {
|
||||
name = name.replace(originalColumnNames[i], columnNames[i]);
|
||||
}
|
||||
}
|
||||
|
||||
primaryKeysObject[name] = {
|
||||
columns: columnNames,
|
||||
name,
|
||||
};
|
||||
} else {
|
||||
columnsObject[getColumnCasing(it.columns[0], casing)].primaryKey = true;
|
||||
}
|
||||
});
|
||||
|
||||
checks.forEach((check) => {
|
||||
const checkName = check.name;
|
||||
if (typeof checksInTable[tableName] !== 'undefined') {
|
||||
if (checksInTable[tableName].includes(check.name)) {
|
||||
console.log(
|
||||
`\n${
|
||||
withStyle.errorWarning(
|
||||
`We\'ve found duplicated check constraint name in ${
|
||||
chalk.underline.blue(
|
||||
tableName,
|
||||
)
|
||||
}. Please rename your check constraint in the ${
|
||||
chalk.underline.blue(
|
||||
tableName,
|
||||
)
|
||||
} table`,
|
||||
)
|
||||
}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
checksInTable[tableName].push(checkName);
|
||||
} else {
|
||||
checksInTable[tableName] = [check.name];
|
||||
}
|
||||
|
||||
checkConstraintObject[checkName] = {
|
||||
name: checkName,
|
||||
value: dialect.sqlToQuery(check.value).sql,
|
||||
};
|
||||
});
|
||||
|
||||
result[tableName] = {
|
||||
name: tableName,
|
||||
columns: columnsObject,
|
||||
indexes: indexesObject,
|
||||
foreignKeys: foreignKeysObject,
|
||||
compositePrimaryKeys: primaryKeysObject,
|
||||
uniqueConstraints: uniqueConstraintObject,
|
||||
checkConstraints: checkConstraintObject,
|
||||
};
|
||||
}
|
||||
|
||||
for (const view of views) {
|
||||
const { name, isExisting, selectedFields, query, schema } = getViewConfig(view);
|
||||
|
||||
const columnsObject: Record<string, Column> = {};
|
||||
|
||||
const existingView = resultViews[name];
|
||||
if (typeof existingView !== 'undefined') {
|
||||
console.log(
|
||||
`\n${
|
||||
withStyle.errorWarning(
|
||||
`We\'ve found duplicated view name across ${
|
||||
chalk.underline.blue(
|
||||
schema ?? 'public',
|
||||
)
|
||||
} schema. Please rename your view`,
|
||||
)
|
||||
}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
for (const key in selectedFields) {
|
||||
if (is(selectedFields[key], SQLiteColumn)) {
|
||||
const column = selectedFields[key];
|
||||
const notNull: boolean = column.notNull;
|
||||
const primaryKey: boolean = column.primary;
|
||||
const generated = column.generated;
|
||||
|
||||
const columnToSet: Column = {
|
||||
name: column.name,
|
||||
type: column.getSQLType(),
|
||||
primaryKey,
|
||||
notNull,
|
||||
autoincrement: is(column, SQLiteBaseInteger)
|
||||
? column.autoIncrement
|
||||
: false,
|
||||
generated: generated
|
||||
? {
|
||||
as: is(generated.as, SQL)
|
||||
? `(${dialect.sqlToQuery(generated.as as SQL, 'indexes').sql})`
|
||||
: typeof generated.as === 'function'
|
||||
? `(${dialect.sqlToQuery(generated.as() as SQL, 'indexes').sql})`
|
||||
: `(${generated.as as any})`,
|
||||
type: generated.mode ?? 'virtual',
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
if (column.default !== undefined) {
|
||||
if (is(column.default, SQL)) {
|
||||
columnToSet.default = sqlToStr(column.default, casing);
|
||||
} else {
|
||||
columnToSet.default = typeof column.default === 'string'
|
||||
? `'${column.default}'`
|
||||
: typeof column.default === 'object'
|
||||
|| Array.isArray(column.default)
|
||||
? `'${JSON.stringify(column.default)}'`
|
||||
: column.default;
|
||||
}
|
||||
}
|
||||
columnsObject[column.name] = columnToSet;
|
||||
}
|
||||
}
|
||||
|
||||
resultViews[name] = {
|
||||
columns: columnsObject,
|
||||
name,
|
||||
isExisting,
|
||||
definition: isExisting ? undefined : dialect.sqlToQuery(query!).sql,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
version: '6',
|
||||
dialect: 'sqlite',
|
||||
tables: result,
|
||||
views: resultViews,
|
||||
enums: {},
|
||||
_meta: {
|
||||
tables: {},
|
||||
columns: {},
|
||||
},
|
||||
internal,
|
||||
};
|
||||
};
|
||||
|
||||
function mapSqlToSqliteType(sqlType: string): string {
|
||||
const lowered = sqlType.toLowerCase();
|
||||
if (
|
||||
[
|
||||
'int',
|
||||
'integer',
|
||||
'integer auto_increment',
|
||||
'tinyint',
|
||||
'smallint',
|
||||
'mediumint',
|
||||
'bigint',
|
||||
'unsigned big int',
|
||||
'int2',
|
||||
'int8',
|
||||
].some((it) => lowered.startsWith(it))
|
||||
) {
|
||||
return 'integer';
|
||||
} else if (
|
||||
[
|
||||
'character',
|
||||
'varchar',
|
||||
'varying character',
|
||||
'national varying character',
|
||||
'nchar',
|
||||
'native character',
|
||||
'nvarchar',
|
||||
'text',
|
||||
'clob',
|
||||
].some((it) => lowered.startsWith(it))
|
||||
) {
|
||||
const match = lowered.match(/\d+/);
|
||||
|
||||
if (match) {
|
||||
return `text(${match[0]})`;
|
||||
}
|
||||
|
||||
return 'text';
|
||||
} else if (lowered.startsWith('blob')) {
|
||||
return 'blob';
|
||||
} else if (
|
||||
['real', 'double', 'double precision', 'float'].some((it) => lowered.startsWith(it))
|
||||
) {
|
||||
return 'real';
|
||||
} else {
|
||||
return 'numeric';
|
||||
}
|
||||
}
|
||||
|
||||
interface ColumnInfo {
|
||||
columnName: string;
|
||||
expression: string;
|
||||
type: 'stored' | 'virtual';
|
||||
}
|
||||
|
||||
function extractGeneratedColumns(input: string): Record<string, ColumnInfo> {
|
||||
const columns: Record<string, ColumnInfo> = {};
|
||||
const lines = input.split(/,\s*(?![^()]*\))/); // Split by commas outside parentheses
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes('GENERATED ALWAYS AS')) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
const columnName = parts[0].replace(/[`'"]/g, ''); // Remove quotes around the column name
|
||||
const expression = line
|
||||
.substring(line.indexOf('('), line.indexOf(')') + 1)
|
||||
.trim();
|
||||
|
||||
// Extract type ensuring to remove any trailing characters like ')'
|
||||
const typeIndex = parts.findIndex((part) => part.match(/(stored|virtual)/i));
|
||||
let type: ColumnInfo['type'] = 'virtual';
|
||||
if (typeIndex !== -1) {
|
||||
type = parts[typeIndex]
|
||||
.replace(/[^a-z]/gi, '')
|
||||
.toLowerCase() as ColumnInfo['type'];
|
||||
}
|
||||
|
||||
columns[columnName] = {
|
||||
columnName: columnName,
|
||||
expression: expression,
|
||||
type,
|
||||
};
|
||||
}
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
|
||||
function filterIgnoredTablesByField(fieldName: string) {
|
||||
// _cf_ is a prefix for internal Cloudflare D1 tables (e.g. _cf_KV, _cf_METADATA)
|
||||
// _litestream_ is a prefix for internal Litestream tables (e.g. _litestream_seq, _litestream_lock)
|
||||
// libsql_ is a prefix for internal libSQL tables (e.g. libsql_wasm_func_table)
|
||||
// sqlite_ is a prefix for internal SQLite tables (e.g. sqlite_sequence, sqlite_stat1)
|
||||
return `${fieldName} != '__drizzle_migrations'
|
||||
AND ${fieldName} NOT LIKE '\\_cf\\_%' ESCAPE '\\'
|
||||
AND ${fieldName} NOT LIKE '\\_litestream\\_%' ESCAPE '\\'
|
||||
AND ${fieldName} NOT LIKE 'libsql\\_%' ESCAPE '\\'
|
||||
AND ${fieldName} NOT LIKE 'sqlite\\_%' ESCAPE '\\'`;
|
||||
}
|
||||
|
||||
export const fromDatabase = async (
|
||||
db: SQLiteDB,
|
||||
tablesFilter: (table: string) => boolean = (table) => true,
|
||||
progressCallback?: (
|
||||
stage: IntrospectStage,
|
||||
count: number,
|
||||
status: IntrospectStatus,
|
||||
) => void,
|
||||
): Promise<SQLiteSchemaInternal> => {
|
||||
const result: Record<string, Table> = {};
|
||||
const resultViews: Record<string, View> = {};
|
||||
|
||||
const columns = await db.query<{
|
||||
tableName: string;
|
||||
columnName: string;
|
||||
columnType: string;
|
||||
notNull: number;
|
||||
defaultValue: string;
|
||||
pk: number;
|
||||
seq: number;
|
||||
hidden: number;
|
||||
sql: string;
|
||||
type: 'view' | 'table';
|
||||
}>(`SELECT
|
||||
m.name as "tableName",
|
||||
p.name as "columnName",
|
||||
p.type as "columnType",
|
||||
p."notnull" as "notNull",
|
||||
p.dflt_value as "defaultValue",
|
||||
p.pk as pk,
|
||||
p.hidden as hidden,
|
||||
m.sql,
|
||||
m.type as type
|
||||
FROM sqlite_master AS m
|
||||
JOIN pragma_table_xinfo(m.name) AS p
|
||||
WHERE (m.type = 'table' OR m.type = 'view')
|
||||
AND ${filterIgnoredTablesByField('m.tbl_name')};`);
|
||||
|
||||
const tablesWithSeq: string[] = [];
|
||||
|
||||
const seq = await db.query<{
|
||||
name: string;
|
||||
}>(`SELECT
|
||||
*
|
||||
FROM sqlite_master
|
||||
WHERE sql GLOB '*[ *' || CHAR(9) || CHAR(10) || CHAR(13) || ']AUTOINCREMENT[^'']*'
|
||||
AND ${filterIgnoredTablesByField('tbl_name')};`);
|
||||
|
||||
for (const s of seq) {
|
||||
tablesWithSeq.push(s.name);
|
||||
}
|
||||
|
||||
let columnsCount = 0;
|
||||
let tablesCount = new Set();
|
||||
let indexesCount = 0;
|
||||
let foreignKeysCount = 0;
|
||||
let checksCount = 0;
|
||||
let viewsCount = 0;
|
||||
|
||||
// append primaryKeys by table
|
||||
const tableToPk: { [tname: string]: string[] } = {};
|
||||
|
||||
let tableToGeneratedColumnsInfo: Record<
|
||||
string,
|
||||
Record<string, ColumnInfo>
|
||||
> = {};
|
||||
|
||||
for (const column of columns) {
|
||||
if (!tablesFilter(column.tableName)) continue;
|
||||
|
||||
// TODO
|
||||
if (column.type !== 'view') {
|
||||
columnsCount += 1;
|
||||
}
|
||||
if (progressCallback) {
|
||||
progressCallback('columns', columnsCount, 'fetching');
|
||||
}
|
||||
const tableName = column.tableName;
|
||||
|
||||
tablesCount.add(tableName);
|
||||
if (progressCallback) {
|
||||
progressCallback('tables', tablesCount.size, 'fetching');
|
||||
}
|
||||
const columnName = column.columnName;
|
||||
const isNotNull = column.notNull === 1; // 'YES', 'NO'
|
||||
const columnType = column.columnType; // varchar(256)
|
||||
const isPrimary = column.pk !== 0; // 'PRI', ''
|
||||
const columnDefault: string = column.defaultValue;
|
||||
|
||||
const isAutoincrement = isPrimary && tablesWithSeq.includes(tableName);
|
||||
|
||||
if (isPrimary) {
|
||||
if (typeof tableToPk[tableName] === 'undefined') {
|
||||
tableToPk[tableName] = [columnName];
|
||||
} else {
|
||||
tableToPk[tableName].push(columnName);
|
||||
}
|
||||
}
|
||||
|
||||
const table = result[tableName];
|
||||
|
||||
if (column.hidden === 2 || column.hidden === 3) {
|
||||
if (
|
||||
typeof tableToGeneratedColumnsInfo[column.tableName] === 'undefined'
|
||||
) {
|
||||
tableToGeneratedColumnsInfo[column.tableName] = extractGeneratedColumns(
|
||||
column.sql,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const newColumn: Column = {
|
||||
default: columnDefault === null
|
||||
? undefined
|
||||
: /^-?[\d.]+(?:e-?\d+)?$/.test(columnDefault)
|
||||
? Number(columnDefault)
|
||||
: ['CURRENT_TIME', 'CURRENT_DATE', 'CURRENT_TIMESTAMP'].includes(
|
||||
columnDefault,
|
||||
)
|
||||
? `(${columnDefault})`
|
||||
: columnDefault === 'false'
|
||||
? false
|
||||
: columnDefault === 'true'
|
||||
? true
|
||||
: columnDefault.startsWith("'") && columnDefault.endsWith("'")
|
||||
? columnDefault
|
||||
// ? columnDefault.substring(1, columnDefault.length - 1)
|
||||
: `(${columnDefault})`,
|
||||
autoincrement: isAutoincrement,
|
||||
name: columnName,
|
||||
type: mapSqlToSqliteType(columnType),
|
||||
primaryKey: false,
|
||||
notNull: isNotNull,
|
||||
generated: tableToGeneratedColumnsInfo[tableName]
|
||||
&& tableToGeneratedColumnsInfo[tableName][columnName]
|
||||
? {
|
||||
type: tableToGeneratedColumnsInfo[tableName][columnName].type,
|
||||
as: tableToGeneratedColumnsInfo[tableName][columnName].expression,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
if (!table) {
|
||||
result[tableName] = {
|
||||
name: tableName,
|
||||
columns: {
|
||||
[columnName]: newColumn,
|
||||
},
|
||||
compositePrimaryKeys: {},
|
||||
indexes: {},
|
||||
foreignKeys: {},
|
||||
uniqueConstraints: {},
|
||||
checkConstraints: {},
|
||||
};
|
||||
} else {
|
||||
result[tableName]!.columns[columnName] = newColumn;
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(tableToPk)) {
|
||||
if (value.length > 1) {
|
||||
result[key].compositePrimaryKeys = {
|
||||
[`${key}_${value.join('_')}_pk`]: {
|
||||
columns: value,
|
||||
name: `${key}_${value.join('_')}_pk`,
|
||||
},
|
||||
};
|
||||
} else if (value.length === 1) {
|
||||
result[key].columns[value[0]].primaryKey = true;
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback('columns', columnsCount, 'done');
|
||||
progressCallback('tables', tablesCount.size, 'done');
|
||||
}
|
||||
try {
|
||||
const fks = await db.query<{
|
||||
tableFrom: string;
|
||||
tableTo: string;
|
||||
from: string;
|
||||
to: string;
|
||||
onUpdate: string;
|
||||
onDelete: string;
|
||||
seq: number;
|
||||
id: number;
|
||||
}>(`SELECT
|
||||
m.name as "tableFrom",
|
||||
f.id as "id",
|
||||
f."table" as "tableTo",
|
||||
f."from",
|
||||
f."to",
|
||||
f."on_update" as "onUpdate",
|
||||
f."on_delete" as "onDelete",
|
||||
f.seq as "seq"
|
||||
FROM
|
||||
sqlite_master m,
|
||||
pragma_foreign_key_list(m.name) as f
|
||||
WHERE ${filterIgnoredTablesByField('m.tbl_name')};`);
|
||||
|
||||
const fkByTableName: Record<string, ForeignKey> = {};
|
||||
|
||||
for (const fkRow of fks) {
|
||||
foreignKeysCount += 1;
|
||||
if (progressCallback) {
|
||||
progressCallback('fks', foreignKeysCount, 'fetching');
|
||||
}
|
||||
const tableName: string = fkRow.tableFrom;
|
||||
const columnName: string = fkRow.from;
|
||||
const refTableName = fkRow.tableTo;
|
||||
const refColumnName: string = fkRow.to;
|
||||
const updateRule: string = fkRow.onUpdate;
|
||||
const deleteRule = fkRow.onDelete;
|
||||
const sequence = fkRow.seq;
|
||||
const id = fkRow.id;
|
||||
|
||||
const tableInResult = result[tableName];
|
||||
if (typeof tableInResult === 'undefined') continue;
|
||||
|
||||
if (typeof fkByTableName[`${tableName}_${id}`] !== 'undefined') {
|
||||
fkByTableName[`${tableName}_${id}`]!.columnsFrom.push(columnName);
|
||||
fkByTableName[`${tableName}_${id}`]!.columnsTo.push(refColumnName);
|
||||
} else {
|
||||
fkByTableName[`${tableName}_${id}`] = {
|
||||
name: '',
|
||||
tableFrom: tableName,
|
||||
tableTo: refTableName,
|
||||
columnsFrom: [columnName],
|
||||
columnsTo: [refColumnName],
|
||||
onDelete: deleteRule?.toLowerCase(),
|
||||
onUpdate: updateRule?.toLowerCase(),
|
||||
};
|
||||
}
|
||||
|
||||
const columnsFrom = fkByTableName[`${tableName}_${id}`].columnsFrom;
|
||||
const columnsTo = fkByTableName[`${tableName}_${id}`].columnsTo;
|
||||
fkByTableName[
|
||||
`${tableName}_${id}`
|
||||
].name = `${tableName}_${
|
||||
columnsFrom.join(
|
||||
'_',
|
||||
)
|
||||
}_${refTableName}_${columnsTo.join('_')}_fk`;
|
||||
}
|
||||
|
||||
for (const idx of Object.keys(fkByTableName)) {
|
||||
const value = fkByTableName[idx];
|
||||
result[value.tableFrom].foreignKeys[value.name] = value;
|
||||
}
|
||||
} catch (e) {
|
||||
// console.log(`Can't proccess foreign keys`);
|
||||
}
|
||||
if (progressCallback) {
|
||||
progressCallback('fks', foreignKeysCount, 'done');
|
||||
}
|
||||
const idxs = await db.query<{
|
||||
tableName: string;
|
||||
indexName: string;
|
||||
columnName: string;
|
||||
isUnique: number;
|
||||
seq: string;
|
||||
}>(`SELECT
|
||||
m.tbl_name as tableName,
|
||||
il.name as indexName,
|
||||
ii.name as columnName,
|
||||
il.[unique] as isUnique,
|
||||
il.seq as seq
|
||||
FROM
|
||||
sqlite_master AS m,
|
||||
pragma_index_list(m.name) AS il,
|
||||
pragma_index_info(il.name) AS ii
|
||||
WHERE
|
||||
m.type = 'table'
|
||||
AND il.name NOT LIKE 'sqlite\\_autoindex\\_%' ESCAPE '\\'
|
||||
AND ${filterIgnoredTablesByField('m.tbl_name')};`);
|
||||
|
||||
for (const idxRow of idxs) {
|
||||
const tableName = idxRow.tableName;
|
||||
const constraintName = idxRow.indexName;
|
||||
const columnName: string = idxRow.columnName;
|
||||
const isUnique = idxRow.isUnique === 1;
|
||||
|
||||
const tableInResult = result[tableName];
|
||||
if (typeof tableInResult === 'undefined') continue;
|
||||
|
||||
indexesCount += 1;
|
||||
if (progressCallback) {
|
||||
progressCallback('indexes', indexesCount, 'fetching');
|
||||
}
|
||||
|
||||
if (
|
||||
typeof tableInResult.indexes[constraintName] !== 'undefined'
|
||||
&& columnName
|
||||
) {
|
||||
tableInResult.indexes[constraintName]!.columns.push(columnName);
|
||||
} else {
|
||||
tableInResult.indexes[constraintName] = {
|
||||
name: constraintName,
|
||||
columns: columnName ? [columnName] : [],
|
||||
isUnique: isUnique,
|
||||
};
|
||||
}
|
||||
// if (isUnique) {
|
||||
// if (typeof tableInResult.uniqueConstraints[constraintName] !== "undefined") {
|
||||
// tableInResult.uniqueConstraints[constraintName]!.columns.push(columnName);
|
||||
// } else {
|
||||
// tableInResult.uniqueConstraints[constraintName] = {
|
||||
// name: constraintName,
|
||||
// columns: [columnName],
|
||||
// };
|
||||
// }
|
||||
// } else {
|
||||
// if (typeof tableInResult.indexes[constraintName] !== "undefined") {
|
||||
// tableInResult.indexes[constraintName]!.columns.push(columnName);
|
||||
// } else {
|
||||
// tableInResult.indexes[constraintName] = {
|
||||
// name: constraintName,
|
||||
// columns: [columnName],
|
||||
// isUnique: isUnique,
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
}
|
||||
if (progressCallback) {
|
||||
progressCallback('indexes', indexesCount, 'done');
|
||||
// progressCallback("enums", 0, "fetching");
|
||||
progressCallback('enums', 0, 'done');
|
||||
}
|
||||
|
||||
const views = await db.query(
|
||||
`SELECT name AS view_name, sql AS sql FROM sqlite_master WHERE type = 'view';`,
|
||||
);
|
||||
|
||||
viewsCount = views.length;
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback('views', viewsCount, 'fetching');
|
||||
}
|
||||
for (const view of views) {
|
||||
const viewName = view['view_name'];
|
||||
const sql = view['sql'];
|
||||
|
||||
const regex = new RegExp(`\\bAS\\b\\s+(SELECT.+)$`, 'i');
|
||||
const match = sql.match(regex);
|
||||
|
||||
if (!match) {
|
||||
console.log('Could not process view');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const viewDefinition = match[1] as string;
|
||||
|
||||
const columns = result[viewName].columns;
|
||||
delete result[viewName];
|
||||
|
||||
resultViews[viewName] = {
|
||||
columns: columns,
|
||||
isExisting: false,
|
||||
name: viewName,
|
||||
definition: viewDefinition,
|
||||
};
|
||||
}
|
||||
if (progressCallback) {
|
||||
progressCallback('views', viewsCount, 'done');
|
||||
}
|
||||
|
||||
const namedCheckPattern = /CONSTRAINT\s*["']?(\w+)["']?\s*CHECK\s*\((.*?)\)/gi;
|
||||
const unnamedCheckPattern = /CHECK\s*\((.*?)\)/gi;
|
||||
let checkCounter = 0;
|
||||
const checkConstraints: Record<string, CheckConstraint> = {};
|
||||
const checks = await db.query<{
|
||||
tableName: string;
|
||||
sql: string;
|
||||
}>(`SELECT
|
||||
name as "tableName",
|
||||
sql as "sql"
|
||||
FROM sqlite_master
|
||||
WHERE type = 'table'
|
||||
AND ${filterIgnoredTablesByField('tbl_name')};`);
|
||||
for (const check of checks) {
|
||||
if (!tablesFilter(check.tableName)) continue;
|
||||
|
||||
const { tableName, sql } = check;
|
||||
|
||||
// Find named CHECK constraints
|
||||
let namedChecks = [...sql.matchAll(namedCheckPattern)];
|
||||
if (namedChecks.length > 0) {
|
||||
namedChecks.forEach(([_, checkName, checkValue]) => {
|
||||
checkConstraints[checkName] = {
|
||||
name: checkName,
|
||||
value: checkValue.trim(),
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// If no named constraints, find unnamed CHECK constraints and assign names
|
||||
let unnamedChecks = [...sql.matchAll(unnamedCheckPattern)];
|
||||
unnamedChecks.forEach(([_, checkValue]) => {
|
||||
let checkName = `${tableName}_check_${++checkCounter}`;
|
||||
checkConstraints[checkName] = {
|
||||
name: checkName,
|
||||
value: checkValue.trim(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
checksCount += Object.values(checkConstraints).length;
|
||||
if (progressCallback) {
|
||||
progressCallback('checks', checksCount, 'fetching');
|
||||
}
|
||||
|
||||
const table = result[tableName];
|
||||
|
||||
if (!table) {
|
||||
result[tableName] = {
|
||||
name: tableName,
|
||||
columns: {},
|
||||
compositePrimaryKeys: {},
|
||||
indexes: {},
|
||||
foreignKeys: {},
|
||||
uniqueConstraints: {},
|
||||
checkConstraints: checkConstraints,
|
||||
};
|
||||
} else {
|
||||
result[tableName]!.checkConstraints = checkConstraints;
|
||||
}
|
||||
}
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback('checks', checksCount, 'done');
|
||||
}
|
||||
|
||||
return {
|
||||
version: '6',
|
||||
dialect: 'sqlite',
|
||||
tables: result,
|
||||
views: resultViews,
|
||||
enums: {},
|
||||
_meta: {
|
||||
tables: {},
|
||||
columns: {},
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,848 @@
|
||||
/// <reference types="@cloudflare/workers-types" />
|
||||
import type { PGlite } from '@electric-sql/pglite';
|
||||
import { serve } from '@hono/node-server';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { createHash } from 'crypto';
|
||||
import type { AnyColumn, AnyTable } from 'drizzle-orm';
|
||||
import { is } from 'drizzle-orm';
|
||||
import type { AnyMySqlTable } from 'drizzle-orm/mysql-core';
|
||||
import { getTableConfig as mysqlTableConfig, MySqlTable } from 'drizzle-orm/mysql-core';
|
||||
import type { AnyPgTable } from 'drizzle-orm/pg-core';
|
||||
import { getTableConfig as pgTableConfig, PgTable } from 'drizzle-orm/pg-core';
|
||||
import type { TablesRelationalConfig } from 'drizzle-orm/relations';
|
||||
import {
|
||||
createTableRelationsHelpers,
|
||||
extractTablesRelationalConfig,
|
||||
Many,
|
||||
normalizeRelation,
|
||||
One,
|
||||
Relations,
|
||||
} from 'drizzle-orm/relations';
|
||||
import type { AnySingleStoreTable } from 'drizzle-orm/singlestore-core';
|
||||
import { getTableConfig as singlestoreTableConfig, SingleStoreTable } from 'drizzle-orm/singlestore-core';
|
||||
import type { AnySQLiteTable } from 'drizzle-orm/sqlite-core';
|
||||
import { getTableConfig as sqliteTableConfig, SQLiteTable } from 'drizzle-orm/sqlite-core';
|
||||
import fs from 'fs';
|
||||
import { Hono } from 'hono';
|
||||
import { compress } from 'hono/compress';
|
||||
import { cors } from 'hono/cors';
|
||||
import { createServer } from 'node:https';
|
||||
import type { CasingType } from 'src/cli/validations/common';
|
||||
import type { LibSQLCredentials } from 'src/cli/validations/libsql';
|
||||
import { assertUnreachable } from 'src/global';
|
||||
import { z } from 'zod';
|
||||
import { safeRegister } from '../cli/commands/utils';
|
||||
import type { MysqlCredentials } from '../cli/validations/mysql';
|
||||
import type { PostgresCredentials } from '../cli/validations/postgres';
|
||||
import type { SingleStoreCredentials } from '../cli/validations/singlestore';
|
||||
import type { SqliteCredentials } from '../cli/validations/sqlite';
|
||||
import type { Proxy, TransactionProxy } from '../utils';
|
||||
import { prepareFilenames } from '.';
|
||||
import { getColumnCasing } from './utils';
|
||||
|
||||
type CustomDefault = {
|
||||
schema: string;
|
||||
table: string;
|
||||
column: string;
|
||||
func: () => unknown;
|
||||
};
|
||||
|
||||
type SchemaFile = {
|
||||
name: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
export type Setup = {
|
||||
dbHash: string;
|
||||
dialect: 'postgresql' | 'mysql' | 'sqlite' | 'singlestore';
|
||||
packageName:
|
||||
| '@aws-sdk/client-rds-data'
|
||||
| 'pglite'
|
||||
| 'pg'
|
||||
| 'postgres'
|
||||
| '@vercel/postgres'
|
||||
| '@neondatabase/serverless'
|
||||
| 'gel'
|
||||
| 'mysql2'
|
||||
| '@planetscale/database'
|
||||
| 'd1-http'
|
||||
| 'd1'
|
||||
| '@libsql/client'
|
||||
| 'better-sqlite3';
|
||||
driver?: 'aws-data-api' | 'd1-http' | 'd1' | 'turso' | 'pglite';
|
||||
databaseName?: string; // for planetscale (driver remove database name from connection string)
|
||||
proxy: Proxy;
|
||||
transactionProxy: TransactionProxy;
|
||||
customDefaults: CustomDefault[];
|
||||
schema: Record<string, Record<string, AnyTable<any>>>;
|
||||
relations: Record<string, Relations>;
|
||||
casing?: CasingType;
|
||||
schemaFiles?: SchemaFile[];
|
||||
};
|
||||
|
||||
export type ProxyParams = {
|
||||
sql: string;
|
||||
params?: any[];
|
||||
typings?: any[];
|
||||
mode: 'array' | 'object';
|
||||
method: 'values' | 'get' | 'all' | 'run' | 'execute';
|
||||
};
|
||||
|
||||
export const preparePgSchema = async (path: string | string[]) => {
|
||||
const imports = prepareFilenames(path);
|
||||
const pgSchema: Record<string, Record<string, AnyPgTable>> = {};
|
||||
const relations: Record<string, Relations> = {};
|
||||
|
||||
// files content as string
|
||||
const files = imports.map((it, index) => ({
|
||||
// get the file name from the path
|
||||
name: it.split('/').pop() || `schema${index}.ts`,
|
||||
content: fs.readFileSync(it, 'utf-8'),
|
||||
}));
|
||||
|
||||
await safeRegister(async () => {
|
||||
for (let i = 0; i < imports.length; i++) {
|
||||
const it = imports[i];
|
||||
|
||||
const i0: Record<string, unknown> = require(`${it}`);
|
||||
const i0values = Object.entries(i0);
|
||||
|
||||
i0values.forEach(([k, t]) => {
|
||||
if (is(t, PgTable)) {
|
||||
const schema = pgTableConfig(t).schema || 'public';
|
||||
pgSchema[schema] = pgSchema[schema] || {};
|
||||
pgSchema[schema][k] = t;
|
||||
}
|
||||
|
||||
if (is(t, Relations)) {
|
||||
relations[k] = t;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return { schema: pgSchema, relations, files };
|
||||
};
|
||||
|
||||
export const prepareMySqlSchema = async (path: string | string[]) => {
|
||||
const imports = prepareFilenames(path);
|
||||
const mysqlSchema: Record<string, Record<string, AnyMySqlTable>> = {
|
||||
public: {},
|
||||
};
|
||||
const relations: Record<string, Relations> = {};
|
||||
|
||||
// files content as string
|
||||
const files = imports.map((it, index) => ({
|
||||
// get the file name from the path
|
||||
name: it.split('/').pop() || `schema${index}.ts`,
|
||||
content: fs.readFileSync(it, 'utf-8'),
|
||||
}));
|
||||
|
||||
await safeRegister(async () => {
|
||||
for (let i = 0; i < imports.length; i++) {
|
||||
const it = imports[i];
|
||||
|
||||
const i0: Record<string, unknown> = require(`${it}`);
|
||||
const i0values = Object.entries(i0);
|
||||
|
||||
i0values.forEach(([k, t]) => {
|
||||
if (is(t, MySqlTable)) {
|
||||
const schema = mysqlTableConfig(t).schema || 'public';
|
||||
mysqlSchema[schema][k] = t;
|
||||
}
|
||||
|
||||
if (is(t, Relations)) {
|
||||
relations[k] = t;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return { schema: mysqlSchema, relations, files };
|
||||
};
|
||||
|
||||
export const prepareSQLiteSchema = async (path: string | string[]) => {
|
||||
const imports = prepareFilenames(path);
|
||||
const sqliteSchema: Record<string, Record<string, AnySQLiteTable>> = {
|
||||
public: {},
|
||||
};
|
||||
const relations: Record<string, Relations> = {};
|
||||
|
||||
// files content as string
|
||||
const files = imports.map((it, index) => ({
|
||||
// get the file name from the path
|
||||
name: it.split('/').pop() || `schema${index}.ts`,
|
||||
content: fs.readFileSync(it, 'utf-8'),
|
||||
}));
|
||||
|
||||
await safeRegister(async () => {
|
||||
for (let i = 0; i < imports.length; i++) {
|
||||
const it = imports[i];
|
||||
|
||||
const i0: Record<string, unknown> = require(`${it}`);
|
||||
const i0values = Object.entries(i0);
|
||||
|
||||
i0values.forEach(([k, t]) => {
|
||||
if (is(t, SQLiteTable)) {
|
||||
const schema = 'public'; // sqlite does not have schemas
|
||||
sqliteSchema[schema][k] = t;
|
||||
}
|
||||
|
||||
if (is(t, Relations)) {
|
||||
relations[k] = t;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return { schema: sqliteSchema, relations, files };
|
||||
};
|
||||
|
||||
export const prepareSingleStoreSchema = async (path: string | string[]) => {
|
||||
const imports = prepareFilenames(path);
|
||||
const singlestoreSchema: Record<
|
||||
string,
|
||||
Record<string, AnySingleStoreTable>
|
||||
> = {
|
||||
public: {},
|
||||
};
|
||||
const relations: Record<string, Relations> = {};
|
||||
|
||||
// files content as string
|
||||
const files = imports.map((it, index) => ({
|
||||
// get the file name from the path
|
||||
name: it.split('/').pop() || `schema${index}.ts`,
|
||||
content: fs.readFileSync(it, 'utf-8'),
|
||||
}));
|
||||
|
||||
await safeRegister(async () => {
|
||||
for (let i = 0; i < imports.length; i++) {
|
||||
const it = imports[i];
|
||||
|
||||
const i0: Record<string, unknown> = require(`${it}`);
|
||||
const i0values = Object.entries(i0);
|
||||
|
||||
i0values.forEach(([k, t]) => {
|
||||
if (is(t, SingleStoreTable)) {
|
||||
const schema = singlestoreTableConfig(t).schema || 'public';
|
||||
singlestoreSchema[schema][k] = t;
|
||||
}
|
||||
|
||||
if (is(t, Relations)) {
|
||||
relations[k] = t;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return { schema: singlestoreSchema, relations, files };
|
||||
};
|
||||
|
||||
const getCustomDefaults = <T extends AnyTable<{}>>(
|
||||
schema: Record<string, Record<string, T>>,
|
||||
casing?: CasingType,
|
||||
): CustomDefault[] => {
|
||||
const customDefaults: CustomDefault[] = [];
|
||||
|
||||
Object.entries(schema).map(([schema, tables]) => {
|
||||
Object.entries(tables).map(([, table]) => {
|
||||
let tableConfig: {
|
||||
name: string;
|
||||
columns: AnyColumn[];
|
||||
};
|
||||
if (is(table, PgTable)) {
|
||||
tableConfig = pgTableConfig(table);
|
||||
} else if (is(table, MySqlTable)) {
|
||||
tableConfig = mysqlTableConfig(table);
|
||||
} else if (is(table, SQLiteTable)) {
|
||||
tableConfig = sqliteTableConfig(table);
|
||||
} else {
|
||||
tableConfig = singlestoreTableConfig(table as SingleStoreTable);
|
||||
}
|
||||
|
||||
tableConfig.columns.map((column) => {
|
||||
if (column.defaultFn) {
|
||||
customDefaults.push({
|
||||
schema,
|
||||
table: tableConfig.name,
|
||||
column: getColumnCasing(column, casing),
|
||||
func: column.defaultFn,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return customDefaults;
|
||||
};
|
||||
|
||||
export const drizzleForPostgres = async (
|
||||
credentials: PostgresCredentials | {
|
||||
driver: 'pglite';
|
||||
client: PGlite;
|
||||
},
|
||||
pgSchema: Record<string, Record<string, AnyPgTable>>,
|
||||
relations: Record<string, Relations>,
|
||||
schemaFiles?: SchemaFile[],
|
||||
casing?: CasingType,
|
||||
): Promise<Setup> => {
|
||||
const { preparePostgresDB } = await import('../cli/connections');
|
||||
const db = await preparePostgresDB(credentials);
|
||||
const customDefaults = getCustomDefaults(pgSchema, casing);
|
||||
|
||||
let dbUrl: string;
|
||||
|
||||
if ('driver' in credentials) {
|
||||
const { driver } = credentials;
|
||||
if (driver === 'aws-data-api') {
|
||||
dbUrl = `aws-data-api://${credentials.database}/${credentials.secretArn}/${credentials.resourceArn}`;
|
||||
} else if (driver === 'pglite') {
|
||||
dbUrl = 'client' in credentials ? credentials.client.dataDir || 'pglite://custom-client' : credentials.url;
|
||||
} else {
|
||||
assertUnreachable(driver);
|
||||
}
|
||||
} else if ('url' in credentials) {
|
||||
dbUrl = credentials.url;
|
||||
} else {
|
||||
dbUrl =
|
||||
`postgresql://${credentials.user}:${credentials.password}@${credentials.host}:${credentials.port}/${credentials.database}`;
|
||||
}
|
||||
|
||||
const dbHash = createHash('sha256').update(dbUrl).digest('hex');
|
||||
|
||||
return {
|
||||
dbHash,
|
||||
dialect: 'postgresql',
|
||||
driver: 'driver' in credentials ? credentials.driver : undefined,
|
||||
packageName: db.packageName,
|
||||
proxy: db.proxy,
|
||||
transactionProxy: db.transactionProxy,
|
||||
customDefaults,
|
||||
schema: pgSchema,
|
||||
relations,
|
||||
schemaFiles,
|
||||
casing,
|
||||
};
|
||||
};
|
||||
|
||||
export const drizzleForMySQL = async (
|
||||
credentials: MysqlCredentials,
|
||||
mysqlSchema: Record<string, Record<string, AnyMySqlTable>>,
|
||||
relations: Record<string, Relations>,
|
||||
schemaFiles?: SchemaFile[],
|
||||
casing?: CasingType,
|
||||
): Promise<Setup> => {
|
||||
const { connectToMySQL } = await import('../cli/connections');
|
||||
const { proxy, transactionProxy, database, packageName } = await connectToMySQL(credentials);
|
||||
|
||||
const customDefaults = getCustomDefaults(mysqlSchema, casing);
|
||||
|
||||
let dbUrl: string;
|
||||
|
||||
if ('url' in credentials) {
|
||||
dbUrl = credentials.url;
|
||||
} else {
|
||||
dbUrl =
|
||||
`mysql://${credentials.user}:${credentials.password}@${credentials.host}:${credentials.port}/${credentials.database}`;
|
||||
}
|
||||
|
||||
const dbHash = createHash('sha256').update(dbUrl).digest('hex');
|
||||
|
||||
return {
|
||||
dbHash,
|
||||
dialect: 'mysql',
|
||||
packageName,
|
||||
databaseName: database,
|
||||
proxy,
|
||||
transactionProxy,
|
||||
customDefaults,
|
||||
schema: mysqlSchema,
|
||||
relations,
|
||||
schemaFiles,
|
||||
casing,
|
||||
};
|
||||
};
|
||||
|
||||
// D1 binding credentials type (mirrors the one in connections.ts)
|
||||
type D1BindingCredentials = {
|
||||
driver: 'd1';
|
||||
binding: D1Database;
|
||||
};
|
||||
|
||||
export const drizzleForSQLite = async (
|
||||
credentials: SqliteCredentials | D1BindingCredentials,
|
||||
sqliteSchema: Record<string, Record<string, AnySQLiteTable>>,
|
||||
relations: Record<string, Relations>,
|
||||
schemaFiles?: SchemaFile[],
|
||||
casing?: CasingType,
|
||||
): Promise<Setup> => {
|
||||
const customDefaults = getCustomDefaults(sqliteSchema, casing);
|
||||
|
||||
if ('driver' in credentials && credentials.driver === 'd1') {
|
||||
const { connectToD1 } = await import('../cli/connections');
|
||||
const sqliteDB = await connectToD1(credentials.binding);
|
||||
|
||||
const dbUrl = 'd1://binding';
|
||||
const dbHash = createHash('sha256').update(dbUrl).digest('hex');
|
||||
|
||||
return {
|
||||
dbHash,
|
||||
dialect: 'sqlite',
|
||||
driver: 'd1',
|
||||
packageName: 'd1',
|
||||
proxy: sqliteDB.proxy,
|
||||
transactionProxy: sqliteDB.transactionProxy,
|
||||
customDefaults,
|
||||
schema: sqliteSchema,
|
||||
relations,
|
||||
schemaFiles,
|
||||
casing,
|
||||
};
|
||||
}
|
||||
|
||||
const { connectToSQLite } = await import('../cli/connections');
|
||||
const sqliteDB = await connectToSQLite(credentials);
|
||||
|
||||
let dbUrl: string;
|
||||
|
||||
if ('driver' in credentials) {
|
||||
const { driver } = credentials;
|
||||
if (driver === 'd1-http') {
|
||||
dbUrl = `d1-http://${credentials.accountId}/${credentials.databaseId}/${credentials.token}`;
|
||||
} else {
|
||||
assertUnreachable(driver);
|
||||
}
|
||||
} else {
|
||||
dbUrl = credentials.url;
|
||||
}
|
||||
|
||||
const dbHash = createHash('sha256').update(dbUrl).digest('hex');
|
||||
|
||||
return {
|
||||
dbHash,
|
||||
dialect: 'sqlite',
|
||||
driver: 'driver' in credentials ? credentials.driver : undefined,
|
||||
packageName: sqliteDB.packageName,
|
||||
proxy: sqliteDB.proxy,
|
||||
transactionProxy: sqliteDB.transactionProxy,
|
||||
customDefaults,
|
||||
schema: sqliteSchema,
|
||||
relations,
|
||||
schemaFiles,
|
||||
casing,
|
||||
};
|
||||
};
|
||||
export const drizzleForLibSQL = async (
|
||||
credentials: LibSQLCredentials,
|
||||
sqliteSchema: Record<string, Record<string, AnySQLiteTable>>,
|
||||
relations: Record<string, Relations>,
|
||||
schemaFiles?: SchemaFile[],
|
||||
casing?: CasingType,
|
||||
): Promise<Setup> => {
|
||||
const { connectToLibSQL } = await import('../cli/connections');
|
||||
|
||||
const sqliteDB = await connectToLibSQL(credentials);
|
||||
const customDefaults = getCustomDefaults(sqliteSchema, casing);
|
||||
|
||||
let dbUrl: string = `turso://${credentials.url}/${credentials.authToken}`;
|
||||
|
||||
const dbHash = createHash('sha256').update(dbUrl).digest('hex');
|
||||
|
||||
return {
|
||||
dbHash,
|
||||
dialect: 'sqlite',
|
||||
driver: undefined,
|
||||
packageName: sqliteDB.packageName,
|
||||
proxy: sqliteDB.proxy,
|
||||
transactionProxy: sqliteDB.transactionProxy,
|
||||
customDefaults,
|
||||
schema: sqliteSchema,
|
||||
relations,
|
||||
schemaFiles,
|
||||
casing,
|
||||
};
|
||||
};
|
||||
|
||||
export const drizzleForSingleStore = async (
|
||||
credentials: SingleStoreCredentials,
|
||||
singlestoreSchema: Record<string, Record<string, AnySingleStoreTable>>,
|
||||
relations: Record<string, Relations>,
|
||||
schemaFiles?: SchemaFile[],
|
||||
casing?: CasingType,
|
||||
): Promise<Setup> => {
|
||||
const { connectToSingleStore } = await import('../cli/connections');
|
||||
const { proxy, transactionProxy, database, packageName } = await connectToSingleStore(credentials);
|
||||
|
||||
const customDefaults = getCustomDefaults(singlestoreSchema, casing);
|
||||
|
||||
let dbUrl: string;
|
||||
|
||||
if ('url' in credentials) {
|
||||
dbUrl = credentials.url;
|
||||
} else {
|
||||
dbUrl =
|
||||
`singlestore://${credentials.user}:${credentials.password}@${credentials.host}:${credentials.port}/${credentials.database}`;
|
||||
}
|
||||
|
||||
const dbHash = createHash('sha256').update(dbUrl).digest('hex');
|
||||
|
||||
return {
|
||||
dbHash,
|
||||
dialect: 'singlestore',
|
||||
databaseName: database,
|
||||
packageName,
|
||||
proxy,
|
||||
transactionProxy,
|
||||
customDefaults,
|
||||
schema: singlestoreSchema,
|
||||
relations,
|
||||
schemaFiles,
|
||||
casing,
|
||||
};
|
||||
};
|
||||
|
||||
type Relation = {
|
||||
name: string;
|
||||
type: 'one' | 'many';
|
||||
table: string;
|
||||
schema: string;
|
||||
columns: string[];
|
||||
refTable: string;
|
||||
refSchema: string;
|
||||
refColumns: string[];
|
||||
};
|
||||
|
||||
export const extractRelations = (
|
||||
tablesConfig: {
|
||||
tables: TablesRelationalConfig;
|
||||
tableNamesMap: Record<string, string>;
|
||||
},
|
||||
casing?: CasingType,
|
||||
): Relation[] => {
|
||||
const relations = Object.values(tablesConfig.tables)
|
||||
.map((it) =>
|
||||
Object.entries(it.relations).map(([name, relation]) => {
|
||||
try {
|
||||
const normalized = normalizeRelation(
|
||||
tablesConfig.tables,
|
||||
tablesConfig.tableNamesMap,
|
||||
relation,
|
||||
);
|
||||
const rel = relation;
|
||||
const refTableName = rel.referencedTableName;
|
||||
const refTable = rel.referencedTable;
|
||||
const fields = normalized.fields
|
||||
.map((it) => getColumnCasing(it, casing))
|
||||
.flat();
|
||||
const refColumns = normalized.references
|
||||
.map((it) => getColumnCasing(it, casing))
|
||||
.flat();
|
||||
|
||||
let refSchema: string | undefined;
|
||||
if (is(refTable, PgTable)) {
|
||||
refSchema = pgTableConfig(refTable).schema;
|
||||
} else if (is(refTable, MySqlTable)) {
|
||||
refSchema = mysqlTableConfig(refTable).schema;
|
||||
} else if (is(refTable, SQLiteTable)) {
|
||||
refSchema = undefined;
|
||||
} else if (is(refTable, SingleStoreTable)) {
|
||||
refSchema = singlestoreTableConfig(refTable).schema;
|
||||
} else {
|
||||
throw new Error('unsupported dialect');
|
||||
}
|
||||
|
||||
let type: 'one' | 'many';
|
||||
if (is(rel, One)) {
|
||||
type = 'one';
|
||||
} else if (is(rel, Many)) {
|
||||
type = 'many';
|
||||
} else {
|
||||
throw new Error('unsupported relation type');
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
type,
|
||||
table: it.dbName,
|
||||
schema: it.schema || 'public',
|
||||
columns: fields,
|
||||
refTable: refTableName,
|
||||
refSchema: refSchema || 'public',
|
||||
refColumns: refColumns,
|
||||
};
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Invalid relation "${relation.fieldName}" for table "${
|
||||
it.schema ? `${it.schema}.${it.dbName}` : it.dbName
|
||||
}"`,
|
||||
);
|
||||
}
|
||||
})
|
||||
)
|
||||
.flat();
|
||||
return relations;
|
||||
};
|
||||
|
||||
const init = z.object({
|
||||
type: z.literal('init'),
|
||||
});
|
||||
|
||||
const proxySchema = z.object({
|
||||
type: z.literal('proxy'),
|
||||
data: z.object({
|
||||
sql: z.string(),
|
||||
params: z.array(z.any()).optional(),
|
||||
typings: z.string().array().optional(),
|
||||
mode: z.enum(['array', 'object']).default('object'),
|
||||
method: z.union([
|
||||
z.literal('values'),
|
||||
z.literal('get'),
|
||||
z.literal('all'),
|
||||
z.literal('run'),
|
||||
z.literal('execute'),
|
||||
]),
|
||||
}),
|
||||
});
|
||||
|
||||
const transactionProxySchema = z.object({
|
||||
type: z.literal('tproxy'),
|
||||
data: z
|
||||
.object({
|
||||
sql: z.string(),
|
||||
method: z
|
||||
.union([
|
||||
z.literal('values'),
|
||||
z.literal('get'),
|
||||
z.literal('all'),
|
||||
z.literal('run'),
|
||||
z.literal('execute'),
|
||||
])
|
||||
.optional(),
|
||||
})
|
||||
.array(),
|
||||
});
|
||||
|
||||
const defaultsSchema = z.object({
|
||||
type: z.literal('defaults'),
|
||||
data: z
|
||||
.array(
|
||||
z.object({
|
||||
schema: z.string(),
|
||||
table: z.string(),
|
||||
column: z.string(),
|
||||
}),
|
||||
)
|
||||
.min(1),
|
||||
});
|
||||
|
||||
const schema = z.union([
|
||||
init,
|
||||
proxySchema,
|
||||
transactionProxySchema,
|
||||
defaultsSchema,
|
||||
]);
|
||||
|
||||
const jsonStringify = (data: any) => {
|
||||
return JSON.stringify(data, (_key, value) => {
|
||||
// Convert Error to object
|
||||
if (value instanceof Error) {
|
||||
return {
|
||||
error: value.message,
|
||||
};
|
||||
}
|
||||
|
||||
// Convert BigInt to string
|
||||
if (typeof value === 'bigint') {
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
// Convert Buffer and ArrayBuffer to base64
|
||||
if (
|
||||
(value
|
||||
&& typeof value === 'object'
|
||||
&& 'type' in value
|
||||
&& 'data' in value
|
||||
&& value.type === 'Buffer')
|
||||
|| value instanceof ArrayBuffer
|
||||
|| value instanceof Buffer
|
||||
) {
|
||||
return Buffer.from(value).toString('base64');
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
};
|
||||
|
||||
export type Server = {
|
||||
start: (params: {
|
||||
host: string;
|
||||
port: number;
|
||||
key?: string;
|
||||
cert?: string;
|
||||
cb: (err: Error | null, address: string) => void;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
export const prepareServer = async (
|
||||
{
|
||||
dialect,
|
||||
driver,
|
||||
packageName,
|
||||
databaseName,
|
||||
proxy,
|
||||
transactionProxy,
|
||||
customDefaults,
|
||||
schema: drizzleSchema,
|
||||
relations,
|
||||
dbHash,
|
||||
casing,
|
||||
schemaFiles,
|
||||
}: Setup,
|
||||
app?: Hono,
|
||||
): Promise<Server> => {
|
||||
app = app !== undefined ? app : new Hono();
|
||||
|
||||
app.use(compress());
|
||||
app.use(async (ctx, next) => {
|
||||
await next();
|
||||
// * https://wicg.github.io/private-network-access/#headers
|
||||
// * https://github.com/drizzle-team/drizzle-orm/issues/1857#issuecomment-2395724232
|
||||
ctx.header('Access-Control-Allow-Private-Network', 'true');
|
||||
});
|
||||
app.use(cors());
|
||||
app.onError((err, ctx) => {
|
||||
console.error(err);
|
||||
return ctx.json({
|
||||
status: 'error',
|
||||
error: err.message,
|
||||
});
|
||||
});
|
||||
|
||||
const relationalSchema: Record<string, unknown> = {
|
||||
...Object.fromEntries(
|
||||
Object.entries(drizzleSchema)
|
||||
.map(([schemaName, schema]) => {
|
||||
// have unique keys across schemas
|
||||
const mappedTableEntries = Object.entries(schema).map(
|
||||
([tableName, table]) => {
|
||||
return [`__${schemaName}__.${tableName}`, table];
|
||||
},
|
||||
);
|
||||
|
||||
return mappedTableEntries;
|
||||
})
|
||||
.flat(),
|
||||
),
|
||||
...relations,
|
||||
};
|
||||
|
||||
const relationsConfig = extractTablesRelationalConfig(
|
||||
relationalSchema,
|
||||
createTableRelationsHelpers,
|
||||
);
|
||||
|
||||
app.post('/', zValidator('json', schema), async (c) => {
|
||||
const body = c.req.valid('json');
|
||||
const { type } = body;
|
||||
|
||||
if (type === 'init') {
|
||||
const preparedDefaults = customDefaults.map((d) => ({
|
||||
schema: d.schema,
|
||||
table: d.table,
|
||||
column: d.column,
|
||||
}));
|
||||
|
||||
let relations: Relation[] = [];
|
||||
// Attempt to extract relations from the relational config.
|
||||
// An error may occur if the relations are ambiguous or misconfigured.
|
||||
try {
|
||||
relations = extractRelations(relationsConfig, casing);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'Failed to extract relations. This is likely due to ambiguous or misconfigured relations.',
|
||||
);
|
||||
console.warn(
|
||||
'Please check your schema and ensure that all relations are correctly defined.',
|
||||
);
|
||||
console.warn(
|
||||
'See: https://orm.drizzle.team/docs/relations#disambiguating-relations',
|
||||
);
|
||||
console.warn('Error message:', (error as Error).message);
|
||||
}
|
||||
|
||||
return c.json({
|
||||
version: '6.2',
|
||||
dialect,
|
||||
driver,
|
||||
packageName,
|
||||
schemaFiles,
|
||||
customDefaults: preparedDefaults,
|
||||
relations,
|
||||
dbHash,
|
||||
databaseName,
|
||||
});
|
||||
}
|
||||
|
||||
if (type === 'proxy') {
|
||||
const result = await proxy({
|
||||
...body.data,
|
||||
params: body.data.params || [],
|
||||
});
|
||||
return c.json(JSON.parse(jsonStringify(result)));
|
||||
}
|
||||
|
||||
if (type === 'tproxy') {
|
||||
const result = await transactionProxy(body.data);
|
||||
return c.json(JSON.parse(jsonStringify(result)));
|
||||
}
|
||||
|
||||
if (type === 'defaults') {
|
||||
const columns = body.data;
|
||||
|
||||
const result = columns.map((column) => {
|
||||
const found = customDefaults.find((d) => {
|
||||
return (
|
||||
d.schema === column.schema
|
||||
&& d.table === column.table
|
||||
&& d.column === column.column
|
||||
);
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
throw new Error(
|
||||
`Custom default not found for ${column.schema}.${column.table}.${column.column}`,
|
||||
);
|
||||
}
|
||||
|
||||
const value = found.func();
|
||||
|
||||
return {
|
||||
...column,
|
||||
value,
|
||||
};
|
||||
});
|
||||
|
||||
return c.json(JSON.parse(jsonStringify(result)));
|
||||
}
|
||||
|
||||
throw new Error(`Unknown type: ${type}`);
|
||||
});
|
||||
|
||||
return {
|
||||
start: (params: Parameters<Server['start']>[0]) => {
|
||||
serve(
|
||||
{
|
||||
fetch: app!.fetch,
|
||||
createServer: params.key ? createServer : undefined,
|
||||
hostname: params.host,
|
||||
port: params.port,
|
||||
serverOptions: {
|
||||
key: params.key,
|
||||
cert: params.cert,
|
||||
},
|
||||
},
|
||||
() => params.cb(null, `${params.host}:${params.port}`),
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { SQL } from 'drizzle-orm';
|
||||
import { CasingCache, toCamelCase, toSnakeCase } from 'drizzle-orm/casing';
|
||||
import { CasingType } from '../cli/validations/common';
|
||||
|
||||
export function getColumnCasing(
|
||||
column: { keyAsName: boolean; name: string | undefined },
|
||||
casing: CasingType | undefined,
|
||||
) {
|
||||
if (!column.name) return '';
|
||||
return !column.keyAsName || casing === undefined
|
||||
? column.name
|
||||
: casing === 'camelCase'
|
||||
? toCamelCase(column.name)
|
||||
: toSnakeCase(column.name);
|
||||
}
|
||||
|
||||
export const sqlToStr = (sql: SQL, casing: CasingType | undefined) => {
|
||||
return sql.toQuery({
|
||||
escapeName: () => {
|
||||
throw new Error("we don't support params for `sql` default values");
|
||||
},
|
||||
escapeParam: () => {
|
||||
throw new Error("we don't support params for `sql` default values");
|
||||
},
|
||||
escapeString: () => {
|
||||
throw new Error("we don't support params for `sql` default values");
|
||||
},
|
||||
casing: new CasingCache(casing),
|
||||
}).sql;
|
||||
};
|
||||
|
||||
export const sqlToStrGenerated = (sql: SQL, casing: CasingType | undefined) => {
|
||||
return sql.toQuery({
|
||||
escapeName: () => {
|
||||
throw new Error("we don't support params for `sql` default values");
|
||||
},
|
||||
escapeParam: () => {
|
||||
throw new Error("we don't support params for `sql` default values");
|
||||
},
|
||||
escapeString: () => {
|
||||
throw new Error("we don't support params for `sql` default values");
|
||||
},
|
||||
casing: new CasingCache(casing),
|
||||
}).sql;
|
||||
};
|
||||
@@ -0,0 +1,157 @@
|
||||
declare global {
|
||||
interface Array<T> {
|
||||
exactlyOne(): T;
|
||||
}
|
||||
}
|
||||
|
||||
Array.prototype.exactlyOne = function() {
|
||||
if (this.length !== 1) {
|
||||
return undefined;
|
||||
}
|
||||
return this[0];
|
||||
};
|
||||
|
||||
interface TablesHandler<T extends Named> {
|
||||
can(added: T[], removed: T[]): boolean;
|
||||
handle(added: T[], removed: T[]): { created: T[]; deleted: T[]; renamed: { from: T; to: T }[] };
|
||||
}
|
||||
|
||||
interface ColumnsHandler<T extends Named> {
|
||||
can(tableName: string, added: T[], removed: T[]): boolean;
|
||||
handle(
|
||||
tableName: string,
|
||||
added: T[],
|
||||
removed: T[],
|
||||
): { tableName: string; created: T[]; deleted: T[]; renamed: { from: T; to: T }[] };
|
||||
}
|
||||
|
||||
class DryRun<T extends Named> implements TablesHandler<T> {
|
||||
can(added: T[], removed: T[]): boolean {
|
||||
return added.length === 0 && removed.length === 0;
|
||||
}
|
||||
handle(added: T[], _: T[]): { created: T[]; deleted: T[]; renamed: { from: T; to: T }[] } {
|
||||
return { created: added, deleted: [], renamed: [] };
|
||||
}
|
||||
}
|
||||
|
||||
// class Fallback implements Handler {
|
||||
// can(_: Table[], __: Table[]): boolean {
|
||||
// return true
|
||||
// }
|
||||
// handle(added: Table[], _: Table[]): { created: Table[]; deleted: Table[]; renamed: { from: Table; to: Table; }[]; } {
|
||||
// return { created: added, deleted: , renamed: [] }
|
||||
// }
|
||||
// }
|
||||
|
||||
class Case1<T extends Named> implements TablesHandler<T> {
|
||||
can(_: T[], removed: T[]): boolean {
|
||||
return removed.length === 1 && removed[0].name === 'citiess';
|
||||
}
|
||||
|
||||
handle(added: T[], removed: T[]): { created: T[]; deleted: T[]; renamed: { from: T; to: T }[] } {
|
||||
return { created: added, deleted: removed, renamed: [] };
|
||||
}
|
||||
}
|
||||
class Case2<T extends Named> implements TablesHandler<T> {
|
||||
// authOtp, deleted, users -> authOtp renamed, cities added, deleted deleted
|
||||
can(_: T[], removed: T[]): boolean {
|
||||
return removed.length === 3 && removed[0].name === 'auth_otp';
|
||||
}
|
||||
|
||||
handle(added: T[], removed: T[]): { created: T[]; deleted: T[]; renamed: { from: T; to: T }[] } {
|
||||
return { created: added.slice(1), deleted: removed.slice(1), renamed: [{ from: removed[0], to: added[0] }] };
|
||||
}
|
||||
}
|
||||
|
||||
type Named = { name: string };
|
||||
|
||||
const handlers: TablesHandler<any>[] = [];
|
||||
handlers.push(new Case1());
|
||||
handlers.push(new Case2());
|
||||
handlers.push(new DryRun());
|
||||
|
||||
export const resolveTables = <T extends Named>(added: T[], removed: T[]) => {
|
||||
const handler = handlers.filter((it) => {
|
||||
return it.can(added, removed);
|
||||
}).exactlyOne();
|
||||
|
||||
if (!handler) {
|
||||
console.log('added', added.map((it) => it.name).join());
|
||||
console.log('removed', removed.map((it) => it.name).join());
|
||||
throw new Error('No handler');
|
||||
}
|
||||
|
||||
console.log(`Simluated by ${handler.constructor.name}`);
|
||||
return handler.handle(added, removed);
|
||||
};
|
||||
class LehaColumnsHandler<T extends Named> implements ColumnsHandler<T> {
|
||||
can(tableName: string, _: T[], __: T[]): boolean {
|
||||
return tableName === 'users';
|
||||
}
|
||||
|
||||
handle(
|
||||
tableName: string,
|
||||
added: T[],
|
||||
removed: T[],
|
||||
): { tableName: string; created: T[]; deleted: T[]; renamed: { from: T; to: T }[] } {
|
||||
return { tableName, created: [], deleted: [], renamed: [{ from: removed[0], to: added[0] }] };
|
||||
}
|
||||
}
|
||||
|
||||
class DryRunColumnsHandler<T extends Named> implements ColumnsHandler<T> {
|
||||
can(tableName: string, _: T[], __: T[]): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
handle(
|
||||
tableName: string,
|
||||
added: T[],
|
||||
removed: T[],
|
||||
): { tableName: string; created: T[]; deleted: T[]; renamed: { from: T; to: T }[] } {
|
||||
return { tableName, created: added, deleted: removed, renamed: [] };
|
||||
}
|
||||
}
|
||||
|
||||
class V1V2AuthOtpColumnsHandler<T extends Named> implements ColumnsHandler<T> {
|
||||
can(tableName: string, _: T[], __: T[]): boolean {
|
||||
return tableName === 'auth_otp';
|
||||
}
|
||||
|
||||
handle(
|
||||
tableName: string,
|
||||
added: T[],
|
||||
removed: T[],
|
||||
): { tableName: string; created: T[]; deleted: T[]; renamed: { from: T; to: T }[] } {
|
||||
const phonePrev = removed.filter((it) => it.name === 'phone')[0];
|
||||
const phoneNew = added.filter((it) => it.name === 'phone1')[0];
|
||||
|
||||
const newAdded = added.filter((it) => it.name !== 'phone1');
|
||||
const newRemoved = removed.filter((it) => it.name !== 'phone');
|
||||
|
||||
return { tableName, created: newAdded, deleted: newRemoved, renamed: [{ from: phonePrev, to: phoneNew }] };
|
||||
}
|
||||
|
||||
// handle(tableName:string, added: T[], _: T[]): { created: T[]; deleted: T[]; renamed: { from: T; to: T; }[]; } {
|
||||
// return { created: added, deleted: [], renamed: [] }
|
||||
// }
|
||||
}
|
||||
|
||||
const columnsHandlers: ColumnsHandler<any>[] = [];
|
||||
columnsHandlers.push(new V1V2AuthOtpColumnsHandler());
|
||||
columnsHandlers.push(new LehaColumnsHandler());
|
||||
columnsHandlers.push(new DryRunColumnsHandler());
|
||||
|
||||
export const resolveColumns = <T extends Named>(tableName: string, added: T[], removed: T[]) => {
|
||||
const handler = columnsHandlers.filter((it) => {
|
||||
return it.can(tableName, added, removed);
|
||||
})[0];
|
||||
|
||||
if (!handler) {
|
||||
console.log('added', added.map((it) => it.name).join());
|
||||
console.log('removed', removed.map((it) => it.name).join());
|
||||
throw new Error('No columns handler for table: ' + tableName);
|
||||
}
|
||||
|
||||
console.log(`${tableName} columns simluated by ${handler.constructor.name}`);
|
||||
return handler.handle(tableName, added, removed);
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,597 @@
|
||||
import {
|
||||
JsonCreateIndexStatement,
|
||||
JsonRecreateTableStatement,
|
||||
JsonStatement,
|
||||
prepareCreateIndexesJson,
|
||||
} from './jsonStatements';
|
||||
import { SingleStoreSchemaSquashed } from './serializer/singlestoreSchema';
|
||||
import { SQLiteSchemaSquashed, SQLiteSquasher } from './serializer/sqliteSchema';
|
||||
|
||||
export const prepareLibSQLRecreateTable = (
|
||||
table: SQLiteSchemaSquashed['tables'][keyof SQLiteSchemaSquashed['tables']],
|
||||
action?: 'push',
|
||||
): (JsonRecreateTableStatement | JsonCreateIndexStatement)[] => {
|
||||
const { name, columns, uniqueConstraints, indexes, checkConstraints } = table;
|
||||
|
||||
const composites: string[][] = Object.values(table.compositePrimaryKeys).map(
|
||||
(it) => SQLiteSquasher.unsquashPK(it),
|
||||
);
|
||||
|
||||
const references: string[] = Object.values(table.foreignKeys);
|
||||
const fks = references.map((it) =>
|
||||
action === 'push' ? SQLiteSquasher.unsquashPushFK(it) : SQLiteSquasher.unsquashFK(it)
|
||||
);
|
||||
|
||||
const statements: (JsonRecreateTableStatement | JsonCreateIndexStatement)[] = [
|
||||
{
|
||||
type: 'recreate_table',
|
||||
tableName: name,
|
||||
columns: Object.values(columns),
|
||||
compositePKs: composites,
|
||||
referenceData: fks,
|
||||
uniqueConstraints: Object.values(uniqueConstraints),
|
||||
checkConstraints: Object.values(checkConstraints),
|
||||
},
|
||||
];
|
||||
|
||||
if (Object.keys(indexes).length) {
|
||||
statements.push(...prepareCreateIndexesJson(name, '', indexes));
|
||||
}
|
||||
return statements;
|
||||
};
|
||||
|
||||
export const prepareSQLiteRecreateTable = (
|
||||
table: SQLiteSchemaSquashed['tables'][keyof SQLiteSchemaSquashed['tables']],
|
||||
action?: 'push',
|
||||
): JsonStatement[] => {
|
||||
const { name, columns, uniqueConstraints, indexes, checkConstraints } = table;
|
||||
|
||||
const composites: string[][] = Object.values(table.compositePrimaryKeys).map(
|
||||
(it) => SQLiteSquasher.unsquashPK(it),
|
||||
);
|
||||
|
||||
const references: string[] = Object.values(table.foreignKeys);
|
||||
const fks = references.map((it) =>
|
||||
action === 'push' ? SQLiteSquasher.unsquashPushFK(it) : SQLiteSquasher.unsquashFK(it)
|
||||
);
|
||||
|
||||
const statements: JsonStatement[] = [
|
||||
{
|
||||
type: 'recreate_table',
|
||||
tableName: name,
|
||||
columns: Object.values(columns),
|
||||
compositePKs: composites,
|
||||
referenceData: fks,
|
||||
uniqueConstraints: Object.values(uniqueConstraints),
|
||||
checkConstraints: Object.values(checkConstraints),
|
||||
},
|
||||
];
|
||||
|
||||
if (Object.keys(indexes).length) {
|
||||
statements.push(...prepareCreateIndexesJson(name, '', indexes));
|
||||
}
|
||||
return statements;
|
||||
};
|
||||
|
||||
export const libSQLCombineStatements = (
|
||||
statements: JsonStatement[],
|
||||
json2: SQLiteSchemaSquashed,
|
||||
action?: 'push',
|
||||
) => {
|
||||
// const tablesContext: Record<string, string[]> = {};
|
||||
const newStatements: Record<string, JsonStatement[]> = {};
|
||||
for (const statement of statements) {
|
||||
if (
|
||||
statement.type === 'alter_table_alter_column_drop_autoincrement'
|
||||
|| statement.type === 'alter_table_alter_column_set_autoincrement'
|
||||
|| statement.type === 'alter_table_alter_column_drop_pk'
|
||||
|| statement.type === 'alter_table_alter_column_set_pk'
|
||||
|| statement.type === 'create_composite_pk'
|
||||
|| statement.type === 'alter_composite_pk'
|
||||
|| statement.type === 'delete_composite_pk'
|
||||
|| statement.type === 'create_check_constraint'
|
||||
|| statement.type === 'delete_check_constraint'
|
||||
) {
|
||||
const tableName = statement.tableName;
|
||||
|
||||
const statementsForTable = newStatements[tableName];
|
||||
|
||||
if (!statementsForTable) {
|
||||
newStatements[tableName] = prepareLibSQLRecreateTable(json2.tables[tableName], action);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!statementsForTable.some(({ type }) => type === 'recreate_table')) {
|
||||
const wasRename = statementsForTable.some(({ type }) => type === 'rename_table');
|
||||
const preparedStatements = prepareLibSQLRecreateTable(json2.tables[tableName], action);
|
||||
|
||||
if (wasRename) {
|
||||
newStatements[tableName].push(...preparedStatements);
|
||||
} else {
|
||||
newStatements[tableName] = preparedStatements;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
statement.type === 'alter_table_alter_column_set_type'
|
||||
|| statement.type === 'alter_table_alter_column_drop_notnull'
|
||||
|| statement.type === 'alter_table_alter_column_set_notnull'
|
||||
|| statement.type === 'alter_table_alter_column_set_default'
|
||||
|| statement.type === 'alter_table_alter_column_drop_default'
|
||||
) {
|
||||
const { tableName, columnName, columnPk } = statement;
|
||||
|
||||
const columnIsPartOfForeignKey = Object.values(
|
||||
json2.tables[tableName].foreignKeys,
|
||||
).some((it) => {
|
||||
const unsquashFk = action === 'push' ? SQLiteSquasher.unsquashPushFK(it) : SQLiteSquasher.unsquashFK(it);
|
||||
|
||||
return (
|
||||
unsquashFk.columnsFrom.includes(columnName)
|
||||
);
|
||||
});
|
||||
|
||||
const statementsForTable = newStatements[tableName];
|
||||
|
||||
if (
|
||||
!statementsForTable && (columnIsPartOfForeignKey || columnPk)
|
||||
) {
|
||||
newStatements[tableName] = prepareLibSQLRecreateTable(json2.tables[tableName], action);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
statementsForTable && (columnIsPartOfForeignKey || columnPk)
|
||||
) {
|
||||
if (!statementsForTable.some(({ type }) => type === 'recreate_table')) {
|
||||
const wasRename = statementsForTable.some(({ type }) => type === 'rename_table');
|
||||
const preparedStatements = prepareLibSQLRecreateTable(json2.tables[tableName], action);
|
||||
|
||||
if (wasRename) {
|
||||
newStatements[tableName].push(...preparedStatements);
|
||||
} else {
|
||||
newStatements[tableName] = preparedStatements;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
statementsForTable && !(columnIsPartOfForeignKey || columnPk)
|
||||
) {
|
||||
if (!statementsForTable.some(({ type }) => type === 'recreate_table')) {
|
||||
newStatements[tableName].push(statement);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
newStatements[tableName] = [statement];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (statement.type === 'create_reference') {
|
||||
const tableName = statement.tableName;
|
||||
|
||||
const data = action === 'push'
|
||||
? SQLiteSquasher.unsquashPushFK(statement.data)
|
||||
: SQLiteSquasher.unsquashFK(statement.data);
|
||||
|
||||
const statementsForTable = newStatements[tableName];
|
||||
|
||||
if (!statementsForTable) {
|
||||
newStatements[tableName] = statement.isMulticolumn
|
||||
? prepareLibSQLRecreateTable(json2.tables[tableName], action)
|
||||
: [statement];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// if add column with reference -> skip create_reference statement
|
||||
if (
|
||||
!statement.isMulticolumn
|
||||
&& statementsForTable.some((st) =>
|
||||
st.type === 'sqlite_alter_table_add_column' && st.column.name === data.columnsFrom[0]
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (statement.isMulticolumn) {
|
||||
if (!statementsForTable.some(({ type }) => type === 'recreate_table')) {
|
||||
const wasRename = statementsForTable.some(({ type }) => type === 'rename_table');
|
||||
const preparedStatements = prepareLibSQLRecreateTable(json2.tables[tableName], action);
|
||||
|
||||
if (wasRename) {
|
||||
newStatements[tableName].push(...preparedStatements);
|
||||
} else {
|
||||
newStatements[tableName] = preparedStatements;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!statementsForTable.some(({ type }) => type === 'recreate_table')) {
|
||||
newStatements[tableName].push(statement);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (statement.type === 'delete_reference') {
|
||||
const tableName = statement.tableName;
|
||||
|
||||
const statementsForTable = newStatements[tableName];
|
||||
|
||||
if (!statementsForTable) {
|
||||
newStatements[tableName] = prepareLibSQLRecreateTable(json2.tables[tableName], action);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!statementsForTable.some(({ type }) => type === 'recreate_table')) {
|
||||
const wasRename = statementsForTable.some(({ type }) => type === 'rename_table');
|
||||
const preparedStatements = prepareLibSQLRecreateTable(json2.tables[tableName], action);
|
||||
|
||||
if (wasRename) {
|
||||
newStatements[tableName].push(...preparedStatements);
|
||||
} else {
|
||||
newStatements[tableName] = preparedStatements;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (statement.type === 'sqlite_alter_table_add_column' && statement.column.primaryKey) {
|
||||
const tableName = statement.tableName;
|
||||
|
||||
const statementsForTable = newStatements[tableName];
|
||||
|
||||
if (!statementsForTable) {
|
||||
newStatements[tableName] = prepareLibSQLRecreateTable(json2.tables[tableName], action);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!statementsForTable.some(({ type }) => type === 'recreate_table')) {
|
||||
const wasRename = statementsForTable.some(({ type }) => type === 'rename_table');
|
||||
const preparedStatements = prepareLibSQLRecreateTable(json2.tables[tableName], action);
|
||||
|
||||
if (wasRename) {
|
||||
newStatements[tableName].push(...preparedStatements);
|
||||
} else {
|
||||
newStatements[tableName] = preparedStatements;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const tableName = statement.type === 'rename_table'
|
||||
? statement.tableNameTo
|
||||
: (statement as { tableName: string }).tableName;
|
||||
const statementsForTable = newStatements[tableName];
|
||||
|
||||
if (!statementsForTable) {
|
||||
newStatements[tableName] = [statement];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!statementsForTable.some(({ type }) => type === 'recreate_table')) {
|
||||
newStatements[tableName].push(statement);
|
||||
}
|
||||
}
|
||||
|
||||
const combinedStatements = Object.values(newStatements).flat();
|
||||
const renamedTables = combinedStatements.filter((it) => it.type === 'rename_table');
|
||||
const renamedColumns = combinedStatements.filter((it) => it.type === 'alter_table_rename_column');
|
||||
|
||||
const rest = combinedStatements.filter((it) => it.type !== 'rename_table' && it.type !== 'alter_table_rename_column');
|
||||
|
||||
return [...renamedTables, ...renamedColumns, ...rest];
|
||||
};
|
||||
|
||||
export const sqliteCombineStatements = (
|
||||
statements: JsonStatement[],
|
||||
json2: SQLiteSchemaSquashed,
|
||||
action?: 'push',
|
||||
) => {
|
||||
// const tablesContext: Record<string, string[]> = {};
|
||||
const newStatements: Record<string, JsonStatement[]> = {};
|
||||
for (const statement of statements) {
|
||||
if (
|
||||
statement.type === 'alter_table_alter_column_set_type'
|
||||
|| statement.type === 'alter_table_alter_column_set_default'
|
||||
|| statement.type === 'alter_table_alter_column_drop_default'
|
||||
|| statement.type === 'alter_table_alter_column_set_notnull'
|
||||
|| statement.type === 'alter_table_alter_column_drop_notnull'
|
||||
|| statement.type === 'alter_table_alter_column_drop_autoincrement'
|
||||
|| statement.type === 'alter_table_alter_column_set_autoincrement'
|
||||
|| statement.type === 'alter_table_alter_column_drop_pk'
|
||||
|| statement.type === 'alter_table_alter_column_set_pk'
|
||||
|| statement.type === 'delete_reference'
|
||||
|| statement.type === 'alter_reference'
|
||||
|| statement.type === 'create_composite_pk'
|
||||
|| statement.type === 'alter_composite_pk'
|
||||
|| statement.type === 'delete_composite_pk'
|
||||
|| statement.type === 'create_unique_constraint'
|
||||
|| statement.type === 'delete_unique_constraint'
|
||||
|| statement.type === 'create_check_constraint'
|
||||
|| statement.type === 'delete_check_constraint'
|
||||
) {
|
||||
const tableName = statement.tableName;
|
||||
|
||||
const statementsForTable = newStatements[tableName];
|
||||
|
||||
if (!statementsForTable) {
|
||||
newStatements[tableName] = prepareSQLiteRecreateTable(json2.tables[tableName], action);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!statementsForTable.some(({ type }) => type === 'recreate_table')) {
|
||||
const wasRename = statementsForTable.some(({ type }) => type === 'rename_table');
|
||||
const preparedStatements = prepareSQLiteRecreateTable(json2.tables[tableName], action);
|
||||
|
||||
if (wasRename) {
|
||||
newStatements[tableName].push(...preparedStatements);
|
||||
} else {
|
||||
newStatements[tableName] = preparedStatements;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (statement.type === 'sqlite_alter_table_add_column' && statement.column.primaryKey) {
|
||||
const tableName = statement.tableName;
|
||||
|
||||
const statementsForTable = newStatements[tableName];
|
||||
|
||||
if (!statementsForTable) {
|
||||
newStatements[tableName] = prepareSQLiteRecreateTable(json2.tables[tableName], action);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!statementsForTable.some(({ type }) => type === 'recreate_table')) {
|
||||
const wasRename = statementsForTable.some(({ type }) => type === 'rename_table');
|
||||
const preparedStatements = prepareSQLiteRecreateTable(json2.tables[tableName], action);
|
||||
|
||||
if (wasRename) {
|
||||
newStatements[tableName].push(...preparedStatements);
|
||||
} else {
|
||||
newStatements[tableName] = preparedStatements;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (statement.type === 'create_reference') {
|
||||
const tableName = statement.tableName;
|
||||
|
||||
const data = action === 'push'
|
||||
? SQLiteSquasher.unsquashPushFK(statement.data)
|
||||
: SQLiteSquasher.unsquashFK(statement.data);
|
||||
const statementsForTable = newStatements[tableName];
|
||||
|
||||
if (!statementsForTable) {
|
||||
newStatements[tableName] = prepareSQLiteRecreateTable(json2.tables[tableName], action);
|
||||
continue;
|
||||
}
|
||||
|
||||
// if add column with reference -> skip create_reference statement
|
||||
if (
|
||||
data.columnsFrom.length === 1
|
||||
&& statementsForTable.some((st) =>
|
||||
st.type === 'sqlite_alter_table_add_column' && st.column.name === data.columnsFrom[0]
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!statementsForTable.some(({ type }) => type === 'recreate_table')) {
|
||||
const wasRename = statementsForTable.some(({ type }) => type === 'rename_table');
|
||||
const preparedStatements = prepareSQLiteRecreateTable(json2.tables[tableName], action);
|
||||
|
||||
if (wasRename) {
|
||||
newStatements[tableName].push(...preparedStatements);
|
||||
} else {
|
||||
newStatements[tableName] = preparedStatements;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const tableName = statement.type === 'rename_table'
|
||||
? statement.tableNameTo
|
||||
: (statement as { tableName: string }).tableName;
|
||||
|
||||
const statementsForTable = newStatements[tableName];
|
||||
|
||||
if (!statementsForTable) {
|
||||
newStatements[tableName] = [statement];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!statementsForTable.some(({ type }) => type === 'recreate_table')) {
|
||||
newStatements[tableName].push(statement);
|
||||
}
|
||||
}
|
||||
|
||||
const combinedStatements = Object.values(newStatements).flat();
|
||||
|
||||
const renamedTables = combinedStatements.filter((it) => it.type === 'rename_table');
|
||||
const renamedColumns = combinedStatements.filter((it) => it.type === 'alter_table_rename_column');
|
||||
|
||||
const rest = combinedStatements.filter((it) => it.type !== 'rename_table' && it.type !== 'alter_table_rename_column');
|
||||
|
||||
return [...renamedTables, ...renamedColumns, ...rest];
|
||||
};
|
||||
|
||||
export const prepareSingleStoreRecreateTable = (
|
||||
table: SingleStoreSchemaSquashed['tables'][keyof SingleStoreSchemaSquashed['tables']],
|
||||
): JsonStatement[] => {
|
||||
const { name, columns, uniqueConstraints, indexes, compositePrimaryKeys } = table;
|
||||
|
||||
const composites: string[] = Object.values(compositePrimaryKeys);
|
||||
|
||||
const statements: JsonStatement[] = [
|
||||
{
|
||||
type: 'singlestore_recreate_table',
|
||||
tableName: name,
|
||||
columns: Object.values(columns),
|
||||
compositePKs: composites,
|
||||
uniqueConstraints: Object.values(uniqueConstraints),
|
||||
},
|
||||
];
|
||||
|
||||
if (Object.keys(indexes).length) {
|
||||
statements.push(...prepareCreateIndexesJson(name, '', indexes));
|
||||
}
|
||||
return statements;
|
||||
};
|
||||
|
||||
export const singleStoreCombineStatements = (
|
||||
statements: JsonStatement[],
|
||||
json2: SingleStoreSchemaSquashed,
|
||||
) => {
|
||||
const newStatements: Record<string, JsonStatement[]> = {};
|
||||
|
||||
for (const statement of statements) {
|
||||
if (
|
||||
statement.type === 'alter_table_alter_column_set_type'
|
||||
|| statement.type === 'alter_table_alter_column_set_notnull'
|
||||
|| statement.type === 'alter_table_alter_column_drop_notnull'
|
||||
|| statement.type === 'alter_table_alter_column_drop_autoincrement'
|
||||
|| statement.type === 'alter_table_alter_column_set_autoincrement'
|
||||
|| statement.type === 'alter_table_alter_column_drop_pk'
|
||||
|| statement.type === 'alter_table_alter_column_set_pk'
|
||||
|| statement.type === 'create_composite_pk'
|
||||
|| statement.type === 'alter_composite_pk'
|
||||
|| statement.type === 'delete_composite_pk'
|
||||
) {
|
||||
const tableName = statement.tableName;
|
||||
|
||||
const statementsForTable = newStatements[tableName];
|
||||
|
||||
if (!statementsForTable) {
|
||||
newStatements[tableName] = prepareSingleStoreRecreateTable(json2.tables[tableName]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!statementsForTable.some(({ type }) => type === 'recreate_table')) {
|
||||
const wasRename = statementsForTable.some(({ type }) =>
|
||||
type === 'rename_table' || type === 'alter_table_rename_column'
|
||||
);
|
||||
const preparedStatements = prepareSingleStoreRecreateTable(json2.tables[tableName]);
|
||||
|
||||
if (wasRename) {
|
||||
newStatements[tableName].push(...preparedStatements);
|
||||
} else {
|
||||
newStatements[tableName] = preparedStatements;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
(statement.type === 'alter_table_alter_column_drop_default'
|
||||
|| statement.type === 'alter_table_alter_column_set_default') && statement.columnNotNull
|
||||
) {
|
||||
const tableName = statement.tableName;
|
||||
|
||||
const statementsForTable = newStatements[tableName];
|
||||
|
||||
if (!statementsForTable) {
|
||||
newStatements[tableName] = prepareSingleStoreRecreateTable(json2.tables[tableName]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!statementsForTable.some(({ type }) => type === 'recreate_table')) {
|
||||
const wasRename = statementsForTable.some(({ type }) => type === 'rename_table');
|
||||
const preparedStatements = prepareSingleStoreRecreateTable(json2.tables[tableName]);
|
||||
|
||||
if (wasRename) {
|
||||
newStatements[tableName].push(...preparedStatements);
|
||||
} else {
|
||||
newStatements[tableName] = preparedStatements;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (statement.type === 'alter_table_add_column' && statement.column.primaryKey) {
|
||||
const tableName = statement.tableName;
|
||||
|
||||
const statementsForTable = newStatements[tableName];
|
||||
|
||||
if (!statementsForTable) {
|
||||
newStatements[tableName] = prepareSingleStoreRecreateTable(json2.tables[tableName]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!statementsForTable.some(({ type }) => type === 'recreate_table')) {
|
||||
const wasRename = statementsForTable.some(({ type }) => type === 'rename_table');
|
||||
const preparedStatements = prepareSingleStoreRecreateTable(json2.tables[tableName]);
|
||||
|
||||
if (wasRename) {
|
||||
newStatements[tableName].push(...preparedStatements);
|
||||
} else {
|
||||
newStatements[tableName] = preparedStatements;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const tableName = statement.type === 'rename_table'
|
||||
? statement.tableNameTo
|
||||
: (statement as { tableName: string }).tableName;
|
||||
|
||||
const statementsForTable = newStatements[tableName];
|
||||
|
||||
if (!statementsForTable) {
|
||||
newStatements[tableName] = [statement];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!statementsForTable.some(({ type }) => type === 'singlestore_recreate_table')) {
|
||||
newStatements[tableName].push(statement);
|
||||
}
|
||||
}
|
||||
|
||||
const combinedStatements = Object.values(newStatements).flat();
|
||||
|
||||
const renamedTables = combinedStatements.filter((it) => it.type === 'rename_table');
|
||||
const renamedColumns = combinedStatements.filter((it) => it.type === 'alter_table_rename_column');
|
||||
|
||||
const rest = combinedStatements.filter((it) => it.type !== 'rename_table' && it.type !== 'alter_table_rename_column');
|
||||
|
||||
return [...renamedTables, ...renamedColumns, ...rest];
|
||||
};
|
||||
@@ -0,0 +1,371 @@
|
||||
import type { RunResult } from 'better-sqlite3';
|
||||
import chalk from 'chalk';
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { parse } from 'url';
|
||||
import type { NamedWithSchema } from './cli/commands/migrate';
|
||||
import { info } from './cli/views';
|
||||
import { assertUnreachable, snapshotVersion } from './global';
|
||||
import type { Dialect } from './schemaValidator';
|
||||
import { backwardCompatibleGelSchema } from './serializer/gelSchema';
|
||||
import { backwardCompatibleMysqlSchema } from './serializer/mysqlSchema';
|
||||
import { backwardCompatiblePgSchema } from './serializer/pgSchema';
|
||||
import { backwardCompatibleSingleStoreSchema } from './serializer/singlestoreSchema';
|
||||
import { backwardCompatibleSqliteSchema } from './serializer/sqliteSchema';
|
||||
import type { ProxyParams } from './serializer/studio';
|
||||
|
||||
export type Proxy = (params: ProxyParams) => Promise<any[]>;
|
||||
|
||||
export type TransactionProxy = (queries: { sql: string; method?: ProxyParams['method'] }[]) => Promise<any[]>;
|
||||
|
||||
export type DB = {
|
||||
query: <T extends any = any>(sql: string, params?: any[]) => Promise<T[]>;
|
||||
};
|
||||
|
||||
export type SQLiteDB = {
|
||||
query: <T extends any = any>(sql: string, params?: any[]) => Promise<T[]>;
|
||||
run(query: string): Promise<void>;
|
||||
};
|
||||
|
||||
export type LibSQLDB = {
|
||||
query: <T extends any = any>(sql: string, params?: any[]) => Promise<T[]>;
|
||||
run(query: string): Promise<void>;
|
||||
batchWithPragma?(queries: string[]): Promise<void>;
|
||||
};
|
||||
|
||||
export const copy = <T>(it: T): T => {
|
||||
return JSON.parse(JSON.stringify(it));
|
||||
};
|
||||
|
||||
export const objectValues = <T extends object>(obj: T): Array<T[keyof T]> => {
|
||||
return Object.values(obj);
|
||||
};
|
||||
|
||||
export const assertV1OutFolder = (out: string) => {
|
||||
if (!existsSync(out)) return;
|
||||
|
||||
const oldMigrationFolders = readdirSync(out).filter(
|
||||
(it) => it.length === 14 && /^\d+$/.test(it),
|
||||
);
|
||||
|
||||
if (oldMigrationFolders.length > 0) {
|
||||
console.log(
|
||||
`Your migrations folder format is outdated, please run ${
|
||||
chalk.green.bold(
|
||||
`drizzle-kit up`,
|
||||
)
|
||||
}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
export type Journal = {
|
||||
version: string;
|
||||
dialect: Dialect;
|
||||
entries: {
|
||||
idx: number;
|
||||
version: string;
|
||||
when: number;
|
||||
tag: string;
|
||||
breakpoints: boolean;
|
||||
}[];
|
||||
};
|
||||
|
||||
export const dryJournal = (dialect: Dialect): Journal => {
|
||||
return {
|
||||
version: snapshotVersion,
|
||||
dialect,
|
||||
entries: [],
|
||||
};
|
||||
};
|
||||
|
||||
// export const preparePushFolder = (dialect: Dialect) => {
|
||||
// const out = ".drizzle";
|
||||
// let snapshot: string = "";
|
||||
// if (!existsSync(join(out))) {
|
||||
// mkdirSync(out);
|
||||
// snapshot = JSON.stringify(dryJournal(dialect));
|
||||
// } else {
|
||||
// snapshot = readdirSync(out)[0];
|
||||
// }
|
||||
|
||||
// return { snapshot };
|
||||
// };
|
||||
|
||||
export const prepareOutFolder = (out: string, dialect: Dialect) => {
|
||||
const meta = join(out, 'meta');
|
||||
const journalPath = join(meta, '_journal.json');
|
||||
|
||||
if (!existsSync(join(out, 'meta'))) {
|
||||
mkdirSync(meta, { recursive: true });
|
||||
writeFileSync(journalPath, JSON.stringify(dryJournal(dialect)));
|
||||
}
|
||||
|
||||
const journal = JSON.parse(readFileSync(journalPath).toString());
|
||||
|
||||
const snapshots = readdirSync(meta)
|
||||
.filter((it) => !it.startsWith('_'))
|
||||
.map((it) => join(meta, it));
|
||||
|
||||
snapshots.sort();
|
||||
return { meta, snapshots, journal };
|
||||
};
|
||||
|
||||
const validatorForDialect = (dialect: Dialect) => {
|
||||
switch (dialect) {
|
||||
case 'postgresql':
|
||||
return { validator: backwardCompatiblePgSchema, version: 7 };
|
||||
case 'sqlite':
|
||||
return { validator: backwardCompatibleSqliteSchema, version: 6 };
|
||||
case 'turso':
|
||||
return { validator: backwardCompatibleSqliteSchema, version: 6 };
|
||||
case 'mysql':
|
||||
return { validator: backwardCompatibleMysqlSchema, version: 5 };
|
||||
case 'singlestore':
|
||||
return { validator: backwardCompatibleSingleStoreSchema, version: 1 };
|
||||
case 'gel':
|
||||
return { validator: backwardCompatibleGelSchema, version: 1 };
|
||||
}
|
||||
};
|
||||
|
||||
export const validateWithReport = (snapshots: string[], dialect: Dialect) => {
|
||||
// ✅ check if drizzle-kit can handle snapshot version
|
||||
// ✅ check if snapshot is of the last version
|
||||
// ✅ check if id of the snapshot is valid
|
||||
// ✅ collect {} of prev id -> snapshotName[], if there's more than one - tell about collision
|
||||
const { validator, version } = validatorForDialect(dialect);
|
||||
|
||||
const result = snapshots.reduce(
|
||||
(accum, it) => {
|
||||
const raw = JSON.parse(readFileSync(`./${it}`).toString());
|
||||
|
||||
accum.rawMap[it] = raw;
|
||||
|
||||
if (raw['version'] && Number(raw['version']) > version) {
|
||||
console.log(
|
||||
info(
|
||||
`${it} snapshot is of unsupported version, please update drizzle-kit`,
|
||||
),
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const result = validator.safeParse(raw);
|
||||
if (!result.success) {
|
||||
accum.malformed.push(it);
|
||||
return accum;
|
||||
}
|
||||
|
||||
const snapshot = result.data;
|
||||
if (snapshot.version !== String(version)) {
|
||||
accum.nonLatest.push(it);
|
||||
return accum;
|
||||
}
|
||||
|
||||
// only if latest version here
|
||||
const idEntry = accum.idsMap[snapshot['prevId']] ?? {
|
||||
parent: it,
|
||||
snapshots: [],
|
||||
};
|
||||
idEntry.snapshots.push(it);
|
||||
accum.idsMap[snapshot['prevId']] = idEntry;
|
||||
|
||||
return accum;
|
||||
},
|
||||
{
|
||||
malformed: [],
|
||||
nonLatest: [],
|
||||
idToNameMap: {},
|
||||
idsMap: {},
|
||||
rawMap: {},
|
||||
} as {
|
||||
malformed: string[];
|
||||
nonLatest: string[];
|
||||
idsMap: Record<string, { parent: string; snapshots: string[] }>;
|
||||
rawMap: Record<string, any>;
|
||||
},
|
||||
);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const prepareMigrationFolder = (
|
||||
outFolder: string = 'drizzle',
|
||||
dialect: Dialect,
|
||||
) => {
|
||||
const { snapshots, journal } = prepareOutFolder(outFolder, dialect);
|
||||
const report = validateWithReport(snapshots, dialect);
|
||||
if (report.nonLatest.length > 0) {
|
||||
console.log(
|
||||
report.nonLatest
|
||||
.map((it) => {
|
||||
return `${it}/snapshot.json is not of the latest version`;
|
||||
})
|
||||
.concat(`Run ${chalk.green.bold(`drizzle-kit up`)}`)
|
||||
.join('\n'),
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (report.malformed.length) {
|
||||
const message = report.malformed
|
||||
.map((it) => {
|
||||
return `${it} data is malformed`;
|
||||
})
|
||||
.join('\n');
|
||||
console.log(message);
|
||||
}
|
||||
|
||||
const collisionEntries = Object.entries(report.idsMap).filter(
|
||||
(it) => it[1].snapshots.length > 1,
|
||||
);
|
||||
|
||||
const message = collisionEntries
|
||||
.map((it) => {
|
||||
const data = it[1];
|
||||
return `[${
|
||||
data.snapshots.join(
|
||||
', ',
|
||||
)
|
||||
}] are pointing to a parent snapshot: ${data.parent}/snapshot.json which is a collision.`;
|
||||
})
|
||||
.join('\n')
|
||||
.trim();
|
||||
if (message) {
|
||||
console.log(chalk.red.bold('Error:'), message);
|
||||
}
|
||||
|
||||
const abort = report.malformed.length!! || collisionEntries.length > 0;
|
||||
|
||||
if (abort) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
return { snapshots, journal };
|
||||
};
|
||||
|
||||
export const prepareMigrationMeta = (
|
||||
schemas: { from: string; to: string }[],
|
||||
tables: { from: NamedWithSchema; to: NamedWithSchema }[],
|
||||
columns: {
|
||||
from: { table: string; schema: string; column: string };
|
||||
to: { table: string; schema: string; column: string };
|
||||
}[],
|
||||
) => {
|
||||
const _meta = {
|
||||
schemas: {} as Record<string, string>,
|
||||
tables: {} as Record<string, string>,
|
||||
columns: {} as Record<string, string>,
|
||||
};
|
||||
|
||||
schemas.forEach((it) => {
|
||||
const from = schemaRenameKey(it.from);
|
||||
const to = schemaRenameKey(it.to);
|
||||
_meta.schemas[from] = to;
|
||||
});
|
||||
tables.forEach((it) => {
|
||||
const from = tableRenameKey(it.from);
|
||||
const to = tableRenameKey(it.to);
|
||||
_meta.tables[from] = to;
|
||||
});
|
||||
|
||||
columns.forEach((it) => {
|
||||
const from = columnRenameKey(it.from.table, it.from.schema, it.from.column);
|
||||
const to = columnRenameKey(it.to.table, it.to.schema, it.to.column);
|
||||
_meta.columns[from] = to;
|
||||
});
|
||||
|
||||
return _meta;
|
||||
};
|
||||
|
||||
export const schemaRenameKey = (it: string) => {
|
||||
return it;
|
||||
};
|
||||
|
||||
export const tableRenameKey = (it: NamedWithSchema) => {
|
||||
const out = it.schema ? `"${it.schema}"."${it.name}"` : `"${it.name}"`;
|
||||
return out;
|
||||
};
|
||||
|
||||
export const columnRenameKey = (
|
||||
table: string,
|
||||
schema: string,
|
||||
column: string,
|
||||
) => {
|
||||
const out = schema
|
||||
? `"${schema}"."${table}"."${column}"`
|
||||
: `"${table}"."${column}"`;
|
||||
return out;
|
||||
};
|
||||
|
||||
export const kloudMeta = () => {
|
||||
return {
|
||||
pg: [5],
|
||||
mysql: [] as number[],
|
||||
sqlite: [] as number[],
|
||||
};
|
||||
};
|
||||
|
||||
export const normaliseSQLiteUrl = (
|
||||
it: string,
|
||||
type: 'libsql' | 'better-sqlite',
|
||||
) => {
|
||||
if (type === 'libsql') {
|
||||
if (it.startsWith('file:')) {
|
||||
return it;
|
||||
}
|
||||
try {
|
||||
const url = parse(it);
|
||||
if (url.protocol === null) {
|
||||
return `file:${it}`;
|
||||
}
|
||||
return it;
|
||||
} catch (e) {
|
||||
return `file:${it}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'better-sqlite') {
|
||||
if (it.startsWith('file:')) {
|
||||
return it.substring(5);
|
||||
}
|
||||
|
||||
return it;
|
||||
}
|
||||
|
||||
assertUnreachable(type);
|
||||
};
|
||||
|
||||
export const normalisePGliteUrl = (
|
||||
it: string,
|
||||
) => {
|
||||
if (it.startsWith('file:')) {
|
||||
return it.substring(5);
|
||||
}
|
||||
|
||||
return it;
|
||||
};
|
||||
|
||||
export function isPgArrayType(sqlType: string) {
|
||||
return sqlType.match(/.*\[\d*\].*|.*\[\].*/g) !== null;
|
||||
}
|
||||
|
||||
export function findAddedAndRemoved(columnNames1: string[], columnNames2: string[]) {
|
||||
const set1 = new Set(columnNames1);
|
||||
const set2 = new Set(columnNames2);
|
||||
|
||||
const addedColumns = columnNames2.filter((it) => !set1.has(it));
|
||||
const removedColumns = columnNames1.filter((it) => !set2.has(it));
|
||||
|
||||
return { addedColumns, removedColumns };
|
||||
}
|
||||
|
||||
export function escapeSingleQuotes(str: string) {
|
||||
return str.replace(/'/g, "''");
|
||||
}
|
||||
|
||||
export function unescapeSingleQuotes(str: string, ignoreFirstAndLastChar: boolean) {
|
||||
const regex = ignoreFirstAndLastChar ? /(?<!^)'(?!$)/g : /'/g;
|
||||
return str.replace(/''/g, "'").replace(regex, "\\'");
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import envPaths from 'env-paths';
|
||||
import { mkdirSync } from 'fs';
|
||||
import { access, readFile } from 'fs/promises';
|
||||
import { exec, ExecOptions } from 'node:child_process';
|
||||
import { join } from 'path';
|
||||
|
||||
export function runCommand(command: string, options: ExecOptions = {}) {
|
||||
return new Promise<{ exitCode: number }>((resolve) => {
|
||||
exec(command, options, (error) => {
|
||||
return resolve({ exitCode: error?.code ?? 0 });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export const certs = async () => {
|
||||
const res = await runCommand('mkcert --help');
|
||||
|
||||
if (res.exitCode === 0) {
|
||||
const p = envPaths('drizzle-studio', {
|
||||
suffix: '',
|
||||
});
|
||||
|
||||
// create ~/.local/share/drizzle-studio
|
||||
mkdirSync(p.data, { recursive: true });
|
||||
|
||||
// ~/.local/share/drizzle-studio
|
||||
const keyPath = join(p.data, 'localhost-key.pem');
|
||||
const certPath = join(p.data, 'localhost.pem');
|
||||
|
||||
try {
|
||||
// check if the files exist
|
||||
await Promise.all([access(keyPath), access(certPath)]);
|
||||
} catch (e) {
|
||||
// if not create them
|
||||
await runCommand(`mkcert localhost`, { cwd: p.data });
|
||||
}
|
||||
const [key, cert] = await Promise.all([
|
||||
readFile(keyPath, { encoding: 'utf-8' }),
|
||||
readFile(certPath, { encoding: 'utf-8' }),
|
||||
]);
|
||||
return key && cert ? { key, cert } : null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,58 @@
|
||||
import chalk from 'chalk';
|
||||
import { assert, test } from 'vitest';
|
||||
import { analyzeImports, ChainLink } from '../imports-checker/checker';
|
||||
|
||||
test('imports-issues', () => {
|
||||
const issues = analyzeImports({
|
||||
basePath: '.',
|
||||
localPaths: ['src'],
|
||||
whiteList: [
|
||||
'@drizzle-team/brocli',
|
||||
'json-diff',
|
||||
'path',
|
||||
'fs',
|
||||
'fs/*',
|
||||
'url',
|
||||
'zod',
|
||||
'node:*',
|
||||
'hono',
|
||||
'glob',
|
||||
'hono/*',
|
||||
'hono/**/*',
|
||||
'@hono/*',
|
||||
'crypto',
|
||||
'hanji',
|
||||
'chalk',
|
||||
'dotenv/config',
|
||||
'camelcase',
|
||||
'semver',
|
||||
'env-paths',
|
||||
],
|
||||
entry: 'src/cli/index.ts',
|
||||
logger: true,
|
||||
ignoreTypes: true,
|
||||
}).issues;
|
||||
|
||||
const chainToString = (chains: ChainLink[]) => {
|
||||
if (chains.length === 0) throw new Error();
|
||||
|
||||
let out = chains[0]!.file + '\n';
|
||||
let indentation = 0;
|
||||
for (let chain of chains) {
|
||||
out += ' '.repeat(indentation)
|
||||
+ '└'
|
||||
+ chain.import
|
||||
+ ` ${chalk.gray(chain.file)}\n`;
|
||||
indentation += 1;
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
console.log();
|
||||
for (const issue of issues) {
|
||||
console.log(chalk.red(issue.imports.map((it) => it.name).join('\n')));
|
||||
console.log(issue.accessChains.map((it) => chainToString(it)).join('\n'));
|
||||
}
|
||||
|
||||
assert.equal(issues.length, 0);
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import { test as brotest } from '@drizzle-team/brocli';
|
||||
import { assert, expect, test } from 'vitest';
|
||||
import { exportRaw } from '../src/cli/schema';
|
||||
|
||||
// good:
|
||||
// #1 drizzle-kit export --dialect=postgresql --schema=schema.ts
|
||||
// #3 drizzle-kit export
|
||||
// #3 drizzle-kit export --config=drizzle1.config.ts
|
||||
|
||||
// errors:
|
||||
// #1 drizzle-kit export --schema=src/schema.ts
|
||||
// #2 drizzle-kit export --dialect=postgresql
|
||||
// #3 drizzle-kit export --dialect=postgresql2
|
||||
// #4 drizzle-kit export --config=drizzle.config.ts --schema=schema.ts
|
||||
// #5 drizzle-kit export --config=drizzle.config.ts --dialect=postgresql
|
||||
|
||||
test('export #1', async (t) => {
|
||||
const res = await brotest(
|
||||
exportRaw,
|
||||
'--dialect=postgresql --schema=schema.ts',
|
||||
);
|
||||
|
||||
if (res.type !== 'handler') assert.fail(res.type, 'handler');
|
||||
|
||||
expect(res.options).toStrictEqual({
|
||||
dialect: 'postgresql',
|
||||
schema: 'schema.ts',
|
||||
sql: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('export #2', async (t) => {
|
||||
const res = await brotest(exportRaw, '');
|
||||
|
||||
if (res.type !== 'handler') assert.fail(res.type, 'handler');
|
||||
expect(res.options).toStrictEqual({
|
||||
dialect: 'postgresql',
|
||||
schema: './schema.ts',
|
||||
sql: true,
|
||||
});
|
||||
});
|
||||
|
||||
// custom config path
|
||||
test('export #3', async (t) => {
|
||||
const res = await brotest(exportRaw, '--config=expo.config.ts');
|
||||
assert.equal(res.type, 'handler');
|
||||
if (res.type !== 'handler') assert.fail(res.type, 'handler');
|
||||
expect(res.options).toStrictEqual({
|
||||
dialect: 'sqlite',
|
||||
schema: './schema.ts',
|
||||
sql: true,
|
||||
});
|
||||
});
|
||||
|
||||
// --- errors ---
|
||||
test('err #1', async (t) => {
|
||||
const res = await brotest(exportRaw, '--schema=src/schema.ts');
|
||||
assert.equal(res.type, 'error');
|
||||
});
|
||||
|
||||
test('err #2', async (t) => {
|
||||
const res = await brotest(exportRaw, '--dialect=postgresql');
|
||||
assert.equal(res.type, 'error');
|
||||
});
|
||||
|
||||
test('err #3', async (t) => {
|
||||
const res = await brotest(exportRaw, '--dialect=postgresql2');
|
||||
assert.equal(res.type, 'error');
|
||||
});
|
||||
|
||||
test('err #4', async (t) => {
|
||||
const res = await brotest(exportRaw, '--config=drizzle.config.ts --schema=schema.ts');
|
||||
assert.equal(res.type, 'error');
|
||||
});
|
||||
|
||||
test('err #5', async (t) => {
|
||||
const res = await brotest(exportRaw, '--config=drizzle.config.ts --dialect=postgresql');
|
||||
assert.equal(res.type, 'error');
|
||||
});
|
||||
@@ -0,0 +1,259 @@
|
||||
import { test as brotest } from '@drizzle-team/brocli';
|
||||
import { assert, expect, test } from 'vitest';
|
||||
import { generate } from '../src/cli/schema';
|
||||
|
||||
// good:
|
||||
// #1 drizzle-kit generate --dialect=postgresql --schema=schema.ts
|
||||
// #2 drizzle-kit generate --dialect=postgresql --schema=schema.ts --out=out
|
||||
// #3 drizzle-kit generate
|
||||
// #4 drizzle-kit generate --custom
|
||||
// #5 drizzle-kit generate --name=custom
|
||||
// #6 drizzle-kit generate --prefix=timestamp
|
||||
// #7 drizzle-kit generate --prefix=timestamp --name=custom --custom
|
||||
// #8 drizzle-kit generate --config=drizzle1.config.ts
|
||||
// #9 drizzle-kit generate --dialect=postgresql --schema=schema.ts --out=out --prefix=timestamp --name=custom --custom
|
||||
|
||||
// errors:
|
||||
// #1 drizzle-kit generate --schema=src/schema.ts
|
||||
// #2 drizzle-kit generate --dialect=postgresql
|
||||
// #3 drizzle-kit generate --dialect=postgresql2
|
||||
// #4 drizzle-kit generate --driver=expo
|
||||
// #5 drizzle-kit generate --dialect=postgresql --out=out
|
||||
// #6 drizzle-kit generate --config=drizzle.config.ts --out=out
|
||||
// #7 drizzle-kit generate --config=drizzle.config.ts --schema=schema.ts
|
||||
// #8 drizzle-kit generate --config=drizzle.config.ts --dialect=postgresql
|
||||
|
||||
test('generate #1', async (t) => {
|
||||
const res = await brotest(
|
||||
generate,
|
||||
'--dialect=postgresql --schema=schema.ts',
|
||||
);
|
||||
if (res.type !== 'handler') assert.fail(res.type, 'handler');
|
||||
expect(res.options).toStrictEqual({
|
||||
dialect: 'postgresql',
|
||||
name: undefined,
|
||||
custom: false,
|
||||
prefix: 'index',
|
||||
breakpoints: true,
|
||||
schema: 'schema.ts',
|
||||
out: 'drizzle',
|
||||
bundle: false,
|
||||
casing: undefined,
|
||||
driver: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test('generate #2', async (t) => {
|
||||
const res = await brotest(
|
||||
generate,
|
||||
'--dialect=postgresql --schema=schema.ts --out=out',
|
||||
);
|
||||
|
||||
if (res.type !== 'handler') assert.fail(res.type, 'handler');
|
||||
expect(res.options).toStrictEqual({
|
||||
dialect: 'postgresql',
|
||||
name: undefined,
|
||||
custom: false,
|
||||
prefix: 'index',
|
||||
breakpoints: true,
|
||||
schema: 'schema.ts',
|
||||
out: 'out',
|
||||
bundle: false,
|
||||
casing: undefined,
|
||||
driver: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test('generate #3', async (t) => {
|
||||
const res = await brotest(generate, '');
|
||||
|
||||
if (res.type !== 'handler') assert.fail(res.type, 'handler');
|
||||
expect(res.options).toStrictEqual({
|
||||
dialect: 'postgresql',
|
||||
name: undefined,
|
||||
custom: false,
|
||||
prefix: 'index',
|
||||
breakpoints: true,
|
||||
schema: './schema.ts',
|
||||
out: 'drizzle',
|
||||
bundle: false,
|
||||
casing: undefined,
|
||||
driver: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
// config | pass through custom
|
||||
test('generate #4', async (t) => {
|
||||
const res = await brotest(generate, '--custom');
|
||||
|
||||
if (res.type !== 'handler') assert.fail(res.type, 'handler');
|
||||
expect(res.options).toStrictEqual({
|
||||
dialect: 'postgresql',
|
||||
name: undefined,
|
||||
custom: true,
|
||||
prefix: 'index',
|
||||
breakpoints: true,
|
||||
schema: './schema.ts',
|
||||
out: 'drizzle',
|
||||
bundle: false,
|
||||
casing: undefined,
|
||||
driver: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
// config | pass through name
|
||||
test('generate #5', async (t) => {
|
||||
const res = await brotest(generate, '--name=custom');
|
||||
if (res.type !== 'handler') assert.fail(res.type, 'handler');
|
||||
expect(res.options).toStrictEqual({
|
||||
dialect: 'postgresql',
|
||||
name: 'custom',
|
||||
custom: false,
|
||||
prefix: 'index',
|
||||
breakpoints: true,
|
||||
schema: './schema.ts',
|
||||
out: 'drizzle',
|
||||
bundle: false,
|
||||
casing: undefined,
|
||||
driver: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
// config | pass through prefix
|
||||
test('generate #6', async (t) => {
|
||||
const res = await brotest(generate, '--prefix=timestamp');
|
||||
if (res.type !== 'handler') assert.fail(res.type, 'handler');
|
||||
expect(res.options).toStrictEqual({
|
||||
dialect: 'postgresql',
|
||||
name: undefined,
|
||||
custom: false,
|
||||
prefix: 'timestamp',
|
||||
breakpoints: true,
|
||||
schema: './schema.ts',
|
||||
out: 'drizzle',
|
||||
bundle: false,
|
||||
casing: undefined,
|
||||
driver: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
// config | pass through name, prefix and custom
|
||||
test('generate #7', async (t) => {
|
||||
const res = await brotest(
|
||||
generate,
|
||||
'--prefix=timestamp --name=custom --custom',
|
||||
);
|
||||
if (res.type !== 'handler') assert.fail(res.type, 'handler');
|
||||
expect(res.options).toStrictEqual({
|
||||
dialect: 'postgresql',
|
||||
name: 'custom',
|
||||
custom: true,
|
||||
prefix: 'timestamp',
|
||||
breakpoints: true,
|
||||
schema: './schema.ts',
|
||||
out: 'drizzle',
|
||||
bundle: false,
|
||||
casing: undefined,
|
||||
driver: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
// custom config path
|
||||
test('generate #8', async (t) => {
|
||||
const res = await brotest(generate, '--config=expo.config.ts');
|
||||
assert.equal(res.type, 'handler');
|
||||
if (res.type !== 'handler') assert.fail(res.type, 'handler');
|
||||
expect(res.options).toStrictEqual({
|
||||
dialect: 'sqlite',
|
||||
name: undefined,
|
||||
custom: false,
|
||||
prefix: 'index',
|
||||
breakpoints: true,
|
||||
schema: './schema.ts',
|
||||
out: 'drizzle',
|
||||
bundle: true, // expo driver
|
||||
casing: undefined,
|
||||
driver: 'expo',
|
||||
});
|
||||
});
|
||||
|
||||
test('generate #9', async (t) => {
|
||||
const res = await brotest(generate, '--config=durable-sqlite.config.ts');
|
||||
assert.equal(res.type, 'handler');
|
||||
if (res.type !== 'handler') assert.fail(res.type, 'handler');
|
||||
expect(res.options).toStrictEqual({
|
||||
dialect: 'sqlite',
|
||||
name: undefined,
|
||||
custom: false,
|
||||
prefix: 'index',
|
||||
breakpoints: true,
|
||||
schema: './schema.ts',
|
||||
out: 'drizzle',
|
||||
bundle: true, // expo driver
|
||||
casing: undefined,
|
||||
driver: 'durable-sqlite',
|
||||
});
|
||||
});
|
||||
|
||||
// cli | pass through name, prefix and custom
|
||||
test('generate #9', async (t) => {
|
||||
const res = await brotest(
|
||||
generate,
|
||||
'--dialect=postgresql --schema=schema.ts --out=out --prefix=timestamp --name=custom --custom',
|
||||
);
|
||||
|
||||
if (res.type !== 'handler') assert.fail(res.type, 'handler');
|
||||
expect(res.options).toStrictEqual({
|
||||
dialect: 'postgresql',
|
||||
name: 'custom',
|
||||
custom: true,
|
||||
prefix: 'timestamp',
|
||||
breakpoints: true,
|
||||
schema: 'schema.ts',
|
||||
out: 'out',
|
||||
bundle: false,
|
||||
casing: undefined,
|
||||
driver: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
// --- errors ---
|
||||
test('err #1', async (t) => {
|
||||
const res = await brotest(generate, '--schema=src/schema.ts');
|
||||
assert.equal(res.type, 'error');
|
||||
});
|
||||
|
||||
test('err #2', async (t) => {
|
||||
const res = await brotest(generate, '--dialect=postgresql');
|
||||
assert.equal(res.type, 'error');
|
||||
});
|
||||
|
||||
test('err #3', async (t) => {
|
||||
const res = await brotest(generate, '--dialect=postgresql2');
|
||||
assert.equal(res.type, 'error');
|
||||
});
|
||||
|
||||
test('err #4', async (t) => {
|
||||
const res = await brotest(generate, '--driver=expo');
|
||||
assert.equal(res.type, 'error');
|
||||
});
|
||||
|
||||
test('err #5', async (t) => {
|
||||
const res = await brotest(generate, '--dialect=postgresql --out=out');
|
||||
assert.equal(res.type, 'error');
|
||||
});
|
||||
|
||||
test('err #6', async (t) => {
|
||||
const res = await brotest(generate, '--config=drizzle.config.ts --out=out');
|
||||
assert.equal(res.type, 'error');
|
||||
});
|
||||
|
||||
test('err #7', async (t) => {
|
||||
const res = await brotest(generate, '--config=drizzle.config.ts --schema=schema.ts');
|
||||
assert.equal(res.type, 'error');
|
||||
});
|
||||
|
||||
test('err #8', async (t) => {
|
||||
const res = await brotest(generate, '--config=drizzle.config.ts --dialect=postgresql');
|
||||
assert.equal(res.type, 'error');
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
import { test as brotest } from '@drizzle-team/brocli';
|
||||
import { assert, expect, test } from 'vitest';
|
||||
import { migrate } from '../src/cli/schema';
|
||||
|
||||
// good:
|
||||
// #1 drizzle-kit generate
|
||||
// #2 drizzle-kit generate --config=turso.config.ts
|
||||
// #3 drizzle-kit generate --config=d1http.config.ts
|
||||
// #4 drizzle-kit generate --config=postgres.config.ts ## spread connection params
|
||||
// #5 drizzle-kit generate --config=drizzle2.config.ts ## custom schema and table for migrations journal
|
||||
|
||||
// errors:
|
||||
// #1 drizzle-kit generate --config=expo.config.ts
|
||||
// TODO: missing required params in config?
|
||||
|
||||
test('migrate #1', async (t) => {
|
||||
const res = await brotest(migrate, '');
|
||||
if (res.type !== 'handler') assert.fail(res.type, 'handler');
|
||||
expect(res.options).toStrictEqual({
|
||||
dialect: 'postgresql',
|
||||
out: 'drizzle',
|
||||
credentials: {
|
||||
url: 'postgresql://postgres:postgres@127.0.0.1:5432/db',
|
||||
},
|
||||
schema: undefined, // drizzle migrations table schema
|
||||
table: undefined, // drizzle migrations table name
|
||||
});
|
||||
});
|
||||
|
||||
test('migrate #2', async (t) => {
|
||||
const res = await brotest(migrate, '--config=turso.config.ts');
|
||||
if (res.type !== 'handler') assert.fail(res.type, 'handler');
|
||||
expect(res.options).toStrictEqual({
|
||||
dialect: 'turso',
|
||||
out: 'drizzle',
|
||||
credentials: {
|
||||
authToken: 'token',
|
||||
url: 'turso.dev',
|
||||
},
|
||||
schema: undefined, // drizzle migrations table schema
|
||||
table: undefined, // drizzle migrations table name
|
||||
});
|
||||
});
|
||||
|
||||
test('migrate #3', async (t) => {
|
||||
const res = await brotest(migrate, '--config=d1http.config.ts');
|
||||
if (res.type !== 'handler') assert.fail(res.type, 'handler');
|
||||
expect(res.options).toStrictEqual({
|
||||
dialect: 'sqlite',
|
||||
out: 'drizzle',
|
||||
credentials: {
|
||||
driver: 'd1-http',
|
||||
accountId: 'accid',
|
||||
databaseId: 'dbid',
|
||||
token: 'token',
|
||||
},
|
||||
schema: undefined, // drizzle migrations table schema
|
||||
table: undefined, // drizzle migrations table name
|
||||
});
|
||||
});
|
||||
|
||||
test('migrate #4', async (t) => {
|
||||
const res = await brotest(migrate, '--config=postgres.config.ts');
|
||||
if (res.type !== 'handler') assert.fail(res.type, 'handler');
|
||||
expect(res.options).toStrictEqual({
|
||||
dialect: 'postgresql',
|
||||
out: 'drizzle',
|
||||
credentials: {
|
||||
database: 'db',
|
||||
host: '127.0.0.1',
|
||||
password: 'postgres',
|
||||
port: 5432,
|
||||
user: 'postgresql',
|
||||
},
|
||||
schema: undefined, // drizzle migrations table schema
|
||||
table: undefined, // drizzle migrations table name
|
||||
});
|
||||
});
|
||||
|
||||
// catched a bug
|
||||
test('migrate #5', async (t) => {
|
||||
const res = await brotest(migrate, '--config=postgres2.config.ts');
|
||||
if (res.type !== 'handler') assert.fail(res.type, 'handler');
|
||||
expect(res.options).toStrictEqual({
|
||||
dialect: 'postgresql',
|
||||
out: 'drizzle',
|
||||
credentials: {
|
||||
database: 'db',
|
||||
host: '127.0.0.1',
|
||||
password: 'postgres',
|
||||
port: 5432,
|
||||
user: 'postgresql',
|
||||
},
|
||||
schema: 'custom', // drizzle migrations table schema
|
||||
table: 'custom', // drizzle migrations table name
|
||||
});
|
||||
});
|
||||
|
||||
// --- errors ---
|
||||
test('err #1', async (t) => {
|
||||
const res = await brotest(migrate, '--config=expo.config.ts');
|
||||
assert.equal(res.type, 'error');
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
import { test as brotest } from '@drizzle-team/brocli';
|
||||
import { assert, expect, test } from 'vitest';
|
||||
import { push } from '../src/cli/schema';
|
||||
|
||||
// good:
|
||||
// #1 drizzle-kit push
|
||||
// #2 drizzle-kit push --config=turso.config.ts
|
||||
// #3 drizzle-kit push --config=d1http.config.ts
|
||||
// #4 drizzle-kit push --config=postgres.config.ts ## spread connection params
|
||||
// #5 drizzle-kit push --config=drizzle2.config.ts ## custom schema and table for migrations journal
|
||||
|
||||
// errors:
|
||||
// #1 drizzle-kit push --config=expo.config.ts
|
||||
// TODO: missing required params in config?
|
||||
|
||||
test('push #1', async (t) => {
|
||||
const res = await brotest(push, '');
|
||||
if (res.type !== 'handler') assert.fail(res.type, 'handler');
|
||||
expect(res.options).toStrictEqual({
|
||||
dialect: 'postgresql',
|
||||
credentials: {
|
||||
url: 'postgresql://postgres:postgres@127.0.0.1:5432/db',
|
||||
},
|
||||
force: false,
|
||||
schemaPath: './schema.ts',
|
||||
schemasFilter: ['public'],
|
||||
tablesFilter: [],
|
||||
entities: undefined,
|
||||
strict: false,
|
||||
verbose: false,
|
||||
casing: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test('push #2', async (t) => {
|
||||
const res = await brotest(push, '--config=turso.config.ts');
|
||||
if (res.type !== 'handler') assert.fail(res.type, 'handler');
|
||||
expect(res.options).toStrictEqual({
|
||||
dialect: 'turso',
|
||||
credentials: {
|
||||
authToken: 'token',
|
||||
url: 'turso.dev',
|
||||
},
|
||||
force: false,
|
||||
schemaPath: './schema.ts',
|
||||
schemasFilter: ['public'],
|
||||
tablesFilter: [],
|
||||
strict: false,
|
||||
verbose: false,
|
||||
casing: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test('push #3', async (t) => {
|
||||
const res = await brotest(push, '--config=d1http.config.ts');
|
||||
if (res.type !== 'handler') assert.fail(res.type, 'handler');
|
||||
expect(res.options).toStrictEqual({
|
||||
dialect: 'sqlite',
|
||||
credentials: {
|
||||
driver: 'd1-http',
|
||||
accountId: 'accid',
|
||||
databaseId: 'dbid',
|
||||
token: 'token',
|
||||
},
|
||||
force: false,
|
||||
schemaPath: './schema.ts',
|
||||
schemasFilter: ['public'],
|
||||
tablesFilter: [],
|
||||
strict: false,
|
||||
verbose: false,
|
||||
casing: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test('push #4', async (t) => {
|
||||
const res = await brotest(push, '--config=postgres.config.ts');
|
||||
if (res.type !== 'handler') assert.fail(res.type, 'handler');
|
||||
expect(res.options).toStrictEqual({
|
||||
dialect: 'postgresql',
|
||||
credentials: {
|
||||
database: 'db',
|
||||
host: '127.0.0.1',
|
||||
password: 'postgres',
|
||||
port: 5432,
|
||||
user: 'postgresql',
|
||||
},
|
||||
force: false,
|
||||
schemaPath: './schema.ts',
|
||||
schemasFilter: ['public'],
|
||||
tablesFilter: [],
|
||||
entities: undefined,
|
||||
strict: false,
|
||||
verbose: false,
|
||||
casing: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
// catched a bug
|
||||
test('push #5', async (t) => {
|
||||
const res = await brotest(push, '--config=postgres2.config.ts');
|
||||
if (res.type !== 'handler') assert.fail(res.type, 'handler');
|
||||
expect(res.options).toStrictEqual({
|
||||
dialect: 'postgresql',
|
||||
credentials: {
|
||||
database: 'db',
|
||||
host: '127.0.0.1',
|
||||
password: 'postgres',
|
||||
port: 5432,
|
||||
user: 'postgresql',
|
||||
},
|
||||
schemaPath: './schema.ts',
|
||||
schemasFilter: ['public'],
|
||||
tablesFilter: [],
|
||||
strict: false,
|
||||
entities: undefined,
|
||||
force: false,
|
||||
verbose: false,
|
||||
casing: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
// --- errors ---
|
||||
test('err #1', async (t) => {
|
||||
const res = await brotest(push, '--config=expo.config.ts');
|
||||
assert.equal(res.type, 'error');
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from '../../src';
|
||||
|
||||
export default defineConfig({
|
||||
schema: './schema.ts',
|
||||
dialect: 'sqlite',
|
||||
driver: 'd1-http',
|
||||
dbCredentials: {
|
||||
accountId: 'accid',
|
||||
databaseId: 'dbid',
|
||||
token: 'token',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from '../../src';
|
||||
|
||||
export default defineConfig({
|
||||
schema: './schema.ts',
|
||||
dialect: 'postgresql',
|
||||
dbCredentials: {
|
||||
url: 'postgresql://postgres:postgres@127.0.0.1:5432/db',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from '../../src';
|
||||
|
||||
export default defineConfig({
|
||||
schema: './schema.ts',
|
||||
dialect: 'sqlite',
|
||||
driver: 'durable-sqlite',
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from '../../src';
|
||||
|
||||
export default defineConfig({
|
||||
schema: './schema.ts',
|
||||
dialect: 'sqlite',
|
||||
driver: 'expo',
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from '../../src';
|
||||
|
||||
export default defineConfig({
|
||||
schema: './schema.ts',
|
||||
dialect: 'postgresql',
|
||||
dbCredentials: {
|
||||
host: '127.0.0.1',
|
||||
port: 5432,
|
||||
user: 'postgresql',
|
||||
password: 'postgres',
|
||||
database: 'db',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { defineConfig } from '../../src';
|
||||
|
||||
export default defineConfig({
|
||||
schema: './schema.ts',
|
||||
dialect: 'postgresql',
|
||||
dbCredentials: {
|
||||
host: '127.0.0.1',
|
||||
port: 5432,
|
||||
user: 'postgresql',
|
||||
password: 'postgres',
|
||||
database: 'db',
|
||||
},
|
||||
migrations: {
|
||||
schema: 'custom',
|
||||
table: 'custom',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
// mock
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user