chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:10:05 +08:00
commit d37d8d293b
1388 changed files with 484182 additions and 0 deletions
+25
View File
@@ -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
+79
View File
@@ -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
```
+51
View File
@@ -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');
+153
View File
@@ -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);
});
+78
View File
@@ -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;
}
+296
View File
@@ -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
+48
View File
@@ -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'));
}
+144
View File
@@ -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"
}
}
}
+31
View File
@@ -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
+58
View File
@@ -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 {};
+668
View File
@@ -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;
};
+2
View File
@@ -0,0 +1,2 @@
const _ = '';
export default _;
+52
View File
@@ -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);
}
};
+60
View File
@@ -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`,
);
};
+900
View File
@@ -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)],
};
};
+102
View File
@@ -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 };
};
+269
View File
@@ -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)],
};
};
+179
View File
@@ -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>,
},
};
};
+639
View File
@@ -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)],
};
};
+52
View File
@@ -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: {},
};
};
+938
View File
@@ -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
+48
View File
@@ -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,
});
+856
View File
@@ -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);
}
},
});
+38
View File
@@ -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!,
};
}
}
+112
View File
@@ -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 {};
}
};
+79
View File
@@ -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>;
+193
View File
@@ -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')}`;
};
+80
View File
@@ -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);
};
+27
View File
@@ -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);
};
+61
View File
@@ -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);
};
+104
View File
@@ -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);
};
+26
View File
@@ -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(),
});
+657
View File
@@ -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 [];
};
+10
View File
@@ -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',
];
+61
View File
@@ -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;
};
+349
View File
@@ -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 lets 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.
* Its optional and true by default, its 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.
* Its 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 lets 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.
* Its optional and true by default, its 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.
* Its 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
+913
View File
@@ -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;
};
+533
View File
@@ -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;
};
+870
View File
@@ -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
+57
View File
@@ -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);
}
+208
View File
@@ -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;
};
+23
View File
@@ -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>;
+633
View File
@@ -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
+130
View File
@@ -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 };
};
+423
View File
@@ -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,
};
};
+110
View File
@@ -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,
};
};
+886
View File
@@ -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 };
};
+352
View File
@@ -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: {},
},
};
};
+848
View File
@@ -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}`),
);
},
};
};
+45
View File
@@ -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;
};
+157
View File
@@ -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
+597
View File
@@ -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];
};
+371
View File
@@ -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, "\\'");
}
+44
View File
@@ -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
+58
View File
@@ -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);
});
+79
View File
@@ -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');
});
+259
View File
@@ -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');
});
+103
View File
@@ -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');
});
+126
View File
@@ -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');
});
+12
View File
@@ -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',
},
});
+9
View File
@@ -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',
});
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from '../../src';
export default defineConfig({
schema: './schema.ts',
dialect: 'sqlite',
driver: 'expo',
});
+13
View File
@@ -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',
},
});
+17
View File
@@ -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',
},
});
+1
View File
@@ -0,0 +1 @@
// mock

Some files were not shown because too many files have changed in this diff Show More