Compare commits
18
Commits
3bbf00cdb0
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4097a5e5d | ||
|
|
e8f630ae98 | ||
|
|
b8e9ab07f2 | ||
|
|
44ec3d4c11 | ||
|
|
90fcd3867d | ||
|
|
4d85a69505 | ||
|
|
e9f0730405 | ||
|
|
c97bedc271 | ||
|
|
4f203ce4e7 | ||
|
|
000d40b4cb | ||
|
|
c40d8596c7 | ||
|
|
e51f524443 | ||
|
|
7ded5e949d | ||
|
|
42805c4a69 | ||
|
|
9357cd404a | ||
|
|
e1038ec8ea | ||
|
|
0420d50f15 | ||
|
|
246c42a214 |
@@ -34,3 +34,5 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
|||||||
/test
|
/test
|
||||||
.vscode
|
.vscode
|
||||||
.dump
|
.dump
|
||||||
|
|
||||||
|
/.bun-mariadb
|
||||||
@@ -86,7 +86,7 @@ bun add github:moduletrace/bun-mariadb
|
|||||||
Connection settings are read from the environment (not the config file):
|
Connection settings are read from the environment (not the config file):
|
||||||
|
|
||||||
| Variable | Required | Description |
|
| Variable | Required | Description |
|
||||||
| --------------------------------- | -------- | ------------------------------------ |
|
| --------------------------------- | -------- | ---------------------------------- |
|
||||||
| `BUN_MARIADB_SERVER_HOST` | Yes | MariaDB host |
|
| `BUN_MARIADB_SERVER_HOST` | Yes | MariaDB host |
|
||||||
| `BUN_MARIADB_SERVER_USERNAME` | Yes | Database user |
|
| `BUN_MARIADB_SERVER_USERNAME` | Yes | Database user |
|
||||||
| `BUN_MARIADB_SERVER_PASSWORD` | Yes | Database password |
|
| `BUN_MARIADB_SERVER_PASSWORD` | Yes | Database password |
|
||||||
@@ -135,9 +135,22 @@ const schema: BUN_MARIADB_DatabaseSchemaType = {
|
|||||||
{
|
{
|
||||||
tableName: "users",
|
tableName: "users",
|
||||||
fields: [
|
fields: [
|
||||||
{ fieldName: "first_name", dataType: "VARCHAR", integerLength: 255 },
|
{
|
||||||
{ fieldName: "last_name", dataType: "VARCHAR", integerLength: 255 },
|
fieldName: "first_name",
|
||||||
{ fieldName: "email", dataType: "VARCHAR", integerLength: 255, unique: true },
|
dataType: "VARCHAR",
|
||||||
|
dataLength: 255,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: "last_name",
|
||||||
|
dataType: "VARCHAR",
|
||||||
|
dataLength: 255,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: "email",
|
||||||
|
dataType: "VARCHAR",
|
||||||
|
dataLength: 255,
|
||||||
|
unique: true,
|
||||||
|
},
|
||||||
{ fieldName: "bio", dataType: "LONGTEXT", html: true },
|
{ fieldName: "bio", dataType: "LONGTEXT", html: true },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -188,7 +201,7 @@ await BunMariaDB.delete({ table: "users", targetId: 1 });
|
|||||||
The config file must be named `bun-mariadb.config.ts` and placed at the project root.
|
The config file must be named `bun-mariadb.config.ts` and placed at the project root.
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
| ------------------- | -------- | -------- | --------------------------------------------------------------------------- |
|
| -------------------- | -------- | -------- | ----------------------------------------------------------------------------- |
|
||||||
| `db_name` | `string` | Yes | MariaDB database name |
|
| `db_name` | `string` | Yes | MariaDB database name |
|
||||||
| `db_dir` | `string` | Yes | Directory for schema, types, and local artifacts (relative to project root) |
|
| `db_dir` | `string` | Yes | Directory for schema, types, and local artifacts (relative to project root) |
|
||||||
| `db_backup_dir` | `string` | No | Backup directory name, relative to `db_dir` (default: `.backups`) |
|
| `db_backup_dir` | `string` | No | Backup directory name, relative to `db_dir` (default: `.backups`) |
|
||||||
@@ -197,7 +210,7 @@ The config file must be named `bun-mariadb.config.ts` and placed at the project
|
|||||||
| `typedef_file_path` | `string` | No | Output path for generated TypeScript types (relative to project root) |
|
| `typedef_file_path` | `string` | No | Output path for generated TypeScript types (relative to project root) |
|
||||||
| `db_config` | `object` | No | Extra options passed to Bun's `SQL` MariaDB adapter |
|
| `db_config` | `object` | No | Extra options passed to Bun's `SQL` MariaDB adapter |
|
||||||
| `charset` | `string` | No | Database charset (default: `utf8mb4`) |
|
| `charset` | `string` | No | Database charset (default: `utf8mb4`) |
|
||||||
| `connection_timeout`| `number` | No | Connection timeout in ms (default: `10000`) |
|
| `connection_timeout` | `number` | No | Connection timeout in ms (default: `10000`) |
|
||||||
| `ssl_ca` | `string` | No | Path to SSL CA certificate (relative to project root) |
|
| `ssl_ca` | `string` | No | Path to SSL CA certificate (relative to project root) |
|
||||||
| `html_sanitize` | `object` | No | Extra HTML sanitizer allowlists (see [HTML Sanitization](#html-sanitization)) |
|
| `html_sanitize` | `object` | No | Extra HTML sanitizer allowlists (see [HTML Sanitization](#html-sanitization)) |
|
||||||
|
|
||||||
@@ -228,7 +241,6 @@ interface BUN_MARIADB_TableSchemaType {
|
|||||||
parentTableName?: string; // inherit / merge fields from another table
|
parentTableName?: string; // inherit / merge fields from another table
|
||||||
tableNameOld?: string; // rename: old name triggers ALTER TABLE RENAME
|
tableNameOld?: string; // rename: old name triggers ALTER TABLE RENAME
|
||||||
collation?: "utf8mb4_bin" | "utf8mb4_unicode_520_ci";
|
collation?: "utf8mb4_bin" | "utf8mb4_unicode_520_ci";
|
||||||
isVector?: boolean; // mark as vector-oriented table
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -238,12 +250,38 @@ interface BUN_MARIADB_TableSchemaType {
|
|||||||
type BUN_MARIADB_FieldSchemaType = {
|
type BUN_MARIADB_FieldSchemaType = {
|
||||||
fieldName?: string;
|
fieldName?: string;
|
||||||
dataType:
|
dataType:
|
||||||
| "CHAR" | "VARCHAR" | "TEXT" | "TINYTEXT" | "MEDIUMTEXT" | "LONGTEXT"
|
| "CHAR"
|
||||||
| "TINYINT" | "SMALLINT" | "MEDIUMINT" | "INT" | "BIGINT"
|
| "VARCHAR"
|
||||||
| "FLOAT" | "DOUBLE" | "DECIMAL"
|
| "TEXT"
|
||||||
| "BINARY" | "VARBINARY" | "BLOB" | "TINYBLOB" | "MEDIUMBLOB" | "LONGBLOB"
|
| "TINYTEXT"
|
||||||
| "DATE" | "TIME" | "DATETIME" | "TIMESTAMP" | "YEAR"
|
| "MEDIUMTEXT"
|
||||||
| "BOOLEAN" | "UUID" | "JSON" | "INET6" | "ENUM" | "SET" | "VECTOR";
|
| "LONGTEXT"
|
||||||
|
| "TINYINT"
|
||||||
|
| "SMALLINT"
|
||||||
|
| "MEDIUMINT"
|
||||||
|
| "INT"
|
||||||
|
| "BIGINT"
|
||||||
|
| "FLOAT"
|
||||||
|
| "DOUBLE"
|
||||||
|
| "DECIMAL"
|
||||||
|
| "BINARY"
|
||||||
|
| "VARBINARY"
|
||||||
|
| "BLOB"
|
||||||
|
| "TINYBLOB"
|
||||||
|
| "MEDIUMBLOB"
|
||||||
|
| "LONGBLOB"
|
||||||
|
| "DATE"
|
||||||
|
| "TIME"
|
||||||
|
| "DATETIME"
|
||||||
|
| "TIMESTAMP"
|
||||||
|
| "YEAR"
|
||||||
|
| "BOOLEAN"
|
||||||
|
| "UUID"
|
||||||
|
| "JSON"
|
||||||
|
| "INET6"
|
||||||
|
| "ENUM"
|
||||||
|
| "SET"
|
||||||
|
| "VECTOR";
|
||||||
primaryKey?: boolean;
|
primaryKey?: boolean;
|
||||||
autoIncrement?: boolean;
|
autoIncrement?: boolean;
|
||||||
notNullValue?: boolean;
|
notNullValue?: boolean;
|
||||||
@@ -253,7 +291,7 @@ type BUN_MARIADB_FieldSchemaType = {
|
|||||||
onUpdate?: string;
|
onUpdate?: string;
|
||||||
onUpdateLiteral?: string;
|
onUpdateLiteral?: string;
|
||||||
foreignKey?: BUN_MARIADB_ForeignKeyType;
|
foreignKey?: BUN_MARIADB_ForeignKeyType;
|
||||||
integerLength?: string | number; // e.g. VARCHAR length
|
dataLength?: string | number; // e.g. VARCHAR length
|
||||||
decimals?: string | number; // DECIMAL scale
|
decimals?: string | number; // DECIMAL scale
|
||||||
options?: (string | number)[]; // ENUM / SET values
|
options?: (string | number)[]; // ENUM / SET values
|
||||||
isVector?: boolean; // native VECTOR column
|
isVector?: boolean; // native VECTOR column
|
||||||
@@ -369,7 +407,7 @@ bunx bun-mariadb export [options]
|
|||||||
```
|
```
|
||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
| ------------------- | --------------------------------------------------------------------------- |
|
| ---------------- | -------------------------------------------------------------------------- |
|
||||||
| `-o`, `--output` | Output archive path (`.tar.gz` or `.zip`). Defaults to `{db_dir}/.exports` |
|
| `-o`, `--output` | Output archive path (`.tar.gz` or `.zip`). Defaults to `{db_dir}/.exports` |
|
||||||
| `-f`, `--format` | When `--output` is omitted: `tar.gz` (default) or `zip` |
|
| `-f`, `--format` | When `--output` is omitted: `tar.gz` (default) or `zip` |
|
||||||
|
|
||||||
@@ -401,7 +439,7 @@ bunx bun-mariadb import [file] [options]
|
|||||||
```
|
```
|
||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
| ---------------- | --------------------------------------------------------------------------- |
|
| --------------- | --------------------------------------------------------------------- |
|
||||||
| `[file]` | Path to a `.sql` dump or export archive (`.tar.gz` / `.tar` / `.zip`) |
|
| `[file]` | Path to a `.sql` dump or export archive (`.tar.gz` / `.tar` / `.zip`) |
|
||||||
| `--sql-only` | Archive only: restore SQL, do not overwrite `schema.ts` |
|
| `--sql-only` | Archive only: restore SQL, do not overwrite `schema.ts` |
|
||||||
| `--schema-only` | Archive only: write `schema.ts`, do not restore SQL |
|
| `--schema-only` | Archive only: write `schema.ts`, do not restore SQL |
|
||||||
@@ -642,7 +680,7 @@ type ServerQueryParam<T> = {
|
|||||||
### Equality Operators
|
### Equality Operators
|
||||||
|
|
||||||
| Equality | SQL Equivalent |
|
| Equality | SQL Equivalent |
|
||||||
| ----------------------- | ------------------------------------------------------ |
|
| ----------------------- | ----------------------------- |
|
||||||
| `EQUAL` (default) | `=` |
|
| `EQUAL` (default) | `=` |
|
||||||
| `NOT EQUAL` | `!=` |
|
| `NOT EQUAL` | `!=` |
|
||||||
| `LIKE` | `LIKE '%value%'` |
|
| `LIKE` | `LIKE '%value%'` |
|
||||||
@@ -704,7 +742,6 @@ MariaDB native `VECTOR(n)` columns and `VECTOR INDEX` are supported (MariaDB 11.
|
|||||||
```ts
|
```ts
|
||||||
{
|
{
|
||||||
tableName: "documents",
|
tableName: "documents",
|
||||||
isVector: true,
|
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
fieldName: "embedding",
|
fieldName: "embedding",
|
||||||
@@ -715,7 +752,7 @@ MariaDB native `VECTOR(n)` columns and `VECTOR INDEX` are supported (MariaDB 11.
|
|||||||
{
|
{
|
||||||
fieldName: "title",
|
fieldName: "title",
|
||||||
dataType: "VARCHAR",
|
dataType: "VARCHAR",
|
||||||
integerLength: 255,
|
dataLength: 255,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
indexes: [
|
indexes: [
|
||||||
@@ -826,7 +863,7 @@ const res = await BunMariaDB.select<BUN_MARIADB_MY_APP_USERS>({
|
|||||||
Every table automatically receives:
|
Every table automatically receives:
|
||||||
|
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
| ------------ | ------------------------------- | -------------------------------------- |
|
| ------------ | -------------------------------------------- | -------------------------------------- |
|
||||||
| `id` | `BIGINT PRIMARY KEY AUTO_INCREMENT NOT NULL` | Unique row identifier |
|
| `id` | `BIGINT PRIMARY KEY AUTO_INCREMENT NOT NULL` | Unique row identifier |
|
||||||
| `created_at` | `BIGINT` | Unix timestamp set on insert |
|
| `created_at` | `BIGINT` | Unix timestamp set on insert |
|
||||||
| `updated_at` | `BIGINT` | Unix timestamp updated on every update |
|
| `updated_at` | `BIGINT` | Unix timestamp updated on every update |
|
||||||
@@ -865,7 +902,6 @@ bun-mariadb/
|
|||||||
│ │ ├── create-db-schema.ts
|
│ │ ├── create-db-schema.ts
|
||||||
│ │ ├── create-table.ts
|
│ │ ├── create-table.ts
|
||||||
│ │ ├── update-table.ts
|
│ │ ├── update-table.ts
|
||||||
│ │ ├── recreate-table.ts
|
|
||||||
│ │ ├── sync-indexes.ts
|
│ │ ├── sync-indexes.ts
|
||||||
│ │ └── ...
|
│ │ └── ...
|
||||||
│ ├── types/
|
│ ├── types/
|
||||||
|
|||||||
Vendored
+18
-8
@@ -3,16 +3,17 @@ import path from "path";
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import grabDBDir from "../utils/grab-db-dir";
|
import grabDBDir from "../utils/grab-db-dir";
|
||||||
import { AppData } from "../data/app-data";
|
|
||||||
import { dumpDatabase } from "../utils/mariadb-dump-restore";
|
import { dumpDatabase } from "../utils/mariadb-dump-restore";
|
||||||
import { writeExportArchive } from "../utils/export-archive";
|
import { writeExportArchive } from "../utils/export-archive";
|
||||||
import trimExports from "../utils/trim-exports";
|
import trimExports from "../utils/trim-exports";
|
||||||
|
import { resolveDataFile, supportedDataFileNames, } from "../utils/resolve-and-load-data-file";
|
||||||
|
import { AppData } from "../data/app-data";
|
||||||
function defaultExportFileName(dbName, format) {
|
function defaultExportFileName(dbName, format) {
|
||||||
return `${dbName}-${Date.now()}.${format}`;
|
return `${dbName}-${Date.now()}.${format}`;
|
||||||
}
|
}
|
||||||
export default function () {
|
export default function () {
|
||||||
return new Command("export")
|
return new Command("export")
|
||||||
.description("Export database SQL dump + schema.ts into a portable archive")
|
.description("Export database SQL dump + schema into a portable archive")
|
||||||
.option("-o, --output <path>", "Output archive path (.tar.gz or .zip). Defaults to db exports dir")
|
.option("-o, --output <path>", "Output archive path (.tar.gz or .zip). Defaults to db exports dir")
|
||||||
.option("-f, --format <format>", "Archive format when --output is omitted: tar.gz | zip", "tar.gz")
|
.option("-f, --format <format>", "Archive format when --output is omitted: tar.gz | zip", "tar.gz")
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
@@ -22,9 +23,14 @@ export default function () {
|
|||||||
if (!fs.existsSync(export_dir)) {
|
if (!fs.existsSync(export_dir)) {
|
||||||
fs.mkdirSync(export_dir, { recursive: true });
|
fs.mkdirSync(export_dir, { recursive: true });
|
||||||
}
|
}
|
||||||
const schemaPath = path.join(db_dir, AppData.DbSchemaFileName);
|
const schemaResolved = (global.SCHEMA_FILE_PATH &&
|
||||||
if (!fs.existsSync(schemaPath)) {
|
fs.existsSync(global.SCHEMA_FILE_PATH) && {
|
||||||
console.error(chalk.red(`Schema file not found: ${schemaPath}`));
|
path: global.SCHEMA_FILE_PATH,
|
||||||
|
basename: path.basename(global.SCHEMA_FILE_PATH),
|
||||||
|
}) ||
|
||||||
|
resolveDataFile(db_dir, AppData.DbSchemaFileName);
|
||||||
|
if (!schemaResolved) {
|
||||||
|
console.error(chalk.red(`Schema file not found in \`${db_dir}\` (${supportedDataFileNames(AppData.DbSchemaFileName)})`));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const formatRaw = String(opts.format || "tar.gz").toLowerCase();
|
const formatRaw = String(opts.format || "tar.gz").toLowerCase();
|
||||||
@@ -42,16 +48,20 @@ export default function () {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const sql = await dumpDatabase(config);
|
const sql = await dumpDatabase(config);
|
||||||
const schemaTs = fs.readFileSync(schemaPath, "utf-8");
|
const schema = fs.readFileSync(schemaResolved.path, "utf-8");
|
||||||
await writeExportArchive({
|
await writeExportArchive({
|
||||||
contents: { sql, schemaTs },
|
contents: {
|
||||||
|
sql,
|
||||||
|
schema,
|
||||||
|
schemaFileName: schemaResolved.basename,
|
||||||
|
},
|
||||||
outPath,
|
outPath,
|
||||||
});
|
});
|
||||||
if (path.dirname(outPath) === export_dir) {
|
if (path.dirname(outPath) === export_dir) {
|
||||||
trimExports({ config });
|
trimExports({ config });
|
||||||
}
|
}
|
||||||
console.log(`${chalk.bold(chalk.green(`DB Export Success!`))} → ${outPath}`);
|
console.log(`${chalk.bold(chalk.green(`DB Export Success!`))} → ${outPath}`);
|
||||||
console.log(chalk.dim(`Contains: dump.sql + ${AppData.DbSchemaFileName}`));
|
console.log(chalk.dim(`Contains: dump.sql + ${schemaResolved.basename}`));
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
|
|||||||
Vendored
+15
-7
@@ -6,9 +6,10 @@ import { select } from "@inquirer/prompts";
|
|||||||
import grabDBDir from "../utils/grab-db-dir";
|
import grabDBDir from "../utils/grab-db-dir";
|
||||||
import grabSortedExports from "../utils/grab-sorted-exports";
|
import grabSortedExports from "../utils/grab-sorted-exports";
|
||||||
import grabBackupData from "../utils/grab-backup-data";
|
import grabBackupData from "../utils/grab-backup-data";
|
||||||
import { AppData } from "../data/app-data";
|
|
||||||
import { restoreDatabase } from "../utils/mariadb-dump-restore";
|
import { restoreDatabase } from "../utils/mariadb-dump-restore";
|
||||||
import { isArchivePath, isSqlPath, readExportArchive, } from "../utils/export-archive";
|
import { isArchivePath, isSqlPath, readExportArchive, } from "../utils/export-archive";
|
||||||
|
import { resolveDataFile } from "../utils/resolve-and-load-data-file";
|
||||||
|
import { AppData } from "../data/app-data";
|
||||||
function formatChoice(name, index) {
|
function formatChoice(name, index) {
|
||||||
const { backup_date } = grabBackupData({ backup_name: name });
|
const { backup_date } = grabBackupData({ backup_name: name });
|
||||||
const time = Number.isNaN(backup_date.getTime())
|
const time = Number.isNaN(backup_date.getTime())
|
||||||
@@ -18,10 +19,10 @@ function formatChoice(name, index) {
|
|||||||
}
|
}
|
||||||
export default function () {
|
export default function () {
|
||||||
return new Command("import")
|
return new Command("import")
|
||||||
.description("Import an SQL dump, or a full export archive (SQL + schema.ts)")
|
.description("Import an SQL dump, or a full export archive (SQL + schema)")
|
||||||
.argument("[file]", "Path to .sql file or export archive (.tar.gz / .tar / .zip)")
|
.argument("[file]", "Path to .sql file or export archive (.tar.gz / .tar / .zip)")
|
||||||
.option("--sql-only", "When importing an archive, restore SQL only (skip writing schema.ts)")
|
.option("--sql-only", "When importing an archive, restore SQL only (skip writing schema)")
|
||||||
.option("--schema-only", "When importing an archive, write schema.ts only (skip SQL restore)")
|
.option("--schema-only", "When importing an archive, write schema only (skip SQL restore)")
|
||||||
.action(async (fileArg, opts) => {
|
.action(async (fileArg, opts) => {
|
||||||
console.log(`Importing database ...`);
|
console.log(`Importing database ...`);
|
||||||
const config = global.CONFIG;
|
const config = global.CONFIG;
|
||||||
@@ -65,14 +66,21 @@ export default function () {
|
|||||||
console.error(chalk.red(`Cannot combine --sql-only and --schema-only.`));
|
console.error(chalk.red(`Cannot combine --sql-only and --schema-only.`));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const { sql, schemaTs } = await readExportArchive(filePath);
|
const { sql, schema, schemaFileName } = await readExportArchive(filePath);
|
||||||
if (!opts.schemaOnly) {
|
if (!opts.schemaOnly) {
|
||||||
await restoreDatabase(config, sql);
|
await restoreDatabase(config, sql);
|
||||||
console.log(chalk.green(`SQL restored from archive`));
|
console.log(chalk.green(`SQL restored from archive`));
|
||||||
}
|
}
|
||||||
if (!opts.sqlOnly) {
|
if (!opts.sqlOnly) {
|
||||||
const schemaPath = path.join(db_dir, AppData.DbSchemaFileName);
|
const existing = resolveDataFile(db_dir, AppData.DbSchemaFileName);
|
||||||
fs.writeFileSync(schemaPath, schemaTs, "utf-8");
|
const schemaPath = path.join(db_dir, schemaFileName);
|
||||||
|
// Remove a differently-named schema so only one format remains
|
||||||
|
if (existing &&
|
||||||
|
path.resolve(existing.path) !== path.resolve(schemaPath)) {
|
||||||
|
fs.unlinkSync(existing.path);
|
||||||
|
console.log(chalk.dim(`Removed previous schema file: ${existing.basename}`));
|
||||||
|
}
|
||||||
|
fs.writeFileSync(schemaPath, schema, "utf-8");
|
||||||
console.log(chalk.green(`Schema written → ${schemaPath}`));
|
console.log(chalk.green(`Schema written → ${schemaPath}`));
|
||||||
}
|
}
|
||||||
console.log(`${chalk.bold(chalk.green(`DB Import Success!`))} ← ${filePath}`);
|
console.log(`${chalk.bold(chalk.green(`DB Import Success!`))} ← ${filePath}`);
|
||||||
|
|||||||
Vendored
+1
-1
@@ -14,7 +14,7 @@ export default function () {
|
|||||||
const { ROOT_DIR } = grabDirNames();
|
const { ROOT_DIR } = grabDirNames();
|
||||||
const finaldbSchema = appendDefaultFieldsToDbSchema({ dbSchema });
|
const finaldbSchema = appendDefaultFieldsToDbSchema({ dbSchema });
|
||||||
if (!config.typedef_file_path) {
|
if (!config.typedef_file_path) {
|
||||||
console.error(`\`typedef_file_path\` is required in bun-mariadb.config.ts to generate types.`);
|
console.error(`\`typedef_file_path\` is required in bun-mariadb.config to generate types.`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const out_file = path.resolve(ROOT_DIR, config.typedef_file_path);
|
const out_file = path.resolve(ROOT_DIR, config.typedef_file_path);
|
||||||
|
|||||||
Vendored
+4
-2
@@ -1,11 +1,13 @@
|
|||||||
export declare const AppData: {
|
export declare const AppData: {
|
||||||
readonly ConfigFileName: "bun-mariadb.config.ts";
|
readonly ConfigFileName: "bun-mariadb.config";
|
||||||
readonly MaxBackups: 10;
|
readonly MaxBackups: 10;
|
||||||
readonly MaxExports: 10;
|
readonly MaxExports: 10;
|
||||||
readonly DefaultBackupDirName: ".backups";
|
readonly DefaultBackupDirName: ".backups";
|
||||||
readonly DefaultExportDirName: ".exports";
|
readonly DefaultExportDirName: ".exports";
|
||||||
readonly DbSchemaManagerTableName: "__db_schema_manager__";
|
readonly DbSchemaManagerTableName: "__db_schema_manager__";
|
||||||
readonly DbSchemaFileName: "schema.ts";
|
readonly DbSchemaFileName: "schema";
|
||||||
|
/** Priority order when resolving config/schema files */
|
||||||
|
readonly SupportedDataFileExtensions: readonly [".ts", ".js", ".json", ".yaml", ".yml"];
|
||||||
readonly MaxInitRetries: 50;
|
readonly MaxInitRetries: 50;
|
||||||
readonly InitRetryIntervalMilliseconds: 5000;
|
readonly InitRetryIntervalMilliseconds: 5000;
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+4
-2
@@ -1,11 +1,13 @@
|
|||||||
export const AppData = {
|
export const AppData = {
|
||||||
ConfigFileName: "bun-mariadb.config.ts",
|
ConfigFileName: "bun-mariadb.config",
|
||||||
MaxBackups: 10,
|
MaxBackups: 10,
|
||||||
MaxExports: 10,
|
MaxExports: 10,
|
||||||
DefaultBackupDirName: ".backups",
|
DefaultBackupDirName: ".backups",
|
||||||
DefaultExportDirName: ".exports",
|
DefaultExportDirName: ".exports",
|
||||||
DbSchemaManagerTableName: "__db_schema_manager__",
|
DbSchemaManagerTableName: "__db_schema_manager__",
|
||||||
DbSchemaFileName: "schema.ts",
|
DbSchemaFileName: "schema",
|
||||||
|
/** Priority order when resolving config/schema files */
|
||||||
|
SupportedDataFileExtensions: [".ts", ".js", ".json", ".yaml", ".yml"],
|
||||||
MaxInitRetries: 50,
|
MaxInitRetries: 50,
|
||||||
InitRetryIntervalMilliseconds: 5000,
|
InitRetryIntervalMilliseconds: 5000,
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+2
@@ -6,5 +6,7 @@ declare global {
|
|||||||
var CONFIG: BunMariaDBConfig;
|
var CONFIG: BunMariaDBConfig;
|
||||||
var DB_SCHEMA: BUN_MARIADB_DatabaseSchemaType;
|
var DB_SCHEMA: BUN_MARIADB_DatabaseSchemaType;
|
||||||
var MARIADB_CLIENT: Bun.SQL;
|
var MARIADB_CLIENT: Bun.SQL;
|
||||||
|
var CONFIG_FILE_PATH: string;
|
||||||
|
var SCHEMA_FILE_PATH: string;
|
||||||
}
|
}
|
||||||
export default function init(): void;
|
export default function init(): void;
|
||||||
|
|||||||
Vendored
+18
-13
@@ -4,19 +4,19 @@ import { AppData } from "../data/app-data";
|
|||||||
import grabDirNames from "../data/grab-dir-names";
|
import grabDirNames from "../data/grab-dir-names";
|
||||||
import { RequiredENVs, } from "../types";
|
import { RequiredENVs, } from "../types";
|
||||||
import setMariaDBClient from "./set-mariadb-client";
|
import setMariaDBClient from "./set-mariadb-client";
|
||||||
|
import { resolveAndLoadDataFile, supportedDataFileNames, } from "../utils/resolve-and-load-data-file";
|
||||||
export default function init() {
|
export default function init() {
|
||||||
try {
|
try {
|
||||||
const { ROOT_DIR } = grabDirNames();
|
const { ROOT_DIR } = grabDirNames();
|
||||||
const { ConfigFileName } = AppData;
|
const { ConfigFileName } = AppData;
|
||||||
const ConfigFilePath = path.join(ROOT_DIR, ConfigFileName);
|
const loadedConfig = resolveAndLoadDataFile(ROOT_DIR, ConfigFileName);
|
||||||
if (!fs.existsSync(ConfigFilePath)) {
|
if (!loadedConfig) {
|
||||||
console.error(`Please create a \`${ConfigFileName}\` file at the root of your project.`);
|
console.error(`Please create a ${supportedDataFileNames(ConfigFileName)} file at the root of your project.`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const ConfigImport = require(ConfigFilePath);
|
const Config = loadedConfig.data;
|
||||||
const Config = ConfigImport["default"];
|
if (!Config || typeof Config !== "object") {
|
||||||
if (!Config) {
|
console.error(`Invalid config in \`${loadedConfig.path}\`. Expected a config object${loadedConfig.format === "ts" || loadedConfig.format === "js" ? " (export default)" : ""}.`);
|
||||||
console.error(`No default export from \`${ConfigFilePath}\`. Please export a default module.`);
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
if (!Config.db_name) {
|
if (!Config.db_name) {
|
||||||
@@ -34,20 +34,23 @@ export default function init() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!Config.db_dir) {
|
if (!Config.db_dir) {
|
||||||
console.error(`\`db_dir\` is required in your config. This directory holds all database related configuration. Also note that a \`${AppData["DbSchemaFileName"]}\` file is also required in this directory to define your database schema`);
|
console.error(`\`db_dir\` is required in your config. This directory holds all database related configuration. Also note that a ${supportedDataFileNames(AppData.DbSchemaFileName)} file is also required in this directory to define your database schema`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const db_dir = path.resolve(ROOT_DIR, Config.db_dir);
|
const db_dir = path.resolve(ROOT_DIR, Config.db_dir);
|
||||||
if (!fs.existsSync(db_dir)) {
|
if (!fs.existsSync(db_dir)) {
|
||||||
fs.mkdirSync(db_dir, { recursive: true });
|
fs.mkdirSync(db_dir, { recursive: true });
|
||||||
}
|
}
|
||||||
const DBSchemaFilePath = path.join(db_dir, AppData["DbSchemaFileName"]);
|
const loadedSchema = resolveAndLoadDataFile(db_dir, AppData.DbSchemaFileName);
|
||||||
if (!fs.existsSync(DBSchemaFilePath)) {
|
if (!loadedSchema) {
|
||||||
console.error(`Please create a schema file at \`${DBSchemaFilePath}\`. Don't forget to export a default module from this file.`);
|
console.error(`Please create a schema file (${supportedDataFileNames(AppData.DbSchemaFileName)}) in \`${db_dir}\`${loadedConfig.format === "ts" || loadedConfig.format === "js" ? ". Don't forget to export a default module from .ts/.js files." : "."}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const DbSchema = loadedSchema.data;
|
||||||
|
if (!DbSchema || typeof DbSchema !== "object") {
|
||||||
|
console.error(`Invalid schema in \`${loadedSchema.path}\`. Expected a schema object${loadedSchema.format === "ts" || loadedSchema.format === "js" ? " (export default)" : ""}.`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const DbSchemaImport = require(DBSchemaFilePath);
|
|
||||||
const DbSchema = DbSchemaImport["default"];
|
|
||||||
const backup_dir = Config.db_backup_dir || AppData["DefaultBackupDirName"];
|
const backup_dir = Config.db_backup_dir || AppData["DefaultBackupDirName"];
|
||||||
const BackupDir = path.resolve(db_dir, backup_dir);
|
const BackupDir = path.resolve(db_dir, backup_dir);
|
||||||
if (!fs.existsSync(BackupDir)) {
|
if (!fs.existsSync(BackupDir)) {
|
||||||
@@ -59,6 +62,8 @@ export default function init() {
|
|||||||
}
|
}
|
||||||
global.CONFIG = Config;
|
global.CONFIG = Config;
|
||||||
global.DB_SCHEMA = DbSchema;
|
global.DB_SCHEMA = DbSchema;
|
||||||
|
global.CONFIG_FILE_PATH = loadedConfig.path;
|
||||||
|
global.SCHEMA_FILE_PATH = loadedSchema.path;
|
||||||
if (!global.CONFIG) {
|
if (!global.CONFIG) {
|
||||||
console.error(`Couldn't grab global Config.`);
|
console.error(`Couldn't grab global Config.`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
Vendored
+2
-1
@@ -17,4 +17,5 @@ declare const BunMariaDB: {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
export default BunMariaDB;
|
export default BunMariaDB;
|
||||||
export type { BunMariaDBConfig, BUN_MARIADB_DatabaseSchemaType, BUN_MARIADB_TableSchemaType, BUN_MARIADB_FieldSchemaType, BUN_MARIADB_IndexSchemaType, BUN_MARIADB_UniqueConstraintSchemaType, BUN_MARIADB_ForeignKeyType, DBResponseObject, ServerQueryParam, } from "./types";
|
export type * from "./types";
|
||||||
|
export { UsersOmitedFields, MariaDBCollations, MariaDBCharsets, TextFieldTypesArray, BUN_MARIADB_DATATYPES, MariaDBIndexTypes, ServerQueryOperators, ServerQueryEqualities, SQlComparisons, DataCrudRequestMethods, DataCrudRequestMethodsLowerCase, DsqlCrudActions, QueryFields, DockerComposeServices, IndexTypes, DefaultFields, RequiredENVs, } from "./types";
|
||||||
|
|||||||
Vendored
+1
@@ -19,3 +19,4 @@ const BunMariaDB = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
export default BunMariaDB;
|
export default BunMariaDB;
|
||||||
|
export { UsersOmitedFields, MariaDBCollations, MariaDBCharsets, TextFieldTypesArray, BUN_MARIADB_DATATYPES, MariaDBIndexTypes, ServerQueryOperators, ServerQueryEqualities, SQlComparisons, DataCrudRequestMethods, DataCrudRequestMethodsLowerCase, DsqlCrudActions, QueryFields, DockerComposeServices, IndexTypes, DefaultFields, RequiredENVs, } from "./types";
|
||||||
|
|||||||
Vendored
+1
@@ -49,6 +49,7 @@ export default async function dbHandler({ query, values, config }) {
|
|||||||
single_res: res_array?.[0],
|
single_res: res_array?.[0],
|
||||||
insert_return,
|
insert_return,
|
||||||
count,
|
count,
|
||||||
|
db_res: res,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
|
|||||||
Vendored
+3
-2
@@ -12,7 +12,7 @@ export default async function DbDelete({ table, query, targetId, config, }) {
|
|||||||
finalQuery = _.merge(finalQuery, {
|
finalQuery = _.merge(finalQuery, {
|
||||||
query: {
|
query: {
|
||||||
id: {
|
id: {
|
||||||
value: String(targetId),
|
value: Number(targetId),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -41,8 +41,9 @@ export default async function DbDelete({ table, query, targetId, config, }) {
|
|||||||
});
|
});
|
||||||
if (!res.success) {
|
if (!res.success) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
|
||||||
msg: "Database delete failed",
|
msg: "Database delete failed",
|
||||||
|
...res,
|
||||||
|
success: false,
|
||||||
debug: {
|
debug: {
|
||||||
sqlObj,
|
sqlObj,
|
||||||
},
|
},
|
||||||
|
|||||||
Vendored
+2
-1
@@ -38,8 +38,9 @@ export default async function DbInsert({ table, data, update_on_duplicate, confi
|
|||||||
});
|
});
|
||||||
if (!res.success) {
|
if (!res.success) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
|
||||||
msg: "Database insert failed",
|
msg: "Database insert failed",
|
||||||
|
...res,
|
||||||
|
success: false,
|
||||||
debug: {
|
debug: {
|
||||||
sqlObj,
|
sqlObj,
|
||||||
},
|
},
|
||||||
|
|||||||
Vendored
+3
-2
@@ -12,7 +12,7 @@ export default async function DbSelect({ table, query, count, targetId, config,
|
|||||||
finalQuery = _.merge(finalQuery, {
|
finalQuery = _.merge(finalQuery, {
|
||||||
query: {
|
query: {
|
||||||
id: {
|
id: {
|
||||||
value: String(targetId),
|
value: Number(targetId),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -28,8 +28,9 @@ export default async function DbSelect({ table, query, count, targetId, config,
|
|||||||
});
|
});
|
||||||
if (!res.success) {
|
if (!res.success) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
|
||||||
msg: "Database select failed",
|
msg: "Database select failed",
|
||||||
|
...res,
|
||||||
|
success: false,
|
||||||
debug: {
|
debug: {
|
||||||
sqlObj,
|
sqlObj,
|
||||||
sql: sqlObj.string,
|
sql: sqlObj.string,
|
||||||
|
|||||||
Vendored
+6
-2
@@ -9,8 +9,9 @@ export default async function DbSQL({ sql, values }) {
|
|||||||
});
|
});
|
||||||
if (!res.success) {
|
if (!res.success) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
|
||||||
msg: "Database query failed",
|
msg: "Database query failed",
|
||||||
|
...res,
|
||||||
|
success: false,
|
||||||
debug: {
|
debug: {
|
||||||
sqlObj: {
|
sqlObj: {
|
||||||
sql: trimmedSql,
|
sql: trimmedSql,
|
||||||
@@ -24,7 +25,10 @@ export default async function DbSQL({ sql, values }) {
|
|||||||
const single_res = isSelect ? payload?.[0] : res.single_res;
|
const single_res = isSelect ? payload?.[0] : res.single_res;
|
||||||
const singleRaw = res.single_res;
|
const singleRaw = res.single_res;
|
||||||
return {
|
return {
|
||||||
success: true,
|
...res,
|
||||||
|
success: isSelect
|
||||||
|
? Boolean(single_res) || Boolean(payload?.[0])
|
||||||
|
: true,
|
||||||
payload,
|
payload,
|
||||||
single_res,
|
single_res,
|
||||||
debug: {
|
debug: {
|
||||||
|
|||||||
Vendored
+7
-22
@@ -13,7 +13,7 @@ export default async function DbUpdate({ table, data, query, targetId, config, }
|
|||||||
finalQuery = _.merge(finalQuery, {
|
finalQuery = _.merge(finalQuery, {
|
||||||
query: {
|
query: {
|
||||||
id: {
|
id: {
|
||||||
value: String(targetId),
|
value: Number(targetId),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -60,41 +60,26 @@ export default async function DbUpdate({ table, data, query, targetId, config, }
|
|||||||
values: values,
|
values: values,
|
||||||
config,
|
config,
|
||||||
});
|
});
|
||||||
|
if (res.error) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
sqlObj.string = sql;
|
sqlObj.string = sql;
|
||||||
sqlObj.values = values;
|
sqlObj.values = values;
|
||||||
let updated_sql = ``;
|
let updated_sql = ``;
|
||||||
let updated_sql_values = [];
|
let updated_sql_values = [];
|
||||||
updated_sql += `SELECT * FROM ${quoteIdentifier(table)} ${whereClause}`;
|
updated_sql += `SELECT * FROM ${quoteIdentifier(table)} ${whereClause}`;
|
||||||
updated_sql_values = [...updated_sql_values, ...sqlQueryObj.values];
|
updated_sql_values = [...sqlQueryObj.values];
|
||||||
updated_sql += ` AND `;
|
|
||||||
for (let i = 0; i < keys.length; i++) {
|
|
||||||
const key = keys[i];
|
|
||||||
if (!key)
|
|
||||||
continue;
|
|
||||||
if (key == "updated_at")
|
|
||||||
continue;
|
|
||||||
const isLast = i == keys.length - 1;
|
|
||||||
updated_sql += ` ${quoteIdentifier(key)}=?`;
|
|
||||||
updated_sql_values.push(finalData[key] ?? null);
|
|
||||||
if (!isLast) {
|
|
||||||
updated_sql += ` AND `;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const updated_res = await dbHandler({
|
const updated_res = await dbHandler({
|
||||||
query: updated_sql,
|
query: updated_sql,
|
||||||
values: updated_sql_values,
|
values: updated_sql_values,
|
||||||
config,
|
config,
|
||||||
});
|
});
|
||||||
const affected_rows = updated_res.payload?.length;
|
|
||||||
return {
|
return {
|
||||||
...res,
|
...updated_res,
|
||||||
success: Boolean(affected_rows),
|
|
||||||
insert_return: {
|
|
||||||
affected_rows,
|
|
||||||
},
|
|
||||||
debug: {
|
debug: {
|
||||||
sqlObj,
|
sqlObj,
|
||||||
},
|
},
|
||||||
|
db_res: res.db_res,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
|
|||||||
+7
-1
@@ -1,2 +1,8 @@
|
|||||||
import type { BUN_MARIADB_FieldSchemaType } from "../../types";
|
import type { BUN_MARIADB_FieldSchemaType } from "../../types";
|
||||||
export default function buildColumnDefinition(field: BUN_MARIADB_FieldSchemaType): string;
|
export type BuildColumnDefinitionOptions = {
|
||||||
|
/** UNIQUE is managed by syncUniqueConstraints on existing tables */
|
||||||
|
omitUnique?: boolean;
|
||||||
|
};
|
||||||
|
export default function buildColumnDefinition(field: BUN_MARIADB_FieldSchemaType, options?: BuildColumnDefinitionOptions): string;
|
||||||
|
/** Whether schema field requires NOT NULL */
|
||||||
|
export declare function fieldRequiresNotNull(field: BUN_MARIADB_FieldSchemaType): boolean;
|
||||||
|
|||||||
+9
-2
@@ -1,7 +1,7 @@
|
|||||||
import isVectorField from "./is-vector-field";
|
import isVectorField from "./is-vector-field";
|
||||||
import mapDataType from "./map-data-types";
|
import mapDataType from "./map-data-types";
|
||||||
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
export default function buildColumnDefinition(field) {
|
export default function buildColumnDefinition(field, options = {}) {
|
||||||
if (!field.fieldName) {
|
if (!field.fieldName) {
|
||||||
throw new Error("Field name is required");
|
throw new Error("Field name is required");
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,10 @@ export default function buildColumnDefinition(field) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// VECTOR columns cannot be UNIQUE in the usual sense
|
// VECTOR columns cannot be UNIQUE in the usual sense
|
||||||
if (field.unique && !field.primaryKey && !isVectorField(field)) {
|
if (!options.omitUnique &&
|
||||||
|
field.unique &&
|
||||||
|
!field.primaryKey &&
|
||||||
|
!isVectorField(field)) {
|
||||||
parts.push("UNIQUE");
|
parts.push("UNIQUE");
|
||||||
}
|
}
|
||||||
if (field.defaultValue !== undefined) {
|
if (field.defaultValue !== undefined) {
|
||||||
@@ -41,3 +44,7 @@ export default function buildColumnDefinition(field) {
|
|||||||
}
|
}
|
||||||
return parts.join(" ");
|
return parts.join(" ");
|
||||||
}
|
}
|
||||||
|
/** Whether schema field requires NOT NULL */
|
||||||
|
export function fieldRequiresNotNull(field) {
|
||||||
|
return Boolean(field.notNullValue || field.primaryKey || isVectorField(field));
|
||||||
|
}
|
||||||
|
|||||||
+3
-1
@@ -1,2 +1,4 @@
|
|||||||
import type { BUN_MARIADB_FieldSchemaType } from "../../types";
|
import type { BUN_MARIADB_FieldSchemaType } from "../../types";
|
||||||
export default function buildForeignKeyConstraint(field: BUN_MARIADB_FieldSchemaType): string;
|
export declare function defaultForeignKeyName(tableName: string, fieldName: string): string;
|
||||||
|
export declare function resolveForeignKeyName(field: BUN_MARIADB_FieldSchemaType, tableName: string): string;
|
||||||
|
export default function buildForeignKeyConstraint(field: BUN_MARIADB_FieldSchemaType, tableName: string): string;
|
||||||
|
|||||||
+10
-5
@@ -1,10 +1,15 @@
|
|||||||
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
export default function buildForeignKeyConstraint(field) {
|
export function defaultForeignKeyName(tableName, fieldName) {
|
||||||
|
return `fk_${tableName}_${fieldName}`;
|
||||||
|
}
|
||||||
|
export function resolveForeignKeyName(field, tableName) {
|
||||||
|
const fieldName = field.fieldName || "column";
|
||||||
|
return field.foreignKey?.foreignKeyName || defaultForeignKeyName(tableName, fieldName);
|
||||||
|
}
|
||||||
|
export default function buildForeignKeyConstraint(field, tableName) {
|
||||||
const fk = field.foreignKey;
|
const fk = field.foreignKey;
|
||||||
const constraintName = fk.foreignKeyName
|
const constraintName = resolveForeignKeyName(field, tableName);
|
||||||
? `CONSTRAINT ${MariaDBQuoteGen(fk.foreignKeyName)} `
|
let constraint = `CONSTRAINT ${MariaDBQuoteGen(constraintName)} FOREIGN KEY (${MariaDBQuoteGen(field.fieldName)}) REFERENCES ${MariaDBQuoteGen(fk.destinationTableName)}(${MariaDBQuoteGen(fk.destinationTableColumnName)})`;
|
||||||
: "";
|
|
||||||
let constraint = `${constraintName}FOREIGN KEY (${MariaDBQuoteGen(field.fieldName)}) REFERENCES ${MariaDBQuoteGen(fk.destinationTableName)}(${MariaDBQuoteGen(fk.destinationTableColumnName)})`;
|
|
||||||
if (fk.cascadeDelete) {
|
if (fk.cascadeDelete) {
|
||||||
constraint += " ON DELETE CASCADE";
|
constraint += " ON DELETE CASCADE";
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+10
-10
@@ -15,8 +15,8 @@ export default async function createTable({ table, config, }) {
|
|||||||
if (field.primaryKey && field.fieldName) {
|
if (field.primaryKey && field.fieldName) {
|
||||||
primaryKeys.push(field.fieldName);
|
primaryKeys.push(field.fieldName);
|
||||||
}
|
}
|
||||||
if (field.foreignKey && !table.isVector) {
|
if (field.foreignKey) {
|
||||||
foreignKeys.push(buildForeignKeyConstraint(field));
|
foreignKeys.push(buildForeignKeyConstraint(field, table.tableName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (primaryKeys.length > 0) {
|
if (primaryKeys.length > 0) {
|
||||||
@@ -25,15 +25,15 @@ export default async function createTable({ table, config, }) {
|
|||||||
}
|
}
|
||||||
if (table.uniqueConstraints) {
|
if (table.uniqueConstraints) {
|
||||||
for (const constraint of table.uniqueConstraints) {
|
for (const constraint of table.uniqueConstraints) {
|
||||||
if (constraint.constraintTableFields &&
|
const columns = (constraint.constraintTableFields || [])
|
||||||
constraint.constraintTableFields.length > 0) {
|
.map((field) => field.value)
|
||||||
const fields = constraint.constraintTableFields
|
.filter((value) => Boolean(value));
|
||||||
.map((field) => MariaDBQuoteGen(field.value))
|
if (columns.length === 0) {
|
||||||
.join(", ");
|
continue;
|
||||||
const constraintName = constraint.constraintName ||
|
|
||||||
`unique_${fields.replace(/`/g, "")}`;
|
|
||||||
columnDefinitions.push(`CONSTRAINT ${MariaDBQuoteGen(constraintName)} UNIQUE (${fields})`);
|
|
||||||
}
|
}
|
||||||
|
const fields = columns.map((col) => MariaDBQuoteGen(col)).join(", ");
|
||||||
|
const constraintName = constraint.constraintName || `unique_${columns.join("_")}`;
|
||||||
|
columnDefinitions.push(`CONSTRAINT ${MariaDBQuoteGen(constraintName)} UNIQUE (${fields})`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const sql = `CREATE TABLE IF NOT EXISTS ${MariaDBQuoteGen(table.tableName)} (${[...columnDefinitions, ...foreignKeys].join(", ")})${buildTableOptions(table)}`;
|
const sql = `CREATE TABLE IF NOT EXISTS ${MariaDBQuoteGen(table.tableName)} (${[...columnDefinitions, ...foreignKeys].join(", ")})${buildTableOptions(table)}`;
|
||||||
|
|||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
import type { BunMariaDBConfig } from "../../types";
|
||||||
|
export type ColumnInfoRow = {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
comment?: string;
|
||||||
|
isNullable: boolean;
|
||||||
|
columnDefault: string | null;
|
||||||
|
extra: string;
|
||||||
|
};
|
||||||
|
export default function getTableColumnsGemini({ tableName, config, }: {
|
||||||
|
tableName: string;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<ColumnInfoRow[]>;
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
import { querySchemaRows } from "./run-schema-query";
|
||||||
|
import schemaCondition from "./schema-condition";
|
||||||
|
export default async function getTableColumnsGemini({ tableName, config, }) {
|
||||||
|
const schemaCond = schemaCondition(config);
|
||||||
|
const rows = await querySchemaRows({
|
||||||
|
query: `
|
||||||
|
SELECT
|
||||||
|
c.COLUMN_NAME,
|
||||||
|
c.COLUMN_TYPE,
|
||||||
|
c.COLUMN_COMMENT,
|
||||||
|
c.IS_NULLABLE,
|
||||||
|
c.COLUMN_DEFAULT,
|
||||||
|
c.EXTRA,
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.CHECK_CONSTRAINTS cc
|
||||||
|
WHERE cc.CONSTRAINT_SCHEMA = c.TABLE_SCHEMA
|
||||||
|
AND cc.TABLE_NAME = c.TABLE_NAME
|
||||||
|
AND cc.CHECK_CLAUSE LIKE CONCAT('%json_valid(\`', c.COLUMN_NAME, '\`)%')
|
||||||
|
) AS IS_JSON
|
||||||
|
FROM information_schema.COLUMNS c
|
||||||
|
WHERE ${schemaCond.where.replace(/\bTABLE_SCHEMA\b/g, "c.TABLE_SCHEMA")}
|
||||||
|
AND c.TABLE_NAME = ?
|
||||||
|
ORDER BY c.ORDINAL_POSITION
|
||||||
|
`,
|
||||||
|
values: [...schemaCond.values, tableName],
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
return rows.map((row) => {
|
||||||
|
const isJson = Number(row.IS_JSON) === 1;
|
||||||
|
// Normalize longtext with a json_valid constraint to "json"
|
||||||
|
let resolvedType = row.COLUMN_TYPE;
|
||||||
|
if (isJson && row.COLUMN_TYPE.toLowerCase() === "longtext") {
|
||||||
|
resolvedType = "json";
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: row.COLUMN_NAME,
|
||||||
|
type: resolvedType,
|
||||||
|
comment: row.COLUMN_COMMENT,
|
||||||
|
isNullable: String(row.IS_NULLABLE).toUpperCase() === "YES",
|
||||||
|
columnDefault: row.COLUMN_DEFAULT,
|
||||||
|
extra: row.EXTRA || "",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
+3
@@ -3,6 +3,9 @@ export type ColumnInfoRow = {
|
|||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
comment?: string;
|
comment?: string;
|
||||||
|
isNullable: boolean;
|
||||||
|
columnDefault: string | null;
|
||||||
|
extra: string;
|
||||||
};
|
};
|
||||||
export default function getTableColumns({ tableName, config, }: {
|
export default function getTableColumns({ tableName, config, }: {
|
||||||
tableName: string;
|
tableName: string;
|
||||||
|
|||||||
Vendored
+4
-1
@@ -3,7 +3,7 @@ import schemaCondition from "./schema-condition";
|
|||||||
export default async function getTableColumns({ tableName, config, }) {
|
export default async function getTableColumns({ tableName, config, }) {
|
||||||
const schemaCond = schemaCondition(config);
|
const schemaCond = schemaCondition(config);
|
||||||
const rows = await querySchemaRows({
|
const rows = await querySchemaRows({
|
||||||
query: `SELECT COLUMN_NAME, COLUMN_TYPE, COLUMN_COMMENT FROM information_schema.COLUMNS WHERE ${schemaCond.where} AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION`,
|
query: `SELECT COLUMN_NAME, COLUMN_TYPE, COLUMN_COMMENT, IS_NULLABLE, COLUMN_DEFAULT, EXTRA FROM information_schema.COLUMNS WHERE ${schemaCond.where} AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION`,
|
||||||
values: [...schemaCond.values, tableName],
|
values: [...schemaCond.values, tableName],
|
||||||
config,
|
config,
|
||||||
});
|
});
|
||||||
@@ -11,5 +11,8 @@ export default async function getTableColumns({ tableName, config, }) {
|
|||||||
name: row.COLUMN_NAME,
|
name: row.COLUMN_NAME,
|
||||||
type: row.COLUMN_TYPE,
|
type: row.COLUMN_TYPE,
|
||||||
comment: row.COLUMN_COMMENT,
|
comment: row.COLUMN_COMMENT,
|
||||||
|
isNullable: String(row.IS_NULLABLE).toUpperCase() === "YES",
|
||||||
|
columnDefault: row.COLUMN_DEFAULT,
|
||||||
|
extra: row.EXTRA || "",
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-4
@@ -3,14 +3,17 @@ import MariaDBQuoteGen from "./mariadb-quote-gen";
|
|||||||
import resolveTable from "./resolve-table";
|
import resolveTable from "./resolve-table";
|
||||||
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
||||||
import schemaCondition from "./schema-condition";
|
import schemaCondition from "./schema-condition";
|
||||||
|
import { dropObsoleteForeignKeys, ensureForeignKeys, } from "./sync-foreign-keys";
|
||||||
import syncIndexes from "./sync-indexes";
|
import syncIndexes from "./sync-indexes";
|
||||||
|
import syncPrimaryKey from "./sync-primary-key";
|
||||||
|
import syncTableOptions from "./sync-table-options";
|
||||||
|
import syncUniqueConstraints from "./sync-unique-constraints";
|
||||||
import updateTable from "./update-table";
|
import updateTable from "./update-table";
|
||||||
import upsertDbManagerTable, { removeDbManagerTable, } from "./upsert-db-manager-table";
|
import upsertDbManagerTable, { removeDbManagerTable, } from "./upsert-db-manager-table";
|
||||||
export default async function handleDBSchemaTable({ db_schema, config, table, db_manager_table_name, existing_live_table, }) {
|
export default async function handleDBSchemaTable({ db_schema, config, table, db_manager_table_name, existing_live_table, }) {
|
||||||
const resolvedTable = resolveTable(table, db_schema);
|
const resolvedTable = resolveTable(table, db_schema);
|
||||||
let tableExistsTracked = Boolean(db_manager_table_name);
|
let tableExistsTracked = Boolean(db_manager_table_name);
|
||||||
let tableExistsLive = Boolean(existing_live_table?.TABLE_NAME);
|
let tableExistsLive = Boolean(existing_live_table?.TABLE_NAME);
|
||||||
let wasRenamed = false;
|
|
||||||
if (resolvedTable.tableNameOld &&
|
if (resolvedTable.tableNameOld &&
|
||||||
resolvedTable.tableNameOld !== resolvedTable.tableName) {
|
resolvedTable.tableNameOld !== resolvedTable.tableName) {
|
||||||
// Only hit information_schema when a rename is declared
|
// Only hit information_schema when a rename is declared
|
||||||
@@ -36,7 +39,6 @@ export default async function handleDBSchemaTable({ db_schema, config, table, db
|
|||||||
});
|
});
|
||||||
tableExistsTracked = true;
|
tableExistsTracked = true;
|
||||||
tableExistsLive = true;
|
tableExistsLive = true;
|
||||||
wasRenamed = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!tableExistsTracked && !tableExistsLive) {
|
if (!tableExistsTracked && !tableExistsLive) {
|
||||||
@@ -47,16 +49,26 @@ export default async function handleDBSchemaTable({ db_schema, config, table, db
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (!wasRenamed) {
|
// Columns first (also drops FKs on removed/modified columns)
|
||||||
await updateTable({
|
await updateTable({
|
||||||
table: resolvedTable,
|
table: resolvedTable,
|
||||||
config,
|
config,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
await upsertDbManagerTable({
|
await upsertDbManagerTable({
|
||||||
tableName: resolvedTable.tableName,
|
tableName: resolvedTable.tableName,
|
||||||
config,
|
config,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// Order matters:
|
||||||
|
// 1. Table options (engine/collation)
|
||||||
|
// 2. Drop obsolete/changed FKs (unlocks index cleanup)
|
||||||
|
// 3. Primary key
|
||||||
|
// 4. Indexes / uniques (FK columns need supporting indexes)
|
||||||
|
// 5. Ensure foreign keys exist
|
||||||
|
await syncTableOptions({ table: resolvedTable, config });
|
||||||
|
await dropObsoleteForeignKeys({ table: resolvedTable, config });
|
||||||
|
await syncPrimaryKey({ table: resolvedTable, config });
|
||||||
await syncIndexes({ table: resolvedTable, config });
|
await syncIndexes({ table: resolvedTable, config });
|
||||||
|
await syncUniqueConstraints({ table: resolvedTable, config });
|
||||||
|
await ensureForeignKeys({ table: resolvedTable, config });
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+16
-17
@@ -7,9 +7,9 @@ export default function mapDataType(field) {
|
|||||||
}
|
}
|
||||||
switch (dataType) {
|
switch (dataType) {
|
||||||
case "CHAR":
|
case "CHAR":
|
||||||
return `CHAR(${field.integerLength || 255})`;
|
return `CHAR(${field.dataLength || 255})`;
|
||||||
case "VARCHAR":
|
case "VARCHAR":
|
||||||
return `VARCHAR(${field.integerLength || 255})`;
|
return `VARCHAR(${field.dataLength || 255})`;
|
||||||
case "TEXT":
|
case "TEXT":
|
||||||
return "TEXT";
|
return "TEXT";
|
||||||
case "TINYTEXT":
|
case "TINYTEXT":
|
||||||
@@ -19,36 +19,34 @@ export default function mapDataType(field) {
|
|||||||
case "LONGTEXT":
|
case "LONGTEXT":
|
||||||
return "LONGTEXT";
|
return "LONGTEXT";
|
||||||
case "TINYINT":
|
case "TINYINT":
|
||||||
return field.integerLength
|
return field.dataLength
|
||||||
? `TINYINT(${field.integerLength})`
|
? `TINYINT(${field.dataLength})`
|
||||||
: "TINYINT";
|
: "TINYINT";
|
||||||
case "SMALLINT":
|
case "SMALLINT":
|
||||||
return field.integerLength
|
return field.dataLength
|
||||||
? `SMALLINT(${field.integerLength})`
|
? `SMALLINT(${field.dataLength})`
|
||||||
: "SMALLINT";
|
: "SMALLINT";
|
||||||
case "MEDIUMINT":
|
case "MEDIUMINT":
|
||||||
return field.integerLength
|
return field.dataLength
|
||||||
? `MEDIUMINT(${field.integerLength})`
|
? `MEDIUMINT(${field.dataLength})`
|
||||||
: "MEDIUMINT";
|
: "MEDIUMINT";
|
||||||
case "INT":
|
case "INT":
|
||||||
return field.integerLength ? `INT(${field.integerLength})` : "INT";
|
return field.dataLength ? `INT(${field.dataLength})` : "INT";
|
||||||
case "BIGINT":
|
case "BIGINT":
|
||||||
return field.integerLength
|
return field.dataLength ? `BIGINT(${field.dataLength})` : "BIGINT";
|
||||||
? `BIGINT(${field.integerLength})`
|
|
||||||
: "BIGINT";
|
|
||||||
case "FLOAT":
|
case "FLOAT":
|
||||||
return "FLOAT";
|
return "FLOAT";
|
||||||
case "DOUBLE":
|
case "DOUBLE":
|
||||||
return "DOUBLE";
|
return "DOUBLE";
|
||||||
case "DECIMAL":
|
case "DECIMAL":
|
||||||
if (field.integerLength && field.decimals) {
|
if (field.dataLength && field.decimals) {
|
||||||
return `DECIMAL(${field.integerLength}, ${field.decimals})`;
|
return `DECIMAL(${field.dataLength}, ${field.decimals})`;
|
||||||
}
|
}
|
||||||
return "DECIMAL(10,2)";
|
return "DECIMAL(10,2)";
|
||||||
case "BINARY":
|
case "BINARY":
|
||||||
return `BINARY(${field.integerLength || 1})`;
|
return `BINARY(${field.dataLength || 1})`;
|
||||||
case "VARBINARY":
|
case "VARBINARY":
|
||||||
return `VARBINARY(${field.integerLength || 255})`;
|
return `VARBINARY(${field.dataLength || 255})`;
|
||||||
case "BLOB":
|
case "BLOB":
|
||||||
return "BLOB";
|
return "BLOB";
|
||||||
case "TINYBLOB":
|
case "TINYBLOB":
|
||||||
@@ -68,11 +66,12 @@ export default function mapDataType(field) {
|
|||||||
case "YEAR":
|
case "YEAR":
|
||||||
return "YEAR";
|
return "YEAR";
|
||||||
case "UUID":
|
case "UUID":
|
||||||
return "CHAR(36)"; // MariaDB does not have a native UUID type
|
return "UUID";
|
||||||
case "JSON":
|
case "JSON":
|
||||||
return "JSON";
|
return "JSON";
|
||||||
case "INET6":
|
case "INET6":
|
||||||
return "INET6";
|
return "INET6";
|
||||||
|
case "BOOL":
|
||||||
case "BOOLEAN":
|
case "BOOLEAN":
|
||||||
return "TINYINT(1)";
|
return "TINYINT(1)";
|
||||||
case "ENUM": {
|
case "ENUM": {
|
||||||
|
|||||||
Vendored
-9
@@ -1,9 +0,0 @@
|
|||||||
import type { BUN_MARIADB_TableSchemaType, BunMariaDBConfig } from "../../types";
|
|
||||||
/**
|
|
||||||
* Full table rebuild. For `isVector` tables this drops and recreates in place
|
|
||||||
* (preserving rows when possible). For regular tables it uses a temp-table swap.
|
|
||||||
*/
|
|
||||||
export default function recreateTable({ table, config, }: {
|
|
||||||
table: BUN_MARIADB_TableSchemaType;
|
|
||||||
config?: BunMariaDBConfig;
|
|
||||||
}): Promise<void>;
|
|
||||||
Vendored
-111
@@ -1,111 +0,0 @@
|
|||||||
import createTable from "./create-table";
|
|
||||||
import getTableColumns from "./get-table-columns";
|
|
||||||
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
|
||||||
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
|
||||||
import schemaCondition from "./schema-condition";
|
|
||||||
async function checkIfTableExists({ tableName, config, }) {
|
|
||||||
const schemaCond = schemaCondition(config);
|
|
||||||
const rows = await querySchemaRows({
|
|
||||||
query: `SELECT 1 AS \`table_exists\` FROM information_schema.TABLES WHERE ${schemaCond.where} AND TABLE_NAME = ? LIMIT 1`,
|
|
||||||
values: [...schemaCond.values, tableName],
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
return Boolean(rows[0]?.table_exists);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Full table rebuild. For `isVector` tables this drops and recreates in place
|
|
||||||
* (preserving rows when possible). For regular tables it uses a temp-table swap.
|
|
||||||
*/
|
|
||||||
export default async function recreateTable({ table, config, }) {
|
|
||||||
const doesTableExist = await checkIfTableExists({
|
|
||||||
tableName: table.tableName,
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
if (!doesTableExist) {
|
|
||||||
await createTable({ table, config });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Vector tables: drop + recreate + reinsert (MariaDB VECTOR INDEX / dim
|
|
||||||
* changes are not reliably alterable in place).
|
|
||||||
*/
|
|
||||||
if (table.isVector) {
|
|
||||||
console.log(`Recreating vector table: ${table.tableName}`);
|
|
||||||
const existingRows = await querySchemaRows({
|
|
||||||
query: `SELECT * FROM ${MariaDBQuoteGen(table.tableName)}`,
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
await runSchemaQuery({ query: `SET FOREIGN_KEY_CHECKS = 0`, config });
|
|
||||||
try {
|
|
||||||
await runSchemaQuery({
|
|
||||||
query: `DROP TABLE IF EXISTS ${MariaDBQuoteGen(table.tableName)}`,
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
await createTable({ table, config });
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
await runSchemaQuery({ query: `SET FOREIGN_KEY_CHECKS = 1`, config });
|
|
||||||
}
|
|
||||||
if (existingRows.length > 0) {
|
|
||||||
const schemaFieldNames = new Set((table.fields || [])
|
|
||||||
.map((f) => f.fieldName)
|
|
||||||
.filter((n) => Boolean(n)));
|
|
||||||
for (const row of existingRows) {
|
|
||||||
const columns = Object.keys(row).filter((c) => schemaFieldNames.has(c));
|
|
||||||
if (columns.length === 0)
|
|
||||||
continue;
|
|
||||||
const placeholders = columns.map(() => "?").join(", ");
|
|
||||||
const columnList = columns
|
|
||||||
.map((c) => MariaDBQuoteGen(c))
|
|
||||||
.join(", ");
|
|
||||||
await runSchemaQuery({
|
|
||||||
query: `INSERT INTO ${MariaDBQuoteGen(table.tableName)} (${columnList}) VALUES (${placeholders})`,
|
|
||||||
values: columns.map((c) => row[c] ?? null),
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const tempTableName = `${table.tableName}_temp_${Date.now()}`;
|
|
||||||
const backupOldTableName = `${table.tableName}_old_${Date.now()}`;
|
|
||||||
const existingColumns = await getTableColumns({
|
|
||||||
tableName: table.tableName,
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
const columnsToKeep = (table.fields || [])
|
|
||||||
.filter((field) => existingColumns.some((column) => column.name === field.fieldName))
|
|
||||||
.map((field) => field.fieldName)
|
|
||||||
.filter((fieldName) => Boolean(fieldName));
|
|
||||||
await createTable({
|
|
||||||
table: { ...table, tableName: tempTableName },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
if (columnsToKeep.length > 0) {
|
|
||||||
const columnList = columnsToKeep
|
|
||||||
.map((column) => MariaDBQuoteGen(column))
|
|
||||||
.join(", ");
|
|
||||||
await runSchemaQuery({
|
|
||||||
query: `INSERT INTO ${MariaDBQuoteGen(tempTableName)} (${columnList}) SELECT ${columnList} FROM ${MariaDBQuoteGen(table.tableName)}`,
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await runSchemaQuery({ query: `SET FOREIGN_KEY_CHECKS = 0`, config });
|
|
||||||
try {
|
|
||||||
await runSchemaQuery({
|
|
||||||
query: `RENAME TABLE ${MariaDBQuoteGen(table.tableName)} TO ${MariaDBQuoteGen(backupOldTableName)}`,
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
await runSchemaQuery({
|
|
||||||
query: `RENAME TABLE ${MariaDBQuoteGen(tempTableName)} TO ${MariaDBQuoteGen(table.tableName)}`,
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
await runSchemaQuery({
|
|
||||||
query: `DROP TABLE ${MariaDBQuoteGen(backupOldTableName)}`,
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
await runSchemaQuery({ query: `SET FOREIGN_KEY_CHECKS = 1`, config });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Vendored
-1
@@ -23,7 +23,6 @@ export default function resolveTable(table, db_schema) {
|
|||||||
tableName: table.tableName,
|
tableName: table.tableName,
|
||||||
tableDescription: table.tableDescription || parentTable.tableDescription,
|
tableDescription: table.tableDescription || parentTable.tableDescription,
|
||||||
collation: table.collation || parentTable.collation,
|
collation: table.collation || parentTable.collation,
|
||||||
isVector: table.isVector !== undefined ? table.isVector : parentTable.isVector,
|
|
||||||
fields: Array.from(mergedFieldsMap.values()),
|
fields: Array.from(mergedFieldsMap.values()),
|
||||||
indexes: _.uniqBy([...(parentTable.indexes || []), ...(table.indexes || [])], "indexName"),
|
indexes: _.uniqBy([...(parentTable.indexes || []), ...(table.indexes || [])], "indexName"),
|
||||||
uniqueConstraints: [
|
uniqueConstraints: [
|
||||||
|
|||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
import type { BUN_MARIADB_FieldSchemaType, BUN_MARIADB_TableSchemaType, BunMariaDBConfig } from "../../types";
|
||||||
|
export type DesiredForeignKey = {
|
||||||
|
name: string;
|
||||||
|
column: string;
|
||||||
|
refTable: string;
|
||||||
|
refColumn: string;
|
||||||
|
cascadeDelete: boolean;
|
||||||
|
cascadeUpdate: boolean;
|
||||||
|
field: BUN_MARIADB_FieldSchemaType;
|
||||||
|
};
|
||||||
|
export type LiveForeignKey = {
|
||||||
|
name: string;
|
||||||
|
column: string;
|
||||||
|
refTable: string;
|
||||||
|
refColumn: string;
|
||||||
|
deleteRule: string;
|
||||||
|
updateRule: string;
|
||||||
|
};
|
||||||
|
export declare function grabDesiredForeignKeys(table: BUN_MARIADB_TableSchemaType): DesiredForeignKey[];
|
||||||
|
export declare function grabLiveForeignKeys({ tableName, config, }: {
|
||||||
|
tableName: string;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<LiveForeignKey[]>;
|
||||||
|
export declare function dropForeignKey({ tableName, constraintName, config, }: {
|
||||||
|
tableName: string;
|
||||||
|
constraintName: string;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void>;
|
||||||
|
export declare function dropForeignKeysOnColumns({ tableName, columns, config, }: {
|
||||||
|
tableName: string;
|
||||||
|
columns: string[];
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Drop foreign keys that are removed from schema or need recreation
|
||||||
|
* (name/cascade/target changed). Run before index cleanup.
|
||||||
|
*/
|
||||||
|
export declare function dropObsoleteForeignKeys({ table, config, }: {
|
||||||
|
table: BUN_MARIADB_TableSchemaType;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Ensure all desired foreign keys exist. Run after indexes/uniques.
|
||||||
|
*/
|
||||||
|
export declare function ensureForeignKeys({ table, config, }: {
|
||||||
|
table: BUN_MARIADB_TableSchemaType;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void>;
|
||||||
|
export default function syncForeignKeys({ table, config, }: {
|
||||||
|
table: BUN_MARIADB_TableSchemaType;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void>;
|
||||||
Vendored
+180
@@ -0,0 +1,180 @@
|
|||||||
|
import buildForeignKeyConstraint, { resolveForeignKeyName, } from "./build-foreign-key-constraint";
|
||||||
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
|
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
||||||
|
function normalizeRule(rule) {
|
||||||
|
return String(rule || "RESTRICT").toUpperCase().replace(/_/g, " ");
|
||||||
|
}
|
||||||
|
function ruleIsCascade(rule) {
|
||||||
|
return normalizeRule(rule) === "CASCADE";
|
||||||
|
}
|
||||||
|
function fkIdentityKey(fk) {
|
||||||
|
return `${fk.column}\0${fk.refTable}\0${fk.refColumn}`;
|
||||||
|
}
|
||||||
|
function rulesMatch(live, desired) {
|
||||||
|
return (ruleIsCascade(live.deleteRule) === desired.cascadeDelete &&
|
||||||
|
ruleIsCascade(live.updateRule) === desired.cascadeUpdate);
|
||||||
|
}
|
||||||
|
export function grabDesiredForeignKeys(table) {
|
||||||
|
const desired = [];
|
||||||
|
for (const field of table.fields || []) {
|
||||||
|
const fk = field.foreignKey;
|
||||||
|
if (!field.fieldName ||
|
||||||
|
!fk?.destinationTableName ||
|
||||||
|
!fk.destinationTableColumnName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
desired.push({
|
||||||
|
name: resolveForeignKeyName(field, table.tableName),
|
||||||
|
column: field.fieldName,
|
||||||
|
refTable: fk.destinationTableName,
|
||||||
|
refColumn: fk.destinationTableColumnName,
|
||||||
|
cascadeDelete: Boolean(fk.cascadeDelete),
|
||||||
|
cascadeUpdate: Boolean(fk.cascadeUpdate),
|
||||||
|
field,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return desired;
|
||||||
|
}
|
||||||
|
export async function grabLiveForeignKeys({ tableName, config, }) {
|
||||||
|
const databaseName = config?.db_name || global.CONFIG?.db_name;
|
||||||
|
const schemaWhere = databaseName
|
||||||
|
? "rc.CONSTRAINT_SCHEMA = ?"
|
||||||
|
: "rc.CONSTRAINT_SCHEMA = DATABASE()";
|
||||||
|
const schemaValues = databaseName ? [databaseName] : [];
|
||||||
|
const rows = await querySchemaRows({
|
||||||
|
query: `
|
||||||
|
SELECT
|
||||||
|
rc.CONSTRAINT_NAME,
|
||||||
|
kcu.COLUMN_NAME,
|
||||||
|
kcu.REFERENCED_TABLE_NAME,
|
||||||
|
kcu.REFERENCED_COLUMN_NAME,
|
||||||
|
rc.DELETE_RULE,
|
||||||
|
rc.UPDATE_RULE
|
||||||
|
FROM information_schema.REFERENTIAL_CONSTRAINTS rc
|
||||||
|
INNER JOIN information_schema.KEY_COLUMN_USAGE kcu
|
||||||
|
ON rc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA
|
||||||
|
AND rc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME
|
||||||
|
AND rc.TABLE_NAME = kcu.TABLE_NAME
|
||||||
|
WHERE ${schemaWhere}
|
||||||
|
AND rc.TABLE_NAME = ?
|
||||||
|
AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
|
||||||
|
ORDER BY rc.CONSTRAINT_NAME, kcu.ORDINAL_POSITION
|
||||||
|
`,
|
||||||
|
values: [...schemaValues, tableName],
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
// Single-column FKs only (schema model is one FK per field)
|
||||||
|
const byName = new Map();
|
||||||
|
for (const row of rows) {
|
||||||
|
if (byName.has(row.CONSTRAINT_NAME)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
byName.set(row.CONSTRAINT_NAME, {
|
||||||
|
name: row.CONSTRAINT_NAME,
|
||||||
|
column: row.COLUMN_NAME,
|
||||||
|
refTable: row.REFERENCED_TABLE_NAME,
|
||||||
|
refColumn: row.REFERENCED_COLUMN_NAME,
|
||||||
|
deleteRule: row.DELETE_RULE,
|
||||||
|
updateRule: row.UPDATE_RULE,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return [...byName.values()];
|
||||||
|
}
|
||||||
|
export async function dropForeignKey({ tableName, constraintName, config, }) {
|
||||||
|
console.log(`Dropping foreign key: ${constraintName} on ${tableName}`);
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `ALTER TABLE ${MariaDBQuoteGen(tableName)} DROP FOREIGN KEY ${MariaDBQuoteGen(constraintName)}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export async function dropForeignKeysOnColumns({ tableName, columns, config, }) {
|
||||||
|
if (columns.length === 0)
|
||||||
|
return;
|
||||||
|
const live = await grabLiveForeignKeys({ tableName, config });
|
||||||
|
const colSet = new Set(columns);
|
||||||
|
for (const fk of live) {
|
||||||
|
if (colSet.has(fk.column)) {
|
||||||
|
await dropForeignKey({
|
||||||
|
tableName,
|
||||||
|
constraintName: fk.name,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Drop foreign keys that are removed from schema or need recreation
|
||||||
|
* (name/cascade/target changed). Run before index cleanup.
|
||||||
|
*/
|
||||||
|
export async function dropObsoleteForeignKeys({ table, config, }) {
|
||||||
|
const desired = grabDesiredForeignKeys(table);
|
||||||
|
const live = await grabLiveForeignKeys({
|
||||||
|
tableName: table.tableName,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
const keepNames = new Set();
|
||||||
|
const droppedNames = new Set();
|
||||||
|
const dropOnce = async (name) => {
|
||||||
|
if (droppedNames.has(name))
|
||||||
|
return;
|
||||||
|
droppedNames.add(name);
|
||||||
|
await dropForeignKey({
|
||||||
|
tableName: table.tableName,
|
||||||
|
constraintName: name,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
for (const want of desired) {
|
||||||
|
const byIdentity = live.find((existing) => !keepNames.has(existing.name) &&
|
||||||
|
!droppedNames.has(existing.name) &&
|
||||||
|
fkIdentityKey(existing) === fkIdentityKey(want));
|
||||||
|
if (byIdentity &&
|
||||||
|
rulesMatch(byIdentity, want) &&
|
||||||
|
byIdentity.name === want.name) {
|
||||||
|
keepNames.add(byIdentity.name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Changed relationship → drop so ensureForeignKeys can recreate
|
||||||
|
if (byIdentity) {
|
||||||
|
await dropOnce(byIdentity.name);
|
||||||
|
}
|
||||||
|
const byName = live.find((existing) => existing.name === want.name);
|
||||||
|
if (byName && byName !== byIdentity) {
|
||||||
|
await dropOnce(byName.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const existing of live) {
|
||||||
|
if (keepNames.has(existing.name) || droppedNames.has(existing.name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Not in schema anymore
|
||||||
|
await dropOnce(existing.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Ensure all desired foreign keys exist. Run after indexes/uniques.
|
||||||
|
*/
|
||||||
|
export async function ensureForeignKeys({ table, config, }) {
|
||||||
|
const desired = grabDesiredForeignKeys(table);
|
||||||
|
const live = await grabLiveForeignKeys({
|
||||||
|
tableName: table.tableName,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
for (const want of desired) {
|
||||||
|
const exists = live.some((existing) => fkIdentityKey(existing) === fkIdentityKey(want) &&
|
||||||
|
rulesMatch(existing, want) &&
|
||||||
|
existing.name === want.name);
|
||||||
|
if (exists) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
console.log(`Creating foreign key: ${want.name} (${want.column} → ${want.refTable}.${want.refColumn}) on ${table.tableName}`);
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} ADD ${buildForeignKeyConstraint(want.field, table.tableName)}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default async function syncForeignKeys({ table, config, }) {
|
||||||
|
await dropObsoleteForeignKeys({ table, config });
|
||||||
|
await ensureForeignKeys({ table, config });
|
||||||
|
}
|
||||||
Vendored
+61
-39
@@ -2,11 +2,10 @@ import isVectorField from "./is-vector-field";
|
|||||||
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
||||||
import schemaCondition from "./schema-condition";
|
import schemaCondition from "./schema-condition";
|
||||||
|
import { grabDesiredUniqueConstraints } from "./sync-unique-constraints";
|
||||||
function isVectorIndexDef(index, table) {
|
function isVectorIndexDef(index, table) {
|
||||||
if (index.indexType === "VECTOR")
|
if (index.indexType === "VECTOR")
|
||||||
return true;
|
return true;
|
||||||
if (table.isVector)
|
|
||||||
return true;
|
|
||||||
const firstFieldName = index.indexTableFields?.[0];
|
const firstFieldName = index.indexTableFields?.[0];
|
||||||
if (!firstFieldName)
|
if (!firstFieldName)
|
||||||
return false;
|
return false;
|
||||||
@@ -20,6 +19,60 @@ function vectorDistanceMetric(index) {
|
|||||||
return "euclidean";
|
return "euclidean";
|
||||||
return "euclidean";
|
return "euclidean";
|
||||||
}
|
}
|
||||||
|
/** Normalize schema index type vs information_schema.INDEX_TYPE */
|
||||||
|
function indexTypesMatch(liveType, schemaIndex, table) {
|
||||||
|
const live = (liveType || "").toUpperCase();
|
||||||
|
const schemaType = schemaIndex.indexType?.toUpperCase();
|
||||||
|
if (isVectorIndexDef(schemaIndex, table)) {
|
||||||
|
// MariaDB may report VECTOR indexes as BTREE or VECTOR depending on version
|
||||||
|
return live === "VECTOR" || live === "BTREE" || live === "";
|
||||||
|
}
|
||||||
|
if (!schemaType || schemaType === "BTREE") {
|
||||||
|
// Default / unspecified → BTREE
|
||||||
|
return live === "BTREE" || live === "";
|
||||||
|
}
|
||||||
|
if (schemaType === "FULLTEXT") {
|
||||||
|
return live === "FULLTEXT";
|
||||||
|
}
|
||||||
|
if (schemaType === "SPATIAL") {
|
||||||
|
return live === "SPATIAL";
|
||||||
|
}
|
||||||
|
if (schemaType === "HASH") {
|
||||||
|
return live === "HASH";
|
||||||
|
}
|
||||||
|
return live === schemaType;
|
||||||
|
}
|
||||||
|
async function createIndex({ table, index, config, }) {
|
||||||
|
if (!index.indexName ||
|
||||||
|
!index.indexTableFields ||
|
||||||
|
index.indexTableFields.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isVectorIndexDef(index, table)) {
|
||||||
|
console.log(`Creating Vector index: ${index.indexName}`);
|
||||||
|
const targetField = MariaDBQuoteGen(index.indexTableFields[0]);
|
||||||
|
const distanceMetric = vectorDistanceMetric(index);
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} ADD VECTOR INDEX ${MariaDBQuoteGen(index.indexName)} (${targetField}) M=8 DISTANCE=${distanceMetric}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`Creating standard index: ${index.indexName}`);
|
||||||
|
const fields = index.indexTableFields
|
||||||
|
.map((field) => MariaDBQuoteGen(field))
|
||||||
|
.join(", ");
|
||||||
|
const typeUpper = index.indexType?.toUpperCase();
|
||||||
|
const isSpecialType = typeUpper === "FULLTEXT" || typeUpper === "SPATIAL";
|
||||||
|
const indexPrefix = isSpecialType ? `${typeUpper} ` : "";
|
||||||
|
const indexSuffix = !isSpecialType && (typeUpper === "BTREE" || typeUpper === "HASH")
|
||||||
|
? ` USING ${typeUpper}`
|
||||||
|
: "";
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `CREATE ${indexPrefix}INDEX ${MariaDBQuoteGen(index.indexName)} ON ${MariaDBQuoteGen(table.tableName)} (${fields})${indexSuffix}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
export default async function syncIndexes({ table, config, }) {
|
export default async function syncIndexes({ table, config, }) {
|
||||||
const schemaCond = schemaCondition(config);
|
const schemaCond = schemaCondition(config);
|
||||||
const rows = await querySchemaRows({
|
const rows = await querySchemaRows({
|
||||||
@@ -37,16 +90,8 @@ export default async function syncIndexes({ table, config, }) {
|
|||||||
config,
|
config,
|
||||||
});
|
});
|
||||||
const protectedIndexNames = new Set(protectedConstraintRows.map((r) => r.CONSTRAINT_NAME));
|
const protectedIndexNames = new Set(protectedConstraintRows.map((r) => r.CONSTRAINT_NAME));
|
||||||
// Column-level UNIQUE creates an index often named after the column
|
for (const constraint of grabDesiredUniqueConstraints(table)) {
|
||||||
for (const field of table.fields || []) {
|
protectedIndexNames.add(constraint.name);
|
||||||
if (field.unique && field.fieldName) {
|
|
||||||
protectedIndexNames.add(field.fieldName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const constraint of table.uniqueConstraints || []) {
|
|
||||||
if (constraint.constraintName) {
|
|
||||||
protectedIndexNames.add(constraint.constraintName);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const existingIndexesMap = new Map();
|
const existingIndexesMap = new Map();
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
@@ -70,6 +115,7 @@ export default async function syncIndexes({ table, config, }) {
|
|||||||
query: `DROP INDEX ${MariaDBQuoteGen(indexName)} ON ${MariaDBQuoteGen(table.tableName)}`,
|
query: `DROP INDEX ${MariaDBQuoteGen(indexName)} ON ${MariaDBQuoteGen(table.tableName)}`,
|
||||||
config,
|
config,
|
||||||
});
|
});
|
||||||
|
existingIndexesMap.delete(indexName);
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
if (String(err?.message || "").includes("needed in a foreign key constraint")) {
|
if (String(err?.message || "").includes("needed in a foreign key constraint")) {
|
||||||
@@ -83,7 +129,8 @@ export default async function syncIndexes({ table, config, }) {
|
|||||||
const schemaColumns = schemaIndex.indexTableFields || [];
|
const schemaColumns = schemaIndex.indexTableFields || [];
|
||||||
const columnsMatch = details.columns.length === schemaColumns.length &&
|
const columnsMatch = details.columns.length === schemaColumns.length &&
|
||||||
details.columns.every((col, idx) => col === schemaColumns[idx]);
|
details.columns.every((col, idx) => col === schemaColumns[idx]);
|
||||||
if (!columnsMatch) {
|
const typeMatch = indexTypesMatch(details.type, schemaIndex, table);
|
||||||
|
if (!columnsMatch || !typeMatch) {
|
||||||
console.log(`Recreating changed index: ${indexName}`);
|
console.log(`Recreating changed index: ${indexName}`);
|
||||||
await runSchemaQuery({
|
await runSchemaQuery({
|
||||||
query: `DROP INDEX ${MariaDBQuoteGen(indexName)} ON ${MariaDBQuoteGen(table.tableName)}`,
|
query: `DROP INDEX ${MariaDBQuoteGen(indexName)} ON ${MariaDBQuoteGen(table.tableName)}`,
|
||||||
@@ -100,32 +147,7 @@ export default async function syncIndexes({ table, config, }) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!existingIndexesMap.has(index.indexName)) {
|
if (!existingIndexesMap.has(index.indexName)) {
|
||||||
if (isVectorIndexDef(index, table)) {
|
await createIndex({ table, index, config });
|
||||||
console.log(`Creating Vector index: ${index.indexName}`);
|
|
||||||
const targetField = MariaDBQuoteGen(index.indexTableFields[0]);
|
|
||||||
const distanceMetric = vectorDistanceMetric(index);
|
|
||||||
await runSchemaQuery({
|
|
||||||
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} ADD VECTOR INDEX ${MariaDBQuoteGen(index.indexName)} (${targetField}) M=8 DISTANCE=${distanceMetric}`,
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.log(`Creating standard index: ${index.indexName}`);
|
|
||||||
const fields = index.indexTableFields
|
|
||||||
.map((field) => MariaDBQuoteGen(field))
|
|
||||||
.join(", ");
|
|
||||||
const typeUpper = index.indexType?.toUpperCase();
|
|
||||||
const isSpecialType = typeUpper === "FULLTEXT" || typeUpper === "SPATIAL";
|
|
||||||
const indexPrefix = isSpecialType ? `${typeUpper} ` : "";
|
|
||||||
const indexSuffix = !isSpecialType &&
|
|
||||||
(typeUpper === "BTREE" || typeUpper === "HASH")
|
|
||||||
? ` USING ${typeUpper}`
|
|
||||||
: "";
|
|
||||||
await runSchemaQuery({
|
|
||||||
query: `CREATE ${indexPrefix}INDEX ${MariaDBQuoteGen(index.indexName)} ON ${MariaDBQuoteGen(table.tableName)} (${fields})${indexSuffix}`,
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
import type { BUN_MARIADB_TableSchemaType, BunMariaDBConfig } from "../../types";
|
||||||
|
export declare function grabDesiredPrimaryKeyColumns(table: BUN_MARIADB_TableSchemaType): string[];
|
||||||
|
export default function syncPrimaryKey({ table, config, }: {
|
||||||
|
table: BUN_MARIADB_TableSchemaType;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void>;
|
||||||
Vendored
+52
@@ -0,0 +1,52 @@
|
|||||||
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
|
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
||||||
|
import schemaCondition from "./schema-condition";
|
||||||
|
function columnsEqual(a, b) {
|
||||||
|
return a.length === b.length && a.every((col, i) => col === b[i]);
|
||||||
|
}
|
||||||
|
export function grabDesiredPrimaryKeyColumns(table) {
|
||||||
|
return (table.fields || [])
|
||||||
|
.filter((field) => field.primaryKey && field.fieldName)
|
||||||
|
.map((field) => field.fieldName);
|
||||||
|
}
|
||||||
|
async function grabLivePrimaryKeyColumns({ tableName, config, }) {
|
||||||
|
const schemaCond = schemaCondition(config);
|
||||||
|
const rows = await querySchemaRows({
|
||||||
|
query: `
|
||||||
|
SELECT COLUMN_NAME, ORDINAL_POSITION
|
||||||
|
FROM information_schema.KEY_COLUMN_USAGE
|
||||||
|
WHERE ${schemaCond.where}
|
||||||
|
AND TABLE_NAME = ?
|
||||||
|
AND CONSTRAINT_NAME = 'PRIMARY'
|
||||||
|
ORDER BY ORDINAL_POSITION
|
||||||
|
`,
|
||||||
|
values: [...schemaCond.values, tableName],
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
return rows.map((row) => row.COLUMN_NAME);
|
||||||
|
}
|
||||||
|
export default async function syncPrimaryKey({ table, config, }) {
|
||||||
|
const desired = grabDesiredPrimaryKeyColumns(table);
|
||||||
|
const live = await grabLivePrimaryKeyColumns({
|
||||||
|
tableName: table.tableName,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
if (columnsEqual(desired, live)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (live.length > 0) {
|
||||||
|
console.log(`Dropping primary key on ${table.tableName}`);
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} DROP PRIMARY KEY`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (desired.length > 0) {
|
||||||
|
const cols = desired.map((col) => MariaDBQuoteGen(col)).join(", ");
|
||||||
|
console.log(`Creating primary key (${desired.join(", ")}) on ${table.tableName}`);
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} ADD PRIMARY KEY (${cols})`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
import type { BUN_MARIADB_TableSchemaType, BunMariaDBConfig } from "../../types";
|
||||||
|
export default function syncTableOptions({ table, config, }: {
|
||||||
|
table: BUN_MARIADB_TableSchemaType;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void>;
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
|
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
||||||
|
import schemaCondition from "./schema-condition";
|
||||||
|
export default async function syncTableOptions({ table, config, }) {
|
||||||
|
const schemaCond = schemaCondition(config);
|
||||||
|
const rows = await querySchemaRows({
|
||||||
|
query: `SELECT ENGINE, TABLE_COLLATION FROM information_schema.TABLES WHERE ${schemaCond.where} AND TABLE_NAME = ? AND TABLE_TYPE = 'BASE TABLE'`,
|
||||||
|
values: [...schemaCond.values, table.tableName],
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
const live = rows[0];
|
||||||
|
if (!live) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const alters = [];
|
||||||
|
if ((live.ENGINE || "").toUpperCase() !== "INNODB") {
|
||||||
|
alters.push("ENGINE=InnoDB");
|
||||||
|
}
|
||||||
|
if (table.collation) {
|
||||||
|
const liveCollation = (live.TABLE_COLLATION || "").toLowerCase();
|
||||||
|
if (liveCollation !== table.collation.toLowerCase()) {
|
||||||
|
alters.push(`CONVERT TO CHARACTER SET utf8mb4 COLLATE ${table.collation}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (alters.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`Updating table options on ${table.tableName}: ${alters.join(", ")}`);
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} ${alters.join(", ")}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
import type { BUN_MARIADB_TableSchemaType, BunMariaDBConfig } from "../../types";
|
||||||
|
type UniqueConstraintDesired = {
|
||||||
|
name: string;
|
||||||
|
columns: string[];
|
||||||
|
};
|
||||||
|
export declare function grabDesiredUniqueConstraints(table: BUN_MARIADB_TableSchemaType): UniqueConstraintDesired[];
|
||||||
|
export default function syncUniqueConstraints({ table, config, }: {
|
||||||
|
table: BUN_MARIADB_TableSchemaType;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void>;
|
||||||
|
export {};
|
||||||
+138
@@ -0,0 +1,138 @@
|
|||||||
|
import isVectorField from "./is-vector-field";
|
||||||
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
|
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
||||||
|
function columnsKey(columns) {
|
||||||
|
return columns.join("\0");
|
||||||
|
}
|
||||||
|
function columnsEqual(a, b) {
|
||||||
|
return a.length === b.length && a.every((col, i) => col === b[i]);
|
||||||
|
}
|
||||||
|
function defaultConstraintName(columns) {
|
||||||
|
return `unique_${columns.join("_")}`;
|
||||||
|
}
|
||||||
|
export function grabDesiredUniqueConstraints(table) {
|
||||||
|
const desired = [];
|
||||||
|
const seenColumnSets = new Set();
|
||||||
|
for (const constraint of table.uniqueConstraints || []) {
|
||||||
|
const columns = (constraint.constraintTableFields || [])
|
||||||
|
.map((field) => field.value)
|
||||||
|
.filter((value) => Boolean(value));
|
||||||
|
if (columns.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const key = columnsKey(columns);
|
||||||
|
if (seenColumnSets.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seenColumnSets.add(key);
|
||||||
|
desired.push({
|
||||||
|
name: constraint.constraintName || defaultConstraintName(columns),
|
||||||
|
columns,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const field of table.fields || []) {
|
||||||
|
if (!field.fieldName ||
|
||||||
|
!field.unique ||
|
||||||
|
field.primaryKey ||
|
||||||
|
isVectorField(field)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const columns = [field.fieldName];
|
||||||
|
const key = columnsKey(columns);
|
||||||
|
if (seenColumnSets.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seenColumnSets.add(key);
|
||||||
|
// Matches MariaDB's default name for column-level UNIQUE
|
||||||
|
desired.push({
|
||||||
|
name: field.fieldName,
|
||||||
|
columns,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return desired;
|
||||||
|
}
|
||||||
|
async function grabLiveUniqueConstraints({ tableName, config, }) {
|
||||||
|
const databaseName = config?.db_name || global.CONFIG?.db_name;
|
||||||
|
const schemaWhere = databaseName
|
||||||
|
? "tc.TABLE_SCHEMA = ?"
|
||||||
|
: "tc.TABLE_SCHEMA = DATABASE()";
|
||||||
|
const schemaValues = databaseName ? [databaseName] : [];
|
||||||
|
const rows = await querySchemaRows({
|
||||||
|
query: `
|
||||||
|
SELECT tc.CONSTRAINT_NAME, kcu.COLUMN_NAME, kcu.ORDINAL_POSITION
|
||||||
|
FROM information_schema.TABLE_CONSTRAINTS tc
|
||||||
|
INNER JOIN information_schema.KEY_COLUMN_USAGE kcu
|
||||||
|
ON tc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA
|
||||||
|
AND tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME
|
||||||
|
AND tc.TABLE_NAME = kcu.TABLE_NAME
|
||||||
|
WHERE ${schemaWhere}
|
||||||
|
AND tc.TABLE_NAME = ?
|
||||||
|
AND tc.CONSTRAINT_TYPE = 'UNIQUE'
|
||||||
|
ORDER BY tc.CONSTRAINT_NAME, kcu.ORDINAL_POSITION
|
||||||
|
`,
|
||||||
|
values: [...schemaValues, tableName],
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
const map = new Map();
|
||||||
|
for (const row of rows) {
|
||||||
|
if (!map.has(row.CONSTRAINT_NAME)) {
|
||||||
|
map.set(row.CONSTRAINT_NAME, []);
|
||||||
|
}
|
||||||
|
map.get(row.CONSTRAINT_NAME).push(row.COLUMN_NAME);
|
||||||
|
}
|
||||||
|
return [...map.entries()].map(([name, columns]) => ({ name, columns }));
|
||||||
|
}
|
||||||
|
export default async function syncUniqueConstraints({ table, config, }) {
|
||||||
|
const desired = grabDesiredUniqueConstraints(table);
|
||||||
|
const live = await grabLiveUniqueConstraints({
|
||||||
|
tableName: table.tableName,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
const matchedLiveNames = new Set();
|
||||||
|
for (const want of desired) {
|
||||||
|
const byColumns = live.find((existing) => !matchedLiveNames.has(existing.name) &&
|
||||||
|
columnsEqual(existing.columns, want.columns));
|
||||||
|
if (byColumns) {
|
||||||
|
matchedLiveNames.add(byColumns.name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const byName = live.find((existing) => existing.name === want.name);
|
||||||
|
if (byName && !matchedLiveNames.has(byName.name)) {
|
||||||
|
console.log(`Recreating changed unique constraint: ${want.name} on ${table.tableName}`);
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} DROP INDEX ${MariaDBQuoteGen(byName.name)}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
matchedLiveNames.add(byName.name);
|
||||||
|
}
|
||||||
|
const fields = want.columns.map((col) => MariaDBQuoteGen(col)).join(", ");
|
||||||
|
console.log(`Creating unique constraint: ${want.name} (${want.columns.join(", ")}) on ${table.tableName}`);
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} ADD CONSTRAINT ${MariaDBQuoteGen(want.name)} UNIQUE (${fields})`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const existing of live) {
|
||||||
|
if (matchedLiveNames.has(existing.name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const stillDesired = desired.some((want) => columnsEqual(want.columns, existing.columns));
|
||||||
|
if (stillDesired) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
console.log(`Dropping unique constraint: ${existing.name} on ${table.tableName}`);
|
||||||
|
try {
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} DROP INDEX ${MariaDBQuoteGen(existing.name)}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
if (String(err?.message || "").includes("needed in a foreign key constraint")) {
|
||||||
|
console.warn(`Skipping drop of unique constraint ${existing.name}: required by a foreign key constraint`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+171
-32
@@ -1,12 +1,13 @@
|
|||||||
import buildColumnDefinition from "./build-column-definition";
|
import buildColumnDefinition, { fieldRequiresNotNull, } from "./build-column-definition";
|
||||||
import createTable from "./create-table";
|
import createTable from "./create-table";
|
||||||
import getTableColumns from "./get-table-columns";
|
import getTableColumns, {} from "./get-table-columns";
|
||||||
|
import getTableColumnsGemini from "./get-table-columns-gemnini";
|
||||||
import isVectorField from "./is-vector-field";
|
import isVectorField from "./is-vector-field";
|
||||||
import mapDataType from "./map-data-types";
|
import mapDataType from "./map-data-types";
|
||||||
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
import recreateTable from "./recreate-table";
|
|
||||||
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
||||||
import schemaCondition from "./schema-condition";
|
import schemaCondition from "./schema-condition";
|
||||||
|
import { dropForeignKeysOnColumns } from "./sync-foreign-keys";
|
||||||
/**
|
/**
|
||||||
* Compare live COLUMN_TYPE with schema-mapped type.
|
* Compare live COLUMN_TYPE with schema-mapped type.
|
||||||
* Live types often include display widths (e.g. bigint(20) vs BIGINT).
|
* Live types often include display widths (e.g. bigint(20) vs BIGINT).
|
||||||
@@ -41,9 +42,87 @@ function vectorTypeDiverged(liveType, liveComment, field) {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
function normalizeDefault(value) {
|
||||||
|
if (value == null)
|
||||||
|
return "";
|
||||||
|
let v = String(value).trim();
|
||||||
|
// Strip surrounding quotes MariaDB may include
|
||||||
|
if ((v.startsWith("'") && v.endsWith("'")) ||
|
||||||
|
(v.startsWith('"') && v.endsWith('"'))) {
|
||||||
|
v = v.slice(1, -1);
|
||||||
|
}
|
||||||
|
return v.toLowerCase().replace(/\s+/g, " ");
|
||||||
|
}
|
||||||
|
function expectedDefault(field) {
|
||||||
|
if (field.defaultValue !== undefined) {
|
||||||
|
return String(field.defaultValue);
|
||||||
|
}
|
||||||
|
if (field.defaultValueLiteral) {
|
||||||
|
return field.defaultValueLiteral;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function expectedOnUpdate(field) {
|
||||||
|
if (field.onUpdate)
|
||||||
|
return field.onUpdate;
|
||||||
|
if (field.onUpdateLiteral)
|
||||||
|
return field.onUpdateLiteral;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function defaultsMatch(liveDefault, expected) {
|
||||||
|
if (liveDefault === expected)
|
||||||
|
return true;
|
||||||
|
if (expected.includes("current_timestamp") &&
|
||||||
|
liveDefault.includes("current_timestamp")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function columnAttributesDiverged(live, field) {
|
||||||
|
const wantsNotNull = fieldRequiresNotNull(field);
|
||||||
|
if (wantsNotNull && live.isNullable) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!wantsNotNull && !live.isNullable && !field.primaryKey) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const liveExtra = (live.extra || "").toLowerCase();
|
||||||
|
const wantsAi = Boolean(field.autoIncrement);
|
||||||
|
const hasAi = liveExtra.includes("auto_increment");
|
||||||
|
if (wantsAi !== hasAi) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const wantsDefault = expectedDefault(field);
|
||||||
|
if (wantsDefault != null) {
|
||||||
|
const liveDefault = normalizeDefault(live.columnDefault);
|
||||||
|
const expected = normalizeDefault(wantsDefault);
|
||||||
|
if (!defaultsMatch(liveDefault, expected)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const wantsOnUpdate = expectedOnUpdate(field);
|
||||||
|
const hasOnUpdate = /on update/i.test(live.extra || "");
|
||||||
|
if (wantsOnUpdate && !hasOnUpdate) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!wantsOnUpdate && hasOnUpdate) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (wantsOnUpdate && hasOnUpdate) {
|
||||||
|
const liveOnUpdate = normalizeDefault((live.extra.match(/on update\s+(.+)/i) || [])[1] || "");
|
||||||
|
const expectedOu = normalizeDefault(wantsOnUpdate);
|
||||||
|
if (liveOnUpdate &&
|
||||||
|
expectedOu &&
|
||||||
|
!defaultsMatch(liveOnUpdate, expectedOu)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
async function addColumn({ tableName, field, config, }) {
|
async function addColumn({ tableName, field, config, }) {
|
||||||
console.log(`Adding column: ${tableName}.${field.fieldName}`);
|
console.log(`Adding column: ${tableName}.${field.fieldName}`);
|
||||||
const columnDef = buildColumnDefinition(field).trim();
|
// omitUnique — unique constraints are applied by syncUniqueConstraints
|
||||||
|
const columnDef = buildColumnDefinition(field, { omitUnique: true }).trim();
|
||||||
await runSchemaQuery({
|
await runSchemaQuery({
|
||||||
query: `ALTER TABLE ${MariaDBQuoteGen(tableName)} ADD COLUMN IF NOT EXISTS ${columnDef}`,
|
query: `ALTER TABLE ${MariaDBQuoteGen(tableName)} ADD COLUMN IF NOT EXISTS ${columnDef}`,
|
||||||
config,
|
config,
|
||||||
@@ -51,7 +130,7 @@ async function addColumn({ tableName, field, config, }) {
|
|||||||
}
|
}
|
||||||
async function modifyColumn({ tableName, field, config, }) {
|
async function modifyColumn({ tableName, field, config, }) {
|
||||||
console.log(`Modifying column: ${tableName}.${field.fieldName}`);
|
console.log(`Modifying column: ${tableName}.${field.fieldName}`);
|
||||||
const columnDef = buildColumnDefinition(field).trim();
|
const columnDef = buildColumnDefinition(field, { omitUnique: true }).trim();
|
||||||
await runSchemaQuery({
|
await runSchemaQuery({
|
||||||
query: `ALTER TABLE ${MariaDBQuoteGen(tableName)} MODIFY COLUMN ${columnDef}`,
|
query: `ALTER TABLE ${MariaDBQuoteGen(tableName)} MODIFY COLUMN ${columnDef}`,
|
||||||
config,
|
config,
|
||||||
@@ -64,8 +143,52 @@ async function dropColumn({ tableName, fieldName, config, }) {
|
|||||||
config,
|
config,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Drop + re-add a column (values discarded). Used when VECTOR dimensions change —
|
||||||
|
* MODIFY cannot resize VECTOR, and a full table rebuild is unnecessary.
|
||||||
|
*/
|
||||||
|
async function recreateColumn({ tableName, field, config, }) {
|
||||||
|
if (!field.fieldName)
|
||||||
|
return;
|
||||||
|
console.log(`Recreating column: ${tableName}.${field.fieldName} (values will be cleared)`);
|
||||||
|
await dropForeignKeysOnColumns({
|
||||||
|
tableName,
|
||||||
|
columns: [field.fieldName],
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
const schemaCond = schemaCondition(config);
|
||||||
|
const indexRows = await querySchemaRows({
|
||||||
|
query: `SELECT INDEX_NAME, COLUMN_NAME FROM information_schema.STATISTICS WHERE ${schemaCond.where} AND TABLE_NAME = ? AND INDEX_NAME <> 'PRIMARY'`,
|
||||||
|
values: [...schemaCond.values, tableName],
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
const indexesToDrop = new Set();
|
||||||
|
for (const row of indexRows) {
|
||||||
|
if (row.COLUMN_NAME === field.fieldName) {
|
||||||
|
indexesToDrop.add(row.INDEX_NAME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const indexName of indexesToDrop) {
|
||||||
|
console.log(`Dropping index ${indexName} because column ${field.fieldName} is being recreated`);
|
||||||
|
try {
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `DROP INDEX ${MariaDBQuoteGen(indexName)} ON ${MariaDBQuoteGen(tableName)}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
if (String(err?.message || "").includes("needed in a foreign key constraint")) {
|
||||||
|
console.warn(`Skipping drop of index ${indexName}: required by a foreign key constraint`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await dropColumn({ tableName, fieldName: field.fieldName, config });
|
||||||
|
await addColumn({ tableName, field, config });
|
||||||
|
}
|
||||||
export default async function updateTable({ table, config, }) {
|
export default async function updateTable({ table, config, }) {
|
||||||
const existingColumns = await getTableColumns({
|
const existingColumns = await getTableColumnsGemini({
|
||||||
tableName: table.tableName,
|
tableName: table.tableName,
|
||||||
config,
|
config,
|
||||||
});
|
});
|
||||||
@@ -73,43 +196,36 @@ export default async function updateTable({ table, config, }) {
|
|||||||
await createTable({ table, config });
|
await createTable({ table, config });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const liveFieldsMap = new Map(existingColumns.map((col) => [
|
const liveFieldsMap = new Map(existingColumns.map((col) => [col.name, col]));
|
||||||
col.name,
|
|
||||||
{ type: col.type.toLowerCase(), comment: col.comment || "" },
|
|
||||||
]));
|
|
||||||
const codeFieldsMap = new Map((table.fields || []).map((f) => [f.fieldName, f]));
|
const codeFieldsMap = new Map((table.fields || []).map((f) => [f.fieldName, f]));
|
||||||
const fieldsToAdd = [];
|
const fieldsToAdd = [];
|
||||||
const fieldsToModify = [];
|
const fieldsToModify = [];
|
||||||
|
const fieldsToRecreate = [];
|
||||||
const fieldsToDrop = [];
|
const fieldsToDrop = [];
|
||||||
let needsVectorRecreate = false;
|
|
||||||
for (const field of table.fields || []) {
|
for (const field of table.fields || []) {
|
||||||
if (!field.fieldName)
|
if (!field.fieldName)
|
||||||
continue;
|
continue;
|
||||||
const liveField = liveFieldsMap.get(field.fieldName);
|
const liveField = liveFieldsMap.get(field.fieldName);
|
||||||
if (!liveField) {
|
if (!liveField) {
|
||||||
// Adding a new vector column can require rebuild if VECTOR INDEX
|
|
||||||
// constraints conflict; still try surgical add first.
|
|
||||||
fieldsToAdd.push(field);
|
fieldsToAdd.push(field);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
let typeDiverged = !columnTypesMatch(liveField.type, mapDataType(field));
|
let mapped_data_type = mapDataType(field);
|
||||||
|
let typeDiverged = !columnTypesMatch(liveField.type, mapped_data_type);
|
||||||
if (isVectorField(field)) {
|
if (isVectorField(field)) {
|
||||||
typeDiverged = vectorTypeDiverged(liveField.type, liveField.comment, field);
|
typeDiverged = vectorTypeDiverged(liveField.type, liveField.comment || "", field);
|
||||||
if (typeDiverged) {
|
if (typeDiverged) {
|
||||||
needsVectorRecreate = true;
|
// VECTOR dimensions / storage cannot be MODIFYed — drop + re-add column
|
||||||
|
fieldsToRecreate.push(field);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (typeDiverged) {
|
const attrsDiverged = !typeDiverged && columnAttributesDiverged(liveField, field);
|
||||||
|
if (typeDiverged || attrsDiverged) {
|
||||||
fieldsToModify.push(field);
|
fieldsToModify.push(field);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Vector dimension / storage type changes → full rebuild automatically
|
|
||||||
if (needsVectorRecreate) {
|
|
||||||
console.log(`Vector column change detected on \`${table.tableName}\`; recreating table`);
|
|
||||||
await recreateTable({ table, config });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const col of existingColumns) {
|
for (const col of existingColumns) {
|
||||||
if (!codeFieldsMap.has(col.name)) {
|
if (!codeFieldsMap.has(col.name)) {
|
||||||
fieldsToDrop.push(col.name);
|
fieldsToDrop.push(col.name);
|
||||||
@@ -117,11 +233,18 @@ export default async function updateTable({ table, config, }) {
|
|||||||
}
|
}
|
||||||
if (fieldsToAdd.length === 0 &&
|
if (fieldsToAdd.length === 0 &&
|
||||||
fieldsToModify.length === 0 &&
|
fieldsToModify.length === 0 &&
|
||||||
|
fieldsToRecreate.length === 0 &&
|
||||||
fieldsToDrop.length === 0) {
|
fieldsToDrop.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(`Surgically updating table structure from database layout: ${table.tableName}`);
|
console.log(`Surgically updating table structure from database layout: ${table.tableName}`);
|
||||||
if (fieldsToDrop.length > 0) {
|
if (fieldsToDrop.length > 0) {
|
||||||
|
// Drop FKs that reference columns being removed
|
||||||
|
await dropForeignKeysOnColumns({
|
||||||
|
tableName: table.tableName,
|
||||||
|
columns: fieldsToDrop,
|
||||||
|
config,
|
||||||
|
});
|
||||||
const schemaCond = schemaCondition(config);
|
const schemaCond = schemaCondition(config);
|
||||||
const pkRows = await querySchemaRows({
|
const pkRows = await querySchemaRows({
|
||||||
query: `SELECT COLUMN_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE ${schemaCond.where} AND TABLE_NAME = ? AND CONSTRAINT_NAME = 'PRIMARY'`,
|
query: `SELECT COLUMN_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE ${schemaCond.where} AND TABLE_NAME = ? AND CONSTRAINT_NAME = 'PRIMARY'`,
|
||||||
@@ -149,27 +272,43 @@ export default async function updateTable({ table, config, }) {
|
|||||||
}
|
}
|
||||||
for (const indexName of indexesToDrop) {
|
for (const indexName of indexesToDrop) {
|
||||||
console.log(`Dropping index ${indexName} because it contains a dropped column`);
|
console.log(`Dropping index ${indexName} because it contains a dropped column`);
|
||||||
|
try {
|
||||||
await runSchemaQuery({
|
await runSchemaQuery({
|
||||||
query: `DROP INDEX ${MariaDBQuoteGen(indexName)} ON ${MariaDBQuoteGen(table.tableName)}`,
|
query: `DROP INDEX ${MariaDBQuoteGen(indexName)} ON ${MariaDBQuoteGen(table.tableName)}`,
|
||||||
config,
|
config,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
catch (err) {
|
||||||
|
if (String(err?.message || "").includes("needed in a foreign key constraint")) {
|
||||||
|
console.warn(`Skipping drop of index ${indexName}: required by a foreign key constraint`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Drop FKs on columns being modified (MODIFY can fail with FK present)
|
||||||
|
if (fieldsToModify.length > 0) {
|
||||||
|
await dropForeignKeysOnColumns({
|
||||||
|
tableName: table.tableName,
|
||||||
|
columns: fieldsToModify
|
||||||
|
.map((f) => f.fieldName)
|
||||||
|
.filter((n) => Boolean(n)),
|
||||||
|
config,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
for (const field of fieldsToAdd) {
|
for (const field of fieldsToAdd) {
|
||||||
await addColumn({ tableName: table.tableName, field, config });
|
await addColumn({ tableName: table.tableName, field, config });
|
||||||
}
|
}
|
||||||
for (const field of fieldsToModify) {
|
for (const field of fieldsToModify) {
|
||||||
try {
|
|
||||||
await modifyColumn({ tableName: table.tableName, field, config });
|
await modifyColumn({ tableName: table.tableName, field, config });
|
||||||
}
|
}
|
||||||
catch (err) {
|
for (const field of fieldsToRecreate) {
|
||||||
if (isVectorField(field)) {
|
await recreateColumn({
|
||||||
console.warn(`[Vector Resize] Re-aligning dimension spaces natively via safe migration schema rebuild.`);
|
tableName: table.tableName,
|
||||||
await recreateTable({ table, config });
|
field,
|
||||||
return;
|
config,
|
||||||
}
|
});
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for (const fieldName of fieldsToDrop) {
|
for (const fieldName of fieldsToDrop) {
|
||||||
await dropColumn({ tableName: table.tableName, fieldName, config });
|
await dropColumn({ tableName: table.tableName, fieldName, config });
|
||||||
|
|||||||
Vendored
+6
-6
@@ -74,10 +74,6 @@ export interface BUN_MARIADB_TableSchemaType {
|
|||||||
*/
|
*/
|
||||||
childTableDbId?: string | number;
|
childTableDbId?: string | number;
|
||||||
collation?: (typeof MariaDBCollations)[number];
|
collation?: (typeof MariaDBCollations)[number];
|
||||||
/**
|
|
||||||
* If this is a vector-oriented table (native MariaDB VECTOR columns/indexes)
|
|
||||||
*/
|
|
||||||
isVector?: boolean;
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Reference object used to link a table to one of its child tables.
|
* Reference object used to link a table to one of its child tables.
|
||||||
@@ -213,7 +209,10 @@ export type BUN_MARIADB_FieldSchemaType = {
|
|||||||
onDelete?: string;
|
onDelete?: string;
|
||||||
onDeleteLiteral?: string;
|
onDeleteLiteral?: string;
|
||||||
cssFiles?: string[];
|
cssFiles?: string[];
|
||||||
integerLength?: string | number;
|
/**
|
||||||
|
* Datatype length. Eg 255 for VARCHAR
|
||||||
|
*/
|
||||||
|
dataLength?: string | number;
|
||||||
decimals?: string | number;
|
decimals?: string | number;
|
||||||
code?: boolean;
|
code?: boolean;
|
||||||
options?: (string | number)[];
|
options?: (string | number)[];
|
||||||
@@ -830,7 +829,7 @@ export type ServerQueryParamsJoin<Table extends string = string, Field extends o
|
|||||||
joinType: "INNER JOIN" | "JOIN" | "LEFT JOIN" | "RIGHT JOIN";
|
joinType: "INNER JOIN" | "JOIN" | "LEFT JOIN" | "RIGHT JOIN";
|
||||||
alias?: string;
|
alias?: string;
|
||||||
tableName: Table;
|
tableName: Table;
|
||||||
match?: ServerQueryParamsJoinMatchObject<Field> | ServerQueryParamsJoinMatchObject<Field>[];
|
match?: ServerQueryParamsJoinMatchObject<Field> | (ServerQueryParamsJoinMatchObject<Field> | undefined)[];
|
||||||
selectFields?: (keyof Field | SelectFieldObject<Field>)[];
|
selectFields?: (keyof Field | SelectFieldObject<Field>)[];
|
||||||
omitFields?: (keyof Field | {
|
omitFields?: (keyof Field | {
|
||||||
field: keyof Field;
|
field: keyof Field;
|
||||||
@@ -1411,6 +1410,7 @@ export type DBResponseObject<T extends {
|
|||||||
msg?: string;
|
msg?: string;
|
||||||
debug?: any;
|
debug?: any;
|
||||||
count?: number;
|
count?: number;
|
||||||
|
db_res?: any;
|
||||||
};
|
};
|
||||||
export type DBInsertReturn = {
|
export type DBInsertReturn = {
|
||||||
count?: number;
|
count?: number;
|
||||||
|
|||||||
Vendored
+5
-3
@@ -1,15 +1,17 @@
|
|||||||
export declare const ExportArchiveMembers: {
|
export declare const ExportArchiveMembers: {
|
||||||
readonly SqlFileName: "dump.sql";
|
readonly SqlFileName: "dump.sql";
|
||||||
readonly SchemaFileName: "schema.ts";
|
readonly SchemaFileName: "schema";
|
||||||
};
|
};
|
||||||
export type ExportArchiveContents = {
|
export type ExportArchiveContents = {
|
||||||
sql: string;
|
sql: string;
|
||||||
schemaTs: string;
|
schema: string;
|
||||||
|
/** Archive member basename, e.g. schema.ts / schema.json / schema.yaml */
|
||||||
|
schemaFileName: string;
|
||||||
};
|
};
|
||||||
export declare function isArchivePath(filePath: string): boolean;
|
export declare function isArchivePath(filePath: string): boolean;
|
||||||
export declare function isSqlPath(filePath: string): boolean;
|
export declare function isSqlPath(filePath: string): boolean;
|
||||||
/**
|
/**
|
||||||
* Create a portable export archive (tar / tar.gz / zip) with dump.sql + schema.ts.
|
* Create a portable export archive (tar / tar.gz / zip) with dump.sql + schema file.
|
||||||
*/
|
*/
|
||||||
export declare function writeExportArchive({ contents, outPath, }: {
|
export declare function writeExportArchive({ contents, outPath, }: {
|
||||||
contents: ExportArchiveContents;
|
contents: ExportArchiveContents;
|
||||||
|
|||||||
Vendored
+55
-16
@@ -2,6 +2,7 @@ import fs from "fs";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { AppData } from "../data/app-data";
|
import { AppData } from "../data/app-data";
|
||||||
import grabDirNames from "../data/grab-dir-names";
|
import grabDirNames from "../data/grab-dir-names";
|
||||||
|
import { isSchemaFileName } from "./resolve-and-load-data-file";
|
||||||
export const ExportArchiveMembers = {
|
export const ExportArchiveMembers = {
|
||||||
SqlFileName: "dump.sql",
|
SqlFileName: "dump.sql",
|
||||||
SchemaFileName: AppData.DbSchemaFileName,
|
SchemaFileName: AppData.DbSchemaFileName,
|
||||||
@@ -15,7 +16,7 @@ export function isSqlPath(filePath) {
|
|||||||
return filePath.toLowerCase().endsWith(".sql");
|
return filePath.toLowerCase().endsWith(".sql");
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Create a portable export archive (tar / tar.gz / zip) with dump.sql + schema.ts.
|
* Create a portable export archive (tar / tar.gz / zip) with dump.sql + schema file.
|
||||||
*/
|
*/
|
||||||
export async function writeExportArchive({ contents, outPath, }) {
|
export async function writeExportArchive({ contents, outPath, }) {
|
||||||
const lower = outPath.toLowerCase();
|
const lower = outPath.toLowerCase();
|
||||||
@@ -25,7 +26,7 @@ export async function writeExportArchive({ contents, outPath, }) {
|
|||||||
}
|
}
|
||||||
const members = {
|
const members = {
|
||||||
[ExportArchiveMembers.SqlFileName]: contents.sql,
|
[ExportArchiveMembers.SqlFileName]: contents.sql,
|
||||||
[ExportArchiveMembers.SchemaFileName]: contents.schemaTs,
|
[contents.schemaFileName]: contents.schema,
|
||||||
};
|
};
|
||||||
const gzip = lower.endsWith(".gz") || lower.endsWith(".tgz");
|
const gzip = lower.endsWith(".gz") || lower.endsWith(".tgz");
|
||||||
if (gzip) {
|
if (gzip) {
|
||||||
@@ -48,18 +49,20 @@ export async function readExportArchive(archivePath) {
|
|||||||
const files = await archive.files();
|
const files = await archive.files();
|
||||||
const sql = (await readArchiveMember(files, ExportArchiveMembers.SqlFileName)) ??
|
const sql = (await readArchiveMember(files, ExportArchiveMembers.SqlFileName)) ??
|
||||||
(await readFirstMatching(files, (name) => name.endsWith(".sql")));
|
(await readFirstMatching(files, (name) => name.endsWith(".sql")));
|
||||||
const schemaTs = (await readArchiveMember(files, ExportArchiveMembers.SchemaFileName)) ??
|
const schemaEntry = await readFirstSchemaMember(files);
|
||||||
(await readFirstMatching(files, (name) => name.endsWith("schema.ts")));
|
|
||||||
if (!sql) {
|
if (!sql) {
|
||||||
throw new Error(`Archive is missing SQL dump (expected \`${ExportArchiveMembers.SqlFileName}\`)`);
|
throw new Error(`Archive is missing SQL dump (expected \`${ExportArchiveMembers.SqlFileName}\`)`);
|
||||||
}
|
}
|
||||||
if (!schemaTs) {
|
if (!schemaEntry) {
|
||||||
throw new Error(`Archive is missing schema TypeScript (expected \`${ExportArchiveMembers.SchemaFileName}\`)`);
|
throw new Error(`Archive is missing schema file (expected \`${AppData.DbSchemaFileName}.ts|json|yaml|yml\`)`);
|
||||||
}
|
}
|
||||||
return { sql, schemaTs };
|
return {
|
||||||
|
sql,
|
||||||
|
schema: schemaEntry.content,
|
||||||
|
schemaFileName: schemaEntry.fileName,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
async function readArchiveMember(files, name) {
|
async function readArchiveMember(files, name) {
|
||||||
// Exact match, or basename match for nested paths
|
|
||||||
for (const [entry, file] of files) {
|
for (const [entry, file] of files) {
|
||||||
if (entry === name || path.basename(entry) === name) {
|
if (entry === name || path.basename(entry) === name) {
|
||||||
return await file.text();
|
return await file.text();
|
||||||
@@ -75,15 +78,27 @@ async function readFirstMatching(files, predicate) {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
async function readFirstSchemaMember(files) {
|
||||||
|
for (const [entry, file] of files) {
|
||||||
|
const base = path.basename(entry);
|
||||||
|
if (isSchemaFileName(base)) {
|
||||||
|
return {
|
||||||
|
content: await file.text(),
|
||||||
|
fileName: base === AppData.DbSchemaFileName ? "schema.ts" : base,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
async function writeZipArchive({ contents, outPath, }) {
|
async function writeZipArchive({ contents, outPath, }) {
|
||||||
const { BUN_MARIADB_TEMP_DIR } = grabDirNames();
|
const { BUN_MARIADB_TEMP_DIR } = grabDirNames();
|
||||||
const tempDir = path.join(BUN_MARIADB_TEMP_DIR, `export-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
const tempDir = path.join(BUN_MARIADB_TEMP_DIR, `export-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
||||||
fs.mkdirSync(tempDir, { recursive: true });
|
fs.mkdirSync(tempDir, { recursive: true });
|
||||||
try {
|
try {
|
||||||
const sqlPath = path.join(tempDir, ExportArchiveMembers.SqlFileName);
|
const sqlPath = path.join(tempDir, ExportArchiveMembers.SqlFileName);
|
||||||
const schemaPath = path.join(tempDir, ExportArchiveMembers.SchemaFileName);
|
const schemaPath = path.join(tempDir, contents.schemaFileName);
|
||||||
fs.writeFileSync(sqlPath, contents.sql, "utf-8");
|
fs.writeFileSync(sqlPath, contents.sql, "utf-8");
|
||||||
fs.writeFileSync(schemaPath, contents.schemaTs, "utf-8");
|
fs.writeFileSync(schemaPath, contents.schema, "utf-8");
|
||||||
const absOut = path.resolve(outPath);
|
const absOut = path.resolve(outPath);
|
||||||
const proc = Bun.spawn([
|
const proc = Bun.spawn([
|
||||||
"zip",
|
"zip",
|
||||||
@@ -91,7 +106,7 @@ async function writeZipArchive({ contents, outPath, }) {
|
|||||||
"-j",
|
"-j",
|
||||||
absOut,
|
absOut,
|
||||||
ExportArchiveMembers.SqlFileName,
|
ExportArchiveMembers.SqlFileName,
|
||||||
ExportArchiveMembers.SchemaFileName,
|
contents.schemaFileName,
|
||||||
], {
|
], {
|
||||||
cwd: tempDir,
|
cwd: tempDir,
|
||||||
stdout: "pipe",
|
stdout: "pipe",
|
||||||
@@ -127,15 +142,18 @@ async function readZipArchive(archivePath) {
|
|||||||
throw new Error(`unzip failed (exit ${exitCode}): ${stderr || "unknown error"}. Ensure \`unzip\` is installed.`);
|
throw new Error(`unzip failed (exit ${exitCode}): ${stderr || "unknown error"}. Ensure \`unzip\` is installed.`);
|
||||||
}
|
}
|
||||||
const sql = findFileContents(tempDir, (name) => name === ExportArchiveMembers.SqlFileName || name.endsWith(".sql"));
|
const sql = findFileContents(tempDir, (name) => name === ExportArchiveMembers.SqlFileName || name.endsWith(".sql"));
|
||||||
const schemaTs = findFileContents(tempDir, (name) => name === ExportArchiveMembers.SchemaFileName ||
|
const schemaHit = findSchemaFile(tempDir);
|
||||||
name.endsWith("schema.ts"));
|
|
||||||
if (!sql) {
|
if (!sql) {
|
||||||
throw new Error(`Archive is missing SQL dump (expected \`${ExportArchiveMembers.SqlFileName}\`)`);
|
throw new Error(`Archive is missing SQL dump (expected \`${ExportArchiveMembers.SqlFileName}\`)`);
|
||||||
}
|
}
|
||||||
if (!schemaTs) {
|
if (!schemaHit) {
|
||||||
throw new Error(`Archive is missing schema TypeScript (expected \`${ExportArchiveMembers.SchemaFileName}\`)`);
|
throw new Error(`Archive is missing schema file (expected \`${AppData.DbSchemaFileName}.ts|json|yaml|yml\`)`);
|
||||||
}
|
}
|
||||||
return { sql, schemaTs };
|
return {
|
||||||
|
sql,
|
||||||
|
schema: schemaHit.content,
|
||||||
|
schemaFileName: schemaHit.fileName,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
@@ -157,3 +175,24 @@ function findFileContents(dir, predicate) {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
function findSchemaFile(dir) {
|
||||||
|
const stack = [dir];
|
||||||
|
while (stack.length) {
|
||||||
|
const current = stack.pop();
|
||||||
|
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
||||||
|
const full = path.join(current, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
stack.push(full);
|
||||||
|
}
|
||||||
|
else if (isSchemaFileName(entry.name)) {
|
||||||
|
return {
|
||||||
|
content: fs.readFileSync(full, "utf-8"),
|
||||||
|
fileName: entry.name === AppData.DbSchemaFileName
|
||||||
|
? "schema.ts"
|
||||||
|
: entry.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
import { AppData } from "../data/app-data";
|
||||||
|
export type DataFileFormat = "ts" | "js" | "json" | "yaml";
|
||||||
|
export type ResolvedDataFile = {
|
||||||
|
path: string;
|
||||||
|
basename: string;
|
||||||
|
extension: (typeof AppData.SupportedDataFileExtensions)[number];
|
||||||
|
format: DataFileFormat;
|
||||||
|
};
|
||||||
|
export type LoadedDataFile<T> = ResolvedDataFile & {
|
||||||
|
data: T;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Resolve a basename (no extension) to an existing file among supported formats.
|
||||||
|
* Priority: .ts > .js > .json > .yaml > .yml
|
||||||
|
* Errors if multiple matches exist.
|
||||||
|
*/
|
||||||
|
export declare function resolveDataFile(dir: string, baseName: string): ResolvedDataFile | null;
|
||||||
|
export declare function supportedDataFileNames(baseName: string): string;
|
||||||
|
export declare function isSchemaFileName(name: string): boolean;
|
||||||
|
/**
|
||||||
|
* Load a data file as a plain object.
|
||||||
|
* - .ts / .js: require() and use default export (or module itself)
|
||||||
|
* - .json: JSON.parse
|
||||||
|
* - .yaml / .yml: Bun.YAML.parse
|
||||||
|
*/
|
||||||
|
export declare function loadDataFile<T>(resolved: ResolvedDataFile): T;
|
||||||
|
export declare function resolveAndLoadDataFile<T>(dir: string, baseName: string): LoadedDataFile<T> | null;
|
||||||
+86
@@ -0,0 +1,86 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { AppData } from "../data/app-data";
|
||||||
|
function extensionToFormat(extension) {
|
||||||
|
switch (extension) {
|
||||||
|
case ".ts":
|
||||||
|
return "ts";
|
||||||
|
case ".js":
|
||||||
|
return "js";
|
||||||
|
case ".json":
|
||||||
|
return "json";
|
||||||
|
case ".yaml":
|
||||||
|
case ".yml":
|
||||||
|
return "yaml";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Resolve a basename (no extension) to an existing file among supported formats.
|
||||||
|
* Priority: .ts > .js > .json > .yaml > .yml
|
||||||
|
* Errors if multiple matches exist.
|
||||||
|
*/
|
||||||
|
export function resolveDataFile(dir, baseName) {
|
||||||
|
const matches = [];
|
||||||
|
for (const extension of AppData.SupportedDataFileExtensions) {
|
||||||
|
const filePath = path.join(dir, `${baseName}${extension}`);
|
||||||
|
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
||||||
|
matches.push({
|
||||||
|
path: filePath,
|
||||||
|
basename: `${baseName}${extension}`,
|
||||||
|
extension,
|
||||||
|
format: extensionToFormat(extension),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (matches.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (matches.length > 1) {
|
||||||
|
const found = matches.map((m) => m.basename).join(", ");
|
||||||
|
throw new Error(`Multiple \`${baseName}\` files found (${found}). Keep only one of: ${AppData.SupportedDataFileExtensions.join(", ")}`);
|
||||||
|
}
|
||||||
|
return matches[0];
|
||||||
|
}
|
||||||
|
export function supportedDataFileNames(baseName) {
|
||||||
|
return AppData.SupportedDataFileExtensions.map((ext) => `\`${baseName}${ext}\``).join(", ");
|
||||||
|
}
|
||||||
|
export function isSchemaFileName(name) {
|
||||||
|
const base = path.basename(name);
|
||||||
|
if (base === AppData.DbSchemaFileName) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return AppData.SupportedDataFileExtensions.some((ext) => base === `${AppData.DbSchemaFileName}${ext}`);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Load a data file as a plain object.
|
||||||
|
* - .ts / .js: require() and use default export (or module itself)
|
||||||
|
* - .json: JSON.parse
|
||||||
|
* - .yaml / .yml: Bun.YAML.parse
|
||||||
|
*/
|
||||||
|
export function loadDataFile(resolved) {
|
||||||
|
if (resolved.format === "ts" || resolved.format === "js") {
|
||||||
|
const imported = require(resolved.path);
|
||||||
|
const data = imported && typeof imported === "object" && "default" in imported
|
||||||
|
? imported.default
|
||||||
|
: imported;
|
||||||
|
if (data == null) {
|
||||||
|
throw new Error(`No default export from \`${resolved.path}\`. Please export a default module.`);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
const text = fs.readFileSync(resolved.path, "utf-8");
|
||||||
|
if (resolved.format === "json") {
|
||||||
|
return JSON.parse(text);
|
||||||
|
}
|
||||||
|
return Bun.YAML.parse(text);
|
||||||
|
}
|
||||||
|
export function resolveAndLoadDataFile(dir, baseName) {
|
||||||
|
const resolved = resolveDataFile(dir, baseName);
|
||||||
|
if (!resolved) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...resolved,
|
||||||
|
data: loadDataFile(resolved),
|
||||||
|
};
|
||||||
|
}
|
||||||
+2
-1
@@ -1,4 +1,4 @@
|
|||||||
import { isUndefined } from "lodash";
|
import _, { isUndefined } from "lodash";
|
||||||
import sqlGenGrabConcatStr from "./sql-generator-grab-concat-str";
|
import sqlGenGrabConcatStr from "./sql-generator-grab-concat-str";
|
||||||
import sqlGenGenJoinStr from "./sql-generator-gen-join-str";
|
import sqlGenGenJoinStr from "./sql-generator-gen-join-str";
|
||||||
import sqlGenGrabSelectFieldSQL from "./sql-generator-grab-select-field-sql";
|
import sqlGenGrabSelectFieldSQL from "./sql-generator-grab-select-field-sql";
|
||||||
@@ -162,6 +162,7 @@ export default function sqlGenGenQueryStr(params) {
|
|||||||
if (Array.isArray(join.match)) {
|
if (Array.isArray(join.match)) {
|
||||||
return ("(" +
|
return ("(" +
|
||||||
join.match
|
join.match
|
||||||
|
.filter((mtch) => !_.isUndefined(mtch))
|
||||||
.map((mtch) => {
|
.map((mtch) => {
|
||||||
const { str, values } = sqlGenGenJoinStr({
|
const { str, values } = sqlGenGenJoinStr({
|
||||||
mtch,
|
mtch,
|
||||||
|
|||||||
Vendored
+1
-1
@@ -31,7 +31,7 @@ export default function sqlInsertGenerator({ tableName, data, dbFullName, }) {
|
|||||||
: value
|
: value
|
||||||
? value
|
? value
|
||||||
: null;
|
: null;
|
||||||
if (!finalValue) {
|
if (!finalValue && typeof value !== "number") {
|
||||||
queryValues.push(null);
|
queryValues.push(null);
|
||||||
return "?";
|
return "?";
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@moduletrace/bun-mariadb",
|
"name": "@moduletrace/bun-mariadb",
|
||||||
"version": "1.0.2",
|
"version": "1.0.19",
|
||||||
"description": "Schema-driven MariaDB manager for Bun",
|
"description": "Schema-driven MariaDB manager for Bun",
|
||||||
"author": "Benjamin Toby",
|
"author": "Benjamin Toby",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -12,6 +12,11 @@
|
|||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"import": "./dist/index.js",
|
"import": "./dist/index.js",
|
||||||
"default": "./dist/index.js"
|
"default": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"./types": {
|
||||||
|
"types": "./dist/types/index.d.ts",
|
||||||
|
"import": "./dist/types/index.js",
|
||||||
|
"default": "./dist/types/index.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
+25
-8
@@ -3,10 +3,14 @@ import path from "path";
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import grabDBDir from "../utils/grab-db-dir";
|
import grabDBDir from "../utils/grab-db-dir";
|
||||||
import { AppData } from "../data/app-data";
|
|
||||||
import { dumpDatabase } from "../utils/mariadb-dump-restore";
|
import { dumpDatabase } from "../utils/mariadb-dump-restore";
|
||||||
import { writeExportArchive } from "../utils/export-archive";
|
import { writeExportArchive } from "../utils/export-archive";
|
||||||
import trimExports from "../utils/trim-exports";
|
import trimExports from "../utils/trim-exports";
|
||||||
|
import {
|
||||||
|
resolveDataFile,
|
||||||
|
supportedDataFileNames,
|
||||||
|
} from "../utils/resolve-and-load-data-file";
|
||||||
|
import { AppData } from "../data/app-data";
|
||||||
|
|
||||||
function defaultExportFileName(
|
function defaultExportFileName(
|
||||||
dbName: string,
|
dbName: string,
|
||||||
@@ -18,7 +22,7 @@ function defaultExportFileName(
|
|||||||
export default function () {
|
export default function () {
|
||||||
return new Command("export")
|
return new Command("export")
|
||||||
.description(
|
.description(
|
||||||
"Export database SQL dump + schema.ts into a portable archive",
|
"Export database SQL dump + schema into a portable archive",
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"-o, --output <path>",
|
"-o, --output <path>",
|
||||||
@@ -39,10 +43,19 @@ export default function () {
|
|||||||
fs.mkdirSync(export_dir, { recursive: true });
|
fs.mkdirSync(export_dir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const schemaPath = path.join(db_dir, AppData.DbSchemaFileName);
|
const schemaResolved =
|
||||||
if (!fs.existsSync(schemaPath)) {
|
(global.SCHEMA_FILE_PATH &&
|
||||||
|
fs.existsSync(global.SCHEMA_FILE_PATH) && {
|
||||||
|
path: global.SCHEMA_FILE_PATH,
|
||||||
|
basename: path.basename(global.SCHEMA_FILE_PATH),
|
||||||
|
}) ||
|
||||||
|
resolveDataFile(db_dir, AppData.DbSchemaFileName);
|
||||||
|
|
||||||
|
if (!schemaResolved) {
|
||||||
console.error(
|
console.error(
|
||||||
chalk.red(`Schema file not found: ${schemaPath}`),
|
chalk.red(
|
||||||
|
`Schema file not found in \`${db_dir}\` (${supportedDataFileNames(AppData.DbSchemaFileName)})`,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -67,10 +80,14 @@ export default function () {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const sql = await dumpDatabase(config);
|
const sql = await dumpDatabase(config);
|
||||||
const schemaTs = fs.readFileSync(schemaPath, "utf-8");
|
const schema = fs.readFileSync(schemaResolved.path, "utf-8");
|
||||||
|
|
||||||
await writeExportArchive({
|
await writeExportArchive({
|
||||||
contents: { sql, schemaTs },
|
contents: {
|
||||||
|
sql,
|
||||||
|
schema,
|
||||||
|
schemaFileName: schemaResolved.basename,
|
||||||
|
},
|
||||||
outPath,
|
outPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,7 +100,7 @@ export default function () {
|
|||||||
);
|
);
|
||||||
console.log(
|
console.log(
|
||||||
chalk.dim(
|
chalk.dim(
|
||||||
`Contains: dump.sql + ${AppData.DbSchemaFileName}`,
|
`Contains: dump.sql + ${schemaResolved.basename}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|||||||
+23
-8
@@ -6,13 +6,14 @@ import { select } from "@inquirer/prompts";
|
|||||||
import grabDBDir from "../utils/grab-db-dir";
|
import grabDBDir from "../utils/grab-db-dir";
|
||||||
import grabSortedExports from "../utils/grab-sorted-exports";
|
import grabSortedExports from "../utils/grab-sorted-exports";
|
||||||
import grabBackupData from "../utils/grab-backup-data";
|
import grabBackupData from "../utils/grab-backup-data";
|
||||||
import { AppData } from "../data/app-data";
|
|
||||||
import { restoreDatabase } from "../utils/mariadb-dump-restore";
|
import { restoreDatabase } from "../utils/mariadb-dump-restore";
|
||||||
import {
|
import {
|
||||||
isArchivePath,
|
isArchivePath,
|
||||||
isSqlPath,
|
isSqlPath,
|
||||||
readExportArchive,
|
readExportArchive,
|
||||||
} from "../utils/export-archive";
|
} from "../utils/export-archive";
|
||||||
|
import { resolveDataFile } from "../utils/resolve-and-load-data-file";
|
||||||
|
import { AppData } from "../data/app-data";
|
||||||
|
|
||||||
function formatChoice(name: string, index: number): string {
|
function formatChoice(name: string, index: number): string {
|
||||||
const { backup_date } = grabBackupData({ backup_name: name });
|
const { backup_date } = grabBackupData({ backup_name: name });
|
||||||
@@ -25,7 +26,7 @@ function formatChoice(name: string, index: number): string {
|
|||||||
export default function () {
|
export default function () {
|
||||||
return new Command("import")
|
return new Command("import")
|
||||||
.description(
|
.description(
|
||||||
"Import an SQL dump, or a full export archive (SQL + schema.ts)",
|
"Import an SQL dump, or a full export archive (SQL + schema)",
|
||||||
)
|
)
|
||||||
.argument(
|
.argument(
|
||||||
"[file]",
|
"[file]",
|
||||||
@@ -33,11 +34,11 @@ export default function () {
|
|||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"--sql-only",
|
"--sql-only",
|
||||||
"When importing an archive, restore SQL only (skip writing schema.ts)",
|
"When importing an archive, restore SQL only (skip writing schema)",
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"--schema-only",
|
"--schema-only",
|
||||||
"When importing an archive, write schema.ts only (skip SQL restore)",
|
"When importing an archive, write schema only (skip SQL restore)",
|
||||||
)
|
)
|
||||||
.action(async (fileArg: string | undefined, opts) => {
|
.action(async (fileArg: string | undefined, opts) => {
|
||||||
console.log(`Importing database ...`);
|
console.log(`Importing database ...`);
|
||||||
@@ -113,7 +114,8 @@ export default function () {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { sql, schemaTs } = await readExportArchive(filePath);
|
const { sql, schema, schemaFileName } =
|
||||||
|
await readExportArchive(filePath);
|
||||||
|
|
||||||
if (!opts.schemaOnly) {
|
if (!opts.schemaOnly) {
|
||||||
await restoreDatabase(config, sql);
|
await restoreDatabase(config, sql);
|
||||||
@@ -121,16 +123,29 @@ export default function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!opts.sqlOnly) {
|
if (!opts.sqlOnly) {
|
||||||
const schemaPath = path.join(
|
const existing = resolveDataFile(
|
||||||
db_dir,
|
db_dir,
|
||||||
AppData.DbSchemaFileName,
|
AppData.DbSchemaFileName,
|
||||||
);
|
);
|
||||||
fs.writeFileSync(schemaPath, schemaTs, "utf-8");
|
const schemaPath = path.join(db_dir, schemaFileName);
|
||||||
|
|
||||||
|
// Remove a differently-named schema so only one format remains
|
||||||
|
if (
|
||||||
|
existing &&
|
||||||
|
path.resolve(existing.path) !== path.resolve(schemaPath)
|
||||||
|
) {
|
||||||
|
fs.unlinkSync(existing.path);
|
||||||
console.log(
|
console.log(
|
||||||
chalk.green(`Schema written → ${schemaPath}`),
|
chalk.dim(
|
||||||
|
`Removed previous schema file: ${existing.basename}`,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(schemaPath, schema, "utf-8");
|
||||||
|
console.log(chalk.green(`Schema written → ${schemaPath}`));
|
||||||
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`${chalk.bold(chalk.green(`DB Import Success!`))} ← ${filePath}`,
|
`${chalk.bold(chalk.green(`DB Import Success!`))} ← ${filePath}`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default function () {
|
|||||||
|
|
||||||
if (!config.typedef_file_path) {
|
if (!config.typedef_file_path) {
|
||||||
console.error(
|
console.error(
|
||||||
`\`typedef_file_path\` is required in bun-mariadb.config.ts to generate types.`,
|
`\`typedef_file_path\` is required in bun-mariadb.config to generate types.`,
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
export const AppData = {
|
export const AppData = {
|
||||||
ConfigFileName: "bun-mariadb.config.ts",
|
ConfigFileName: "bun-mariadb.config",
|
||||||
MaxBackups: 10,
|
MaxBackups: 10,
|
||||||
MaxExports: 10,
|
MaxExports: 10,
|
||||||
DefaultBackupDirName: ".backups",
|
DefaultBackupDirName: ".backups",
|
||||||
DefaultExportDirName: ".exports",
|
DefaultExportDirName: ".exports",
|
||||||
DbSchemaManagerTableName: "__db_schema_manager__",
|
DbSchemaManagerTableName: "__db_schema_manager__",
|
||||||
DbSchemaFileName: "schema.ts",
|
DbSchemaFileName: "schema",
|
||||||
|
/** Priority order when resolving config/schema files */
|
||||||
|
SupportedDataFileExtensions: [".ts", ".js", ".json", ".yaml", ".yml"] as const,
|
||||||
MaxInitRetries: 50,
|
MaxInitRetries: 50,
|
||||||
InitRetryIntervalMilliseconds: 5000,
|
InitRetryIntervalMilliseconds: 5000,
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
+34
-19
@@ -8,6 +8,10 @@ import {
|
|||||||
RequiredENVs,
|
RequiredENVs,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import setMariaDBClient from "./set-mariadb-client";
|
import setMariaDBClient from "./set-mariadb-client";
|
||||||
|
import {
|
||||||
|
resolveAndLoadDataFile,
|
||||||
|
supportedDataFileNames,
|
||||||
|
} from "../utils/resolve-and-load-data-file";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* # Declare Global Variables
|
* # Declare Global Variables
|
||||||
@@ -16,6 +20,8 @@ declare global {
|
|||||||
var CONFIG: BunMariaDBConfig;
|
var CONFIG: BunMariaDBConfig;
|
||||||
var DB_SCHEMA: BUN_MARIADB_DatabaseSchemaType;
|
var DB_SCHEMA: BUN_MARIADB_DatabaseSchemaType;
|
||||||
var MARIADB_CLIENT: Bun.SQL;
|
var MARIADB_CLIENT: Bun.SQL;
|
||||||
|
var CONFIG_FILE_PATH: string;
|
||||||
|
var SCHEMA_FILE_PATH: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function init(): void {
|
export default function init(): void {
|
||||||
@@ -23,21 +29,23 @@ export default function init(): void {
|
|||||||
const { ROOT_DIR } = grabDirNames();
|
const { ROOT_DIR } = grabDirNames();
|
||||||
const { ConfigFileName } = AppData;
|
const { ConfigFileName } = AppData;
|
||||||
|
|
||||||
const ConfigFilePath = path.join(ROOT_DIR, ConfigFileName);
|
const loadedConfig = resolveAndLoadDataFile<BunMariaDBConfig>(
|
||||||
|
ROOT_DIR,
|
||||||
|
ConfigFileName,
|
||||||
|
);
|
||||||
|
|
||||||
if (!fs.existsSync(ConfigFilePath)) {
|
if (!loadedConfig) {
|
||||||
console.error(
|
console.error(
|
||||||
`Please create a \`${ConfigFileName}\` file at the root of your project.`,
|
`Please create a ${supportedDataFileNames(ConfigFileName)} file at the root of your project.`,
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConfigImport = require(ConfigFilePath);
|
const Config = loadedConfig.data;
|
||||||
const Config = ConfigImport["default"] as BunMariaDBConfig;
|
|
||||||
|
|
||||||
if (!Config) {
|
if (!Config || typeof Config !== "object") {
|
||||||
console.error(
|
console.error(
|
||||||
`No default export from \`${ConfigFilePath}\`. Please export a default module.`,
|
`Invalid config in \`${loadedConfig.path}\`. Expected a config object${loadedConfig.format === "ts" || loadedConfig.format === "js" ? " (export default)" : ""}.`,
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -63,7 +71,7 @@ export default function init(): void {
|
|||||||
|
|
||||||
if (!Config.db_dir) {
|
if (!Config.db_dir) {
|
||||||
console.error(
|
console.error(
|
||||||
`\`db_dir\` is required in your config. This directory holds all database related configuration. Also note that a \`${AppData["DbSchemaFileName"]}\` file is also required in this directory to define your database schema`,
|
`\`db_dir\` is required in your config. This directory holds all database related configuration. Also note that a ${supportedDataFileNames(AppData.DbSchemaFileName)} file is also required in this directory to define your database schema`,
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -73,19 +81,27 @@ export default function init(): void {
|
|||||||
fs.mkdirSync(db_dir, { recursive: true });
|
fs.mkdirSync(db_dir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const DBSchemaFilePath = path.join(db_dir, AppData["DbSchemaFileName"]);
|
const loadedSchema =
|
||||||
|
resolveAndLoadDataFile<BUN_MARIADB_DatabaseSchemaType>(
|
||||||
|
db_dir,
|
||||||
|
AppData.DbSchemaFileName,
|
||||||
|
);
|
||||||
|
|
||||||
if (!fs.existsSync(DBSchemaFilePath)) {
|
if (!loadedSchema) {
|
||||||
console.error(
|
console.error(
|
||||||
`Please create a schema file at \`${DBSchemaFilePath}\`. Don't forget to export a default module from this file.`,
|
`Please create a schema file (${supportedDataFileNames(AppData.DbSchemaFileName)}) in \`${db_dir}\`${loadedConfig.format === "ts" || loadedConfig.format === "js" ? ". Don't forget to export a default module from .ts/.js files." : "."}`,
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const DbSchemaImport = require(DBSchemaFilePath);
|
const DbSchema = loadedSchema.data;
|
||||||
const DbSchema = DbSchemaImport[
|
|
||||||
"default"
|
if (!DbSchema || typeof DbSchema !== "object") {
|
||||||
] as BUN_MARIADB_DatabaseSchemaType;
|
console.error(
|
||||||
|
`Invalid schema in \`${loadedSchema.path}\`. Expected a schema object${loadedSchema.format === "ts" || loadedSchema.format === "js" ? " (export default)" : ""}.`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
const backup_dir =
|
const backup_dir =
|
||||||
Config.db_backup_dir || AppData["DefaultBackupDirName"];
|
Config.db_backup_dir || AppData["DefaultBackupDirName"];
|
||||||
@@ -95,16 +111,15 @@ export default function init(): void {
|
|||||||
fs.mkdirSync(BackupDir, { recursive: true });
|
fs.mkdirSync(BackupDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const ExportDir = path.resolve(
|
const ExportDir = path.resolve(db_dir, AppData["DefaultExportDirName"]);
|
||||||
db_dir,
|
|
||||||
AppData["DefaultExportDirName"],
|
|
||||||
);
|
|
||||||
if (!fs.existsSync(ExportDir)) {
|
if (!fs.existsSync(ExportDir)) {
|
||||||
fs.mkdirSync(ExportDir, { recursive: true });
|
fs.mkdirSync(ExportDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
global.CONFIG = Config;
|
global.CONFIG = Config;
|
||||||
global.DB_SCHEMA = DbSchema;
|
global.DB_SCHEMA = DbSchema;
|
||||||
|
global.CONFIG_FILE_PATH = loadedConfig.path;
|
||||||
|
global.SCHEMA_FILE_PATH = loadedSchema.path;
|
||||||
|
|
||||||
if (!global.CONFIG) {
|
if (!global.CONFIG) {
|
||||||
console.error(`Couldn't grab global Config.`);
|
console.error(`Couldn't grab global Config.`);
|
||||||
|
|||||||
+19
-10
@@ -23,14 +23,23 @@ const BunMariaDB = {
|
|||||||
|
|
||||||
export default BunMariaDB;
|
export default BunMariaDB;
|
||||||
|
|
||||||
export type {
|
export type * from "./types";
|
||||||
BunMariaDBConfig,
|
export {
|
||||||
BUN_MARIADB_DatabaseSchemaType,
|
UsersOmitedFields,
|
||||||
BUN_MARIADB_TableSchemaType,
|
MariaDBCollations,
|
||||||
BUN_MARIADB_FieldSchemaType,
|
MariaDBCharsets,
|
||||||
BUN_MARIADB_IndexSchemaType,
|
TextFieldTypesArray,
|
||||||
BUN_MARIADB_UniqueConstraintSchemaType,
|
BUN_MARIADB_DATATYPES,
|
||||||
BUN_MARIADB_ForeignKeyType,
|
MariaDBIndexTypes,
|
||||||
DBResponseObject,
|
ServerQueryOperators,
|
||||||
ServerQueryParam,
|
ServerQueryEqualities,
|
||||||
|
SQlComparisons,
|
||||||
|
DataCrudRequestMethods,
|
||||||
|
DataCrudRequestMethodsLowerCase,
|
||||||
|
DsqlCrudActions,
|
||||||
|
QueryFields,
|
||||||
|
DockerComposeServices,
|
||||||
|
IndexTypes,
|
||||||
|
DefaultFields,
|
||||||
|
RequiredENVs,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ export default async function dbHandler<
|
|||||||
single_res: res_array?.[0],
|
single_res: res_array?.[0],
|
||||||
insert_return,
|
insert_return,
|
||||||
count,
|
count,
|
||||||
|
db_res: res,
|
||||||
};
|
};
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export default async function DbDelete<
|
|||||||
{
|
{
|
||||||
query: {
|
query: {
|
||||||
id: {
|
id: {
|
||||||
value: String(targetId),
|
value: Number(targetId),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -77,8 +77,9 @@ export default async function DbDelete<
|
|||||||
|
|
||||||
if (!res.success) {
|
if (!res.success) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
|
||||||
msg: "Database delete failed",
|
msg: "Database delete failed",
|
||||||
|
...res,
|
||||||
|
success: false,
|
||||||
debug: {
|
debug: {
|
||||||
sqlObj,
|
sqlObj,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -71,8 +71,9 @@ export default async function DbInsert<
|
|||||||
|
|
||||||
if (!res.success) {
|
if (!res.success) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
|
||||||
msg: "Database insert failed",
|
msg: "Database insert failed",
|
||||||
|
...res,
|
||||||
|
success: false,
|
||||||
debug: {
|
debug: {
|
||||||
sqlObj,
|
sqlObj,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export default async function DbSelect<
|
|||||||
{
|
{
|
||||||
query: {
|
query: {
|
||||||
id: {
|
id: {
|
||||||
value: String(targetId),
|
value: Number(targetId),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -63,8 +63,9 @@ export default async function DbSelect<
|
|||||||
|
|
||||||
if (!res.success) {
|
if (!res.success) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
|
||||||
msg: "Database select failed",
|
msg: "Database select failed",
|
||||||
|
...res,
|
||||||
|
success: false,
|
||||||
debug: {
|
debug: {
|
||||||
sqlObj,
|
sqlObj,
|
||||||
sql: sqlObj.string,
|
sql: sqlObj.string,
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ export default async function DbSQL<
|
|||||||
|
|
||||||
if (!res.success) {
|
if (!res.success) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
|
||||||
msg: "Database query failed",
|
msg: "Database query failed",
|
||||||
|
...res,
|
||||||
|
success: false,
|
||||||
debug: {
|
debug: {
|
||||||
sqlObj: {
|
sqlObj: {
|
||||||
sql: trimmedSql,
|
sql: trimmedSql,
|
||||||
@@ -37,7 +38,10 @@ export default async function DbSQL<
|
|||||||
const singleRaw = res.single_res as any;
|
const singleRaw = res.single_res as any;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
...res,
|
||||||
|
success: isSelect
|
||||||
|
? Boolean(single_res) || Boolean(payload?.[0])
|
||||||
|
: true,
|
||||||
payload,
|
payload,
|
||||||
single_res,
|
single_res,
|
||||||
debug: {
|
debug: {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export default async function DbUpdate<
|
|||||||
{
|
{
|
||||||
query: {
|
query: {
|
||||||
id: {
|
id: {
|
||||||
value: String(targetId),
|
value: Number(targetId),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -106,6 +106,10 @@ export default async function DbUpdate<
|
|||||||
config,
|
config,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (res.error) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
sqlObj.string = sql;
|
sqlObj.string = sql;
|
||||||
sqlObj.values = values as any[];
|
sqlObj.values = values as any[];
|
||||||
|
|
||||||
@@ -113,22 +117,7 @@ export default async function DbUpdate<
|
|||||||
let updated_sql_values: any[] = [];
|
let updated_sql_values: any[] = [];
|
||||||
|
|
||||||
updated_sql += `SELECT * FROM ${quoteIdentifier(table)} ${whereClause}`;
|
updated_sql += `SELECT * FROM ${quoteIdentifier(table)} ${whereClause}`;
|
||||||
updated_sql_values = [...updated_sql_values, ...sqlQueryObj.values];
|
updated_sql_values = [...sqlQueryObj.values];
|
||||||
updated_sql += ` AND `;
|
|
||||||
for (let i = 0; i < keys.length; i++) {
|
|
||||||
const key = keys[i];
|
|
||||||
if (!key) continue;
|
|
||||||
if (key == "updated_at") continue;
|
|
||||||
|
|
||||||
const isLast = i == keys.length - 1;
|
|
||||||
|
|
||||||
updated_sql += ` ${quoteIdentifier(key)}=?`;
|
|
||||||
updated_sql_values.push(finalData[key] ?? null);
|
|
||||||
|
|
||||||
if (!isLast) {
|
|
||||||
updated_sql += ` AND `;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updated_res = await dbHandler({
|
const updated_res = await dbHandler({
|
||||||
query: updated_sql,
|
query: updated_sql,
|
||||||
@@ -136,17 +125,12 @@ export default async function DbUpdate<
|
|||||||
config,
|
config,
|
||||||
});
|
});
|
||||||
|
|
||||||
const affected_rows = updated_res.payload?.length;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...res,
|
...updated_res,
|
||||||
success: Boolean(affected_rows),
|
|
||||||
insert_return: {
|
|
||||||
affected_rows,
|
|
||||||
},
|
|
||||||
debug: {
|
debug: {
|
||||||
sqlObj,
|
sqlObj,
|
||||||
},
|
},
|
||||||
|
db_res: res.db_res,
|
||||||
};
|
};
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -3,8 +3,14 @@ import isVectorField from "./is-vector-field";
|
|||||||
import mapDataType from "./map-data-types";
|
import mapDataType from "./map-data-types";
|
||||||
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
|
|
||||||
|
export type BuildColumnDefinitionOptions = {
|
||||||
|
/** UNIQUE is managed by syncUniqueConstraints on existing tables */
|
||||||
|
omitUnique?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export default function buildColumnDefinition(
|
export default function buildColumnDefinition(
|
||||||
field: BUN_MARIADB_FieldSchemaType,
|
field: BUN_MARIADB_FieldSchemaType,
|
||||||
|
options: BuildColumnDefinitionOptions = {},
|
||||||
): string {
|
): string {
|
||||||
if (!field.fieldName) {
|
if (!field.fieldName) {
|
||||||
throw new Error("Field name is required");
|
throw new Error("Field name is required");
|
||||||
@@ -29,7 +35,12 @@ export default function buildColumnDefinition(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// VECTOR columns cannot be UNIQUE in the usual sense
|
// VECTOR columns cannot be UNIQUE in the usual sense
|
||||||
if (field.unique && !field.primaryKey && !isVectorField(field)) {
|
if (
|
||||||
|
!options.omitUnique &&
|
||||||
|
field.unique &&
|
||||||
|
!field.primaryKey &&
|
||||||
|
!isVectorField(field)
|
||||||
|
) {
|
||||||
parts.push("UNIQUE");
|
parts.push("UNIQUE");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,3 +62,12 @@ export default function buildColumnDefinition(
|
|||||||
|
|
||||||
return parts.join(" ");
|
return parts.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether schema field requires NOT NULL */
|
||||||
|
export function fieldRequiresNotNull(
|
||||||
|
field: BUN_MARIADB_FieldSchemaType,
|
||||||
|
): boolean {
|
||||||
|
return Boolean(
|
||||||
|
field.notNullValue || field.primaryKey || isVectorField(field),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,29 @@
|
|||||||
import type { BUN_MARIADB_FieldSchemaType } from "../../types";
|
import type { BUN_MARIADB_FieldSchemaType } from "../../types";
|
||||||
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
|
|
||||||
|
export function defaultForeignKeyName(
|
||||||
|
tableName: string,
|
||||||
|
fieldName: string,
|
||||||
|
): string {
|
||||||
|
return `fk_${tableName}_${fieldName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveForeignKeyName(
|
||||||
|
field: BUN_MARIADB_FieldSchemaType,
|
||||||
|
tableName: string,
|
||||||
|
): string {
|
||||||
|
const fieldName = field.fieldName || "column";
|
||||||
|
return field.foreignKey?.foreignKeyName || defaultForeignKeyName(tableName, fieldName);
|
||||||
|
}
|
||||||
|
|
||||||
export default function buildForeignKeyConstraint(
|
export default function buildForeignKeyConstraint(
|
||||||
field: BUN_MARIADB_FieldSchemaType,
|
field: BUN_MARIADB_FieldSchemaType,
|
||||||
|
tableName: string,
|
||||||
): string {
|
): string {
|
||||||
const fk = field.foreignKey!;
|
const fk = field.foreignKey!;
|
||||||
const constraintName = fk.foreignKeyName
|
const constraintName = resolveForeignKeyName(field, tableName);
|
||||||
? `CONSTRAINT ${MariaDBQuoteGen(fk.foreignKeyName)} `
|
|
||||||
: "";
|
|
||||||
|
|
||||||
let constraint = `${constraintName}FOREIGN KEY (${MariaDBQuoteGen(field.fieldName!)}) REFERENCES ${MariaDBQuoteGen(fk.destinationTableName!)}(${MariaDBQuoteGen(fk.destinationTableColumnName!)})`;
|
let constraint = `CONSTRAINT ${MariaDBQuoteGen(constraintName)} FOREIGN KEY (${MariaDBQuoteGen(field.fieldName!)}) REFERENCES ${MariaDBQuoteGen(fk.destinationTableName!)}(${MariaDBQuoteGen(fk.destinationTableColumnName!)})`;
|
||||||
|
|
||||||
if (fk.cascadeDelete) {
|
if (fk.cascadeDelete) {
|
||||||
constraint += " ON DELETE CASCADE";
|
constraint += " ON DELETE CASCADE";
|
||||||
|
|||||||
@@ -30,8 +30,10 @@ export default async function createTable({
|
|||||||
primaryKeys.push(field.fieldName);
|
primaryKeys.push(field.fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.foreignKey && !table.isVector) {
|
if (field.foreignKey) {
|
||||||
foreignKeys.push(buildForeignKeyConstraint(field));
|
foreignKeys.push(
|
||||||
|
buildForeignKeyConstraint(field, table.tableName),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,23 +44,23 @@ export default async function createTable({
|
|||||||
|
|
||||||
if (table.uniqueConstraints) {
|
if (table.uniqueConstraints) {
|
||||||
for (const constraint of table.uniqueConstraints) {
|
for (const constraint of table.uniqueConstraints) {
|
||||||
if (
|
const columns = (constraint.constraintTableFields || [])
|
||||||
constraint.constraintTableFields &&
|
.map((field) => field.value)
|
||||||
constraint.constraintTableFields.length > 0
|
.filter((value): value is string => Boolean(value));
|
||||||
) {
|
|
||||||
const fields = constraint.constraintTableFields
|
if (columns.length === 0) {
|
||||||
.map((field) => MariaDBQuoteGen(field.value))
|
continue;
|
||||||
.join(", ");
|
}
|
||||||
|
|
||||||
|
const fields = columns.map((col) => MariaDBQuoteGen(col)).join(", ");
|
||||||
const constraintName =
|
const constraintName =
|
||||||
constraint.constraintName ||
|
constraint.constraintName || `unique_${columns.join("_")}`;
|
||||||
`unique_${fields.replace(/`/g, "")}`;
|
|
||||||
|
|
||||||
columnDefinitions.push(
|
columnDefinitions.push(
|
||||||
`CONSTRAINT ${MariaDBQuoteGen(constraintName)} UNIQUE (${fields})`,
|
`CONSTRAINT ${MariaDBQuoteGen(constraintName)} UNIQUE (${fields})`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const sql = `CREATE TABLE IF NOT EXISTS ${MariaDBQuoteGen(table.tableName)} (${[...columnDefinitions, ...foreignKeys].join(", ")})${buildTableOptions(table)}`;
|
const sql = `CREATE TABLE IF NOT EXISTS ${MariaDBQuoteGen(table.tableName)} (${[...columnDefinitions, ...foreignKeys].join(", ")})${buildTableOptions(table)}`;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import type { BunMariaDBConfig } from "../../types";
|
||||||
|
import { querySchemaRows } from "./run-schema-query";
|
||||||
|
import schemaCondition from "./schema-condition";
|
||||||
|
|
||||||
|
export type ColumnInfoRow = {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
comment?: string;
|
||||||
|
isNullable: boolean;
|
||||||
|
columnDefault: string | null;
|
||||||
|
extra: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function getTableColumnsGemini({
|
||||||
|
tableName,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
tableName: string;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<ColumnInfoRow[]> {
|
||||||
|
const schemaCond = schemaCondition(config);
|
||||||
|
const rows = await querySchemaRows<{
|
||||||
|
COLUMN_NAME: string;
|
||||||
|
COLUMN_TYPE: string;
|
||||||
|
COLUMN_COMMENT: string;
|
||||||
|
IS_NULLABLE: string;
|
||||||
|
COLUMN_DEFAULT: string | null;
|
||||||
|
EXTRA: string;
|
||||||
|
IS_JSON: number | boolean;
|
||||||
|
}>({
|
||||||
|
query: `
|
||||||
|
SELECT
|
||||||
|
c.COLUMN_NAME,
|
||||||
|
c.COLUMN_TYPE,
|
||||||
|
c.COLUMN_COMMENT,
|
||||||
|
c.IS_NULLABLE,
|
||||||
|
c.COLUMN_DEFAULT,
|
||||||
|
c.EXTRA,
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.CHECK_CONSTRAINTS cc
|
||||||
|
WHERE cc.CONSTRAINT_SCHEMA = c.TABLE_SCHEMA
|
||||||
|
AND cc.TABLE_NAME = c.TABLE_NAME
|
||||||
|
AND cc.CHECK_CLAUSE LIKE CONCAT('%json_valid(\`', c.COLUMN_NAME, '\`)%')
|
||||||
|
) AS IS_JSON
|
||||||
|
FROM information_schema.COLUMNS c
|
||||||
|
WHERE ${schemaCond.where.replace(/\bTABLE_SCHEMA\b/g, "c.TABLE_SCHEMA")}
|
||||||
|
AND c.TABLE_NAME = ?
|
||||||
|
ORDER BY c.ORDINAL_POSITION
|
||||||
|
`,
|
||||||
|
values: [...schemaCond.values, tableName],
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
return rows.map((row) => {
|
||||||
|
const isJson = Number(row.IS_JSON) === 1;
|
||||||
|
|
||||||
|
// Normalize longtext with a json_valid constraint to "json"
|
||||||
|
let resolvedType = row.COLUMN_TYPE;
|
||||||
|
if (isJson && row.COLUMN_TYPE.toLowerCase() === "longtext") {
|
||||||
|
resolvedType = "json";
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: row.COLUMN_NAME,
|
||||||
|
type: resolvedType,
|
||||||
|
comment: row.COLUMN_COMMENT,
|
||||||
|
isNullable: String(row.IS_NULLABLE).toUpperCase() === "YES",
|
||||||
|
columnDefault: row.COLUMN_DEFAULT,
|
||||||
|
extra: row.EXTRA || "",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -6,6 +6,9 @@ export type ColumnInfoRow = {
|
|||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
comment?: string;
|
comment?: string;
|
||||||
|
isNullable: boolean;
|
||||||
|
columnDefault: string | null;
|
||||||
|
extra: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function getTableColumns({
|
export default async function getTableColumns({
|
||||||
@@ -20,8 +23,11 @@ export default async function getTableColumns({
|
|||||||
COLUMN_NAME: string;
|
COLUMN_NAME: string;
|
||||||
COLUMN_TYPE: string;
|
COLUMN_TYPE: string;
|
||||||
COLUMN_COMMENT: string;
|
COLUMN_COMMENT: string;
|
||||||
|
IS_NULLABLE: string;
|
||||||
|
COLUMN_DEFAULT: string | null;
|
||||||
|
EXTRA: string;
|
||||||
}>({
|
}>({
|
||||||
query: `SELECT COLUMN_NAME, COLUMN_TYPE, COLUMN_COMMENT FROM information_schema.COLUMNS WHERE ${schemaCond.where} AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION`,
|
query: `SELECT COLUMN_NAME, COLUMN_TYPE, COLUMN_COMMENT, IS_NULLABLE, COLUMN_DEFAULT, EXTRA FROM information_schema.COLUMNS WHERE ${schemaCond.where} AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION`,
|
||||||
values: [...schemaCond.values, tableName],
|
values: [...schemaCond.values, tableName],
|
||||||
config,
|
config,
|
||||||
});
|
});
|
||||||
@@ -30,5 +36,8 @@ export default async function getTableColumns({
|
|||||||
name: row.COLUMN_NAME,
|
name: row.COLUMN_NAME,
|
||||||
type: row.COLUMN_TYPE,
|
type: row.COLUMN_TYPE,
|
||||||
comment: row.COLUMN_COMMENT,
|
comment: row.COLUMN_COMMENT,
|
||||||
|
isNullable: String(row.IS_NULLABLE).toUpperCase() === "YES",
|
||||||
|
columnDefault: row.COLUMN_DEFAULT,
|
||||||
|
extra: row.EXTRA || "",
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,14 @@ import MariaDBQuoteGen from "./mariadb-quote-gen";
|
|||||||
import resolveTable from "./resolve-table";
|
import resolveTable from "./resolve-table";
|
||||||
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
||||||
import schemaCondition from "./schema-condition";
|
import schemaCondition from "./schema-condition";
|
||||||
|
import {
|
||||||
|
dropObsoleteForeignKeys,
|
||||||
|
ensureForeignKeys,
|
||||||
|
} from "./sync-foreign-keys";
|
||||||
import syncIndexes from "./sync-indexes";
|
import syncIndexes from "./sync-indexes";
|
||||||
|
import syncPrimaryKey from "./sync-primary-key";
|
||||||
|
import syncTableOptions from "./sync-table-options";
|
||||||
|
import syncUniqueConstraints from "./sync-unique-constraints";
|
||||||
import updateTable from "./update-table";
|
import updateTable from "./update-table";
|
||||||
import upsertDbManagerTable, {
|
import upsertDbManagerTable, {
|
||||||
removeDbManagerTable,
|
removeDbManagerTable,
|
||||||
@@ -21,7 +28,6 @@ export default async function handleDBSchemaTable({
|
|||||||
|
|
||||||
let tableExistsTracked = Boolean(db_manager_table_name);
|
let tableExistsTracked = Boolean(db_manager_table_name);
|
||||||
let tableExistsLive = Boolean(existing_live_table?.TABLE_NAME);
|
let tableExistsLive = Boolean(existing_live_table?.TABLE_NAME);
|
||||||
let wasRenamed = false;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
resolvedTable.tableNameOld &&
|
resolvedTable.tableNameOld &&
|
||||||
@@ -53,7 +59,6 @@ export default async function handleDBSchemaTable({
|
|||||||
});
|
});
|
||||||
tableExistsTracked = true;
|
tableExistsTracked = true;
|
||||||
tableExistsLive = true;
|
tableExistsLive = true;
|
||||||
wasRenamed = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,17 +69,27 @@ export default async function handleDBSchemaTable({
|
|||||||
config,
|
config,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (!wasRenamed) {
|
// Columns first (also drops FKs on removed/modified columns)
|
||||||
await updateTable({
|
await updateTable({
|
||||||
table: resolvedTable,
|
table: resolvedTable,
|
||||||
config,
|
config,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
await upsertDbManagerTable({
|
await upsertDbManagerTable({
|
||||||
tableName: resolvedTable.tableName,
|
tableName: resolvedTable.tableName,
|
||||||
config,
|
config,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Order matters:
|
||||||
|
// 1. Table options (engine/collation)
|
||||||
|
// 2. Drop obsolete/changed FKs (unlocks index cleanup)
|
||||||
|
// 3. Primary key
|
||||||
|
// 4. Indexes / uniques (FK columns need supporting indexes)
|
||||||
|
// 5. Ensure foreign keys exist
|
||||||
|
await syncTableOptions({ table: resolvedTable, config });
|
||||||
|
await dropObsoleteForeignKeys({ table: resolvedTable, config });
|
||||||
|
await syncPrimaryKey({ table: resolvedTable, config });
|
||||||
await syncIndexes({ table: resolvedTable, config });
|
await syncIndexes({ table: resolvedTable, config });
|
||||||
|
await syncUniqueConstraints({ table: resolvedTable, config });
|
||||||
|
await ensureForeignKeys({ table: resolvedTable, config });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,7 +91,8 @@ export default async function handleDBSchemaTables(
|
|||||||
schemaTableNames.includes(row.TABLE_NAME) ||
|
schemaTableNames.includes(row.TABLE_NAME) ||
|
||||||
!tablesToDrop.includes(row.TABLE_NAME)
|
!tablesToDrop.includes(row.TABLE_NAME)
|
||||||
) {
|
) {
|
||||||
const list = referencedByRemaining.get(row.REFERENCED_TABLE_NAME) || [];
|
const list =
|
||||||
|
referencedByRemaining.get(row.REFERENCED_TABLE_NAME) || [];
|
||||||
if (!list.includes(row.TABLE_NAME)) {
|
if (!list.includes(row.TABLE_NAME)) {
|
||||||
list.push(row.TABLE_NAME);
|
list.push(row.TABLE_NAME);
|
||||||
}
|
}
|
||||||
@@ -118,6 +119,7 @@ export default async function handleDBSchemaTables(
|
|||||||
}
|
}
|
||||||
|
|
||||||
await runSchemaQuery({ query: `SET FOREIGN_KEY_CHECKS = 0`, config });
|
await runSchemaQuery({ query: `SET FOREIGN_KEY_CHECKS = 0`, config });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (const tableName of safeToDrop) {
|
for (const tableName of safeToDrop) {
|
||||||
console.log(`Dropping table: ${tableName}`);
|
console.log(`Dropping table: ${tableName}`);
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ export default function mapDataType(
|
|||||||
|
|
||||||
switch (dataType) {
|
switch (dataType) {
|
||||||
case "CHAR":
|
case "CHAR":
|
||||||
return `CHAR(${field.integerLength || 255})`;
|
return `CHAR(${field.dataLength || 255})`;
|
||||||
case "VARCHAR":
|
case "VARCHAR":
|
||||||
return `VARCHAR(${field.integerLength || 255})`;
|
return `VARCHAR(${field.dataLength || 255})`;
|
||||||
case "TEXT":
|
case "TEXT":
|
||||||
return "TEXT";
|
return "TEXT";
|
||||||
case "TINYTEXT":
|
case "TINYTEXT":
|
||||||
@@ -25,36 +25,34 @@ export default function mapDataType(
|
|||||||
case "LONGTEXT":
|
case "LONGTEXT":
|
||||||
return "LONGTEXT";
|
return "LONGTEXT";
|
||||||
case "TINYINT":
|
case "TINYINT":
|
||||||
return field.integerLength
|
return field.dataLength
|
||||||
? `TINYINT(${field.integerLength})`
|
? `TINYINT(${field.dataLength})`
|
||||||
: "TINYINT";
|
: "TINYINT";
|
||||||
case "SMALLINT":
|
case "SMALLINT":
|
||||||
return field.integerLength
|
return field.dataLength
|
||||||
? `SMALLINT(${field.integerLength})`
|
? `SMALLINT(${field.dataLength})`
|
||||||
: "SMALLINT";
|
: "SMALLINT";
|
||||||
case "MEDIUMINT":
|
case "MEDIUMINT":
|
||||||
return field.integerLength
|
return field.dataLength
|
||||||
? `MEDIUMINT(${field.integerLength})`
|
? `MEDIUMINT(${field.dataLength})`
|
||||||
: "MEDIUMINT";
|
: "MEDIUMINT";
|
||||||
case "INT":
|
case "INT":
|
||||||
return field.integerLength ? `INT(${field.integerLength})` : "INT";
|
return field.dataLength ? `INT(${field.dataLength})` : "INT";
|
||||||
case "BIGINT":
|
case "BIGINT":
|
||||||
return field.integerLength
|
return field.dataLength ? `BIGINT(${field.dataLength})` : "BIGINT";
|
||||||
? `BIGINT(${field.integerLength})`
|
|
||||||
: "BIGINT";
|
|
||||||
case "FLOAT":
|
case "FLOAT":
|
||||||
return "FLOAT";
|
return "FLOAT";
|
||||||
case "DOUBLE":
|
case "DOUBLE":
|
||||||
return "DOUBLE";
|
return "DOUBLE";
|
||||||
case "DECIMAL":
|
case "DECIMAL":
|
||||||
if (field.integerLength && field.decimals) {
|
if (field.dataLength && field.decimals) {
|
||||||
return `DECIMAL(${field.integerLength}, ${field.decimals})`;
|
return `DECIMAL(${field.dataLength}, ${field.decimals})`;
|
||||||
}
|
}
|
||||||
return "DECIMAL(10,2)";
|
return "DECIMAL(10,2)";
|
||||||
case "BINARY":
|
case "BINARY":
|
||||||
return `BINARY(${field.integerLength || 1})`;
|
return `BINARY(${field.dataLength || 1})`;
|
||||||
case "VARBINARY":
|
case "VARBINARY":
|
||||||
return `VARBINARY(${field.integerLength || 255})`;
|
return `VARBINARY(${field.dataLength || 255})`;
|
||||||
case "BLOB":
|
case "BLOB":
|
||||||
return "BLOB";
|
return "BLOB";
|
||||||
case "TINYBLOB":
|
case "TINYBLOB":
|
||||||
@@ -74,11 +72,12 @@ export default function mapDataType(
|
|||||||
case "YEAR":
|
case "YEAR":
|
||||||
return "YEAR";
|
return "YEAR";
|
||||||
case "UUID":
|
case "UUID":
|
||||||
return "CHAR(36)"; // MariaDB does not have a native UUID type
|
return "UUID";
|
||||||
case "JSON":
|
case "JSON":
|
||||||
return "JSON";
|
return "JSON";
|
||||||
case "INET6":
|
case "INET6":
|
||||||
return "INET6";
|
return "INET6";
|
||||||
|
case "BOOL":
|
||||||
case "BOOLEAN":
|
case "BOOLEAN":
|
||||||
return "TINYINT(1)";
|
return "TINYINT(1)";
|
||||||
case "ENUM": {
|
case "ENUM": {
|
||||||
|
|||||||
@@ -1,148 +0,0 @@
|
|||||||
import type {
|
|
||||||
BUN_MARIADB_TableSchemaType,
|
|
||||||
BunMariaDBConfig,
|
|
||||||
} from "../../types";
|
|
||||||
import createTable from "./create-table";
|
|
||||||
import getTableColumns from "./get-table-columns";
|
|
||||||
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
|
||||||
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
|
||||||
import schemaCondition from "./schema-condition";
|
|
||||||
|
|
||||||
async function checkIfTableExists({
|
|
||||||
tableName,
|
|
||||||
config,
|
|
||||||
}: {
|
|
||||||
tableName: string;
|
|
||||||
config?: BunMariaDBConfig;
|
|
||||||
}): Promise<boolean> {
|
|
||||||
const schemaCond = schemaCondition(config);
|
|
||||||
const rows = await querySchemaRows<{ table_exists: number }>({
|
|
||||||
query: `SELECT 1 AS \`table_exists\` FROM information_schema.TABLES WHERE ${schemaCond.where} AND TABLE_NAME = ? LIMIT 1`,
|
|
||||||
values: [...schemaCond.values, tableName],
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
return Boolean(rows[0]?.table_exists);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Full table rebuild. For `isVector` tables this drops and recreates in place
|
|
||||||
* (preserving rows when possible). For regular tables it uses a temp-table swap.
|
|
||||||
*/
|
|
||||||
export default async function recreateTable({
|
|
||||||
table,
|
|
||||||
config,
|
|
||||||
}: {
|
|
||||||
table: BUN_MARIADB_TableSchemaType;
|
|
||||||
config?: BunMariaDBConfig;
|
|
||||||
}): Promise<void> {
|
|
||||||
const doesTableExist = await checkIfTableExists({
|
|
||||||
tableName: table.tableName,
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!doesTableExist) {
|
|
||||||
await createTable({ table, config });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vector tables: drop + recreate + reinsert (MariaDB VECTOR INDEX / dim
|
|
||||||
* changes are not reliably alterable in place).
|
|
||||||
*/
|
|
||||||
if (table.isVector) {
|
|
||||||
console.log(`Recreating vector table: ${table.tableName}`);
|
|
||||||
|
|
||||||
const existingRows = await querySchemaRows<Record<string, any>>({
|
|
||||||
query: `SELECT * FROM ${MariaDBQuoteGen(table.tableName)}`,
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
await runSchemaQuery({ query: `SET FOREIGN_KEY_CHECKS = 0`, config });
|
|
||||||
try {
|
|
||||||
await runSchemaQuery({
|
|
||||||
query: `DROP TABLE IF EXISTS ${MariaDBQuoteGen(table.tableName)}`,
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
await createTable({ table, config });
|
|
||||||
} finally {
|
|
||||||
await runSchemaQuery({ query: `SET FOREIGN_KEY_CHECKS = 1`, config });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingRows.length > 0) {
|
|
||||||
const schemaFieldNames = new Set(
|
|
||||||
(table.fields || [])
|
|
||||||
.map((f) => f.fieldName)
|
|
||||||
.filter((n): n is string => Boolean(n)),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const row of existingRows) {
|
|
||||||
const columns = Object.keys(row).filter((c) =>
|
|
||||||
schemaFieldNames.has(c),
|
|
||||||
);
|
|
||||||
if (columns.length === 0) continue;
|
|
||||||
|
|
||||||
const placeholders = columns.map(() => "?").join(", ");
|
|
||||||
const columnList = columns
|
|
||||||
.map((c) => MariaDBQuoteGen(c))
|
|
||||||
.join(", ");
|
|
||||||
|
|
||||||
await runSchemaQuery({
|
|
||||||
query: `INSERT INTO ${MariaDBQuoteGen(table.tableName)} (${columnList}) VALUES (${placeholders})`,
|
|
||||||
values: columns.map((c) => row[c] ?? null),
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tempTableName = `${table.tableName}_temp_${Date.now()}`;
|
|
||||||
const backupOldTableName = `${table.tableName}_old_${Date.now()}`;
|
|
||||||
const existingColumns = await getTableColumns({
|
|
||||||
tableName: table.tableName,
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
const columnsToKeep = (table.fields || [])
|
|
||||||
.filter((field) =>
|
|
||||||
existingColumns.some((column) => column.name === field.fieldName),
|
|
||||||
)
|
|
||||||
.map((field) => field.fieldName)
|
|
||||||
.filter((fieldName): fieldName is string => Boolean(fieldName));
|
|
||||||
|
|
||||||
await createTable({
|
|
||||||
table: { ...table, tableName: tempTableName },
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (columnsToKeep.length > 0) {
|
|
||||||
const columnList = columnsToKeep
|
|
||||||
.map((column) => MariaDBQuoteGen(column))
|
|
||||||
.join(", ");
|
|
||||||
|
|
||||||
await runSchemaQuery({
|
|
||||||
query: `INSERT INTO ${MariaDBQuoteGen(tempTableName)} (${columnList}) SELECT ${columnList} FROM ${MariaDBQuoteGen(table.tableName)}`,
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await runSchemaQuery({ query: `SET FOREIGN_KEY_CHECKS = 0`, config });
|
|
||||||
try {
|
|
||||||
await runSchemaQuery({
|
|
||||||
query: `RENAME TABLE ${MariaDBQuoteGen(table.tableName)} TO ${MariaDBQuoteGen(backupOldTableName)}`,
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
await runSchemaQuery({
|
|
||||||
query: `RENAME TABLE ${MariaDBQuoteGen(tempTableName)} TO ${MariaDBQuoteGen(table.tableName)}`,
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
await runSchemaQuery({
|
|
||||||
query: `DROP TABLE ${MariaDBQuoteGen(backupOldTableName)}`,
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
await runSchemaQuery({ query: `SET FOREIGN_KEY_CHECKS = 1`, config });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -41,8 +41,6 @@ export default function resolveTable(
|
|||||||
tableName: table.tableName,
|
tableName: table.tableName,
|
||||||
tableDescription: table.tableDescription || parentTable.tableDescription,
|
tableDescription: table.tableDescription || parentTable.tableDescription,
|
||||||
collation: table.collation || parentTable.collation,
|
collation: table.collation || parentTable.collation,
|
||||||
isVector:
|
|
||||||
table.isVector !== undefined ? table.isVector : parentTable.isVector,
|
|
||||||
fields: Array.from(mergedFieldsMap.values()),
|
fields: Array.from(mergedFieldsMap.values()),
|
||||||
indexes: _.uniqBy(
|
indexes: _.uniqBy(
|
||||||
[...(parentTable.indexes || []), ...(table.indexes || [])],
|
[...(parentTable.indexes || []), ...(table.indexes || [])],
|
||||||
|
|||||||
@@ -0,0 +1,306 @@
|
|||||||
|
import type {
|
||||||
|
BUN_MARIADB_FieldSchemaType,
|
||||||
|
BUN_MARIADB_TableSchemaType,
|
||||||
|
BunMariaDBConfig,
|
||||||
|
} from "../../types";
|
||||||
|
import buildForeignKeyConstraint, {
|
||||||
|
resolveForeignKeyName,
|
||||||
|
} from "./build-foreign-key-constraint";
|
||||||
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
|
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
||||||
|
|
||||||
|
export type DesiredForeignKey = {
|
||||||
|
name: string;
|
||||||
|
column: string;
|
||||||
|
refTable: string;
|
||||||
|
refColumn: string;
|
||||||
|
cascadeDelete: boolean;
|
||||||
|
cascadeUpdate: boolean;
|
||||||
|
field: BUN_MARIADB_FieldSchemaType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LiveForeignKey = {
|
||||||
|
name: string;
|
||||||
|
column: string;
|
||||||
|
refTable: string;
|
||||||
|
refColumn: string;
|
||||||
|
deleteRule: string;
|
||||||
|
updateRule: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeRule(rule: string | undefined | null): string {
|
||||||
|
return String(rule || "RESTRICT").toUpperCase().replace(/_/g, " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function ruleIsCascade(rule: string): boolean {
|
||||||
|
return normalizeRule(rule) === "CASCADE";
|
||||||
|
}
|
||||||
|
|
||||||
|
function fkIdentityKey(fk: {
|
||||||
|
column: string;
|
||||||
|
refTable: string;
|
||||||
|
refColumn: string;
|
||||||
|
}): string {
|
||||||
|
return `${fk.column}\0${fk.refTable}\0${fk.refColumn}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rulesMatch(
|
||||||
|
live: LiveForeignKey,
|
||||||
|
desired: DesiredForeignKey,
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
ruleIsCascade(live.deleteRule) === desired.cascadeDelete &&
|
||||||
|
ruleIsCascade(live.updateRule) === desired.cascadeUpdate
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function grabDesiredForeignKeys(
|
||||||
|
table: BUN_MARIADB_TableSchemaType,
|
||||||
|
): DesiredForeignKey[] {
|
||||||
|
const desired: DesiredForeignKey[] = [];
|
||||||
|
|
||||||
|
for (const field of table.fields || []) {
|
||||||
|
const fk = field.foreignKey;
|
||||||
|
if (
|
||||||
|
!field.fieldName ||
|
||||||
|
!fk?.destinationTableName ||
|
||||||
|
!fk.destinationTableColumnName
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
desired.push({
|
||||||
|
name: resolveForeignKeyName(field, table.tableName),
|
||||||
|
column: field.fieldName,
|
||||||
|
refTable: fk.destinationTableName,
|
||||||
|
refColumn: fk.destinationTableColumnName,
|
||||||
|
cascadeDelete: Boolean(fk.cascadeDelete),
|
||||||
|
cascadeUpdate: Boolean(fk.cascadeUpdate),
|
||||||
|
field,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return desired;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function grabLiveForeignKeys({
|
||||||
|
tableName,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
tableName: string;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<LiveForeignKey[]> {
|
||||||
|
const databaseName = config?.db_name || global.CONFIG?.db_name;
|
||||||
|
const schemaWhere = databaseName
|
||||||
|
? "rc.CONSTRAINT_SCHEMA = ?"
|
||||||
|
: "rc.CONSTRAINT_SCHEMA = DATABASE()";
|
||||||
|
const schemaValues = databaseName ? [databaseName] : [];
|
||||||
|
|
||||||
|
const rows = await querySchemaRows<{
|
||||||
|
CONSTRAINT_NAME: string;
|
||||||
|
COLUMN_NAME: string;
|
||||||
|
REFERENCED_TABLE_NAME: string;
|
||||||
|
REFERENCED_COLUMN_NAME: string;
|
||||||
|
DELETE_RULE: string;
|
||||||
|
UPDATE_RULE: string;
|
||||||
|
}>({
|
||||||
|
query: `
|
||||||
|
SELECT
|
||||||
|
rc.CONSTRAINT_NAME,
|
||||||
|
kcu.COLUMN_NAME,
|
||||||
|
kcu.REFERENCED_TABLE_NAME,
|
||||||
|
kcu.REFERENCED_COLUMN_NAME,
|
||||||
|
rc.DELETE_RULE,
|
||||||
|
rc.UPDATE_RULE
|
||||||
|
FROM information_schema.REFERENTIAL_CONSTRAINTS rc
|
||||||
|
INNER JOIN information_schema.KEY_COLUMN_USAGE kcu
|
||||||
|
ON rc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA
|
||||||
|
AND rc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME
|
||||||
|
AND rc.TABLE_NAME = kcu.TABLE_NAME
|
||||||
|
WHERE ${schemaWhere}
|
||||||
|
AND rc.TABLE_NAME = ?
|
||||||
|
AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
|
||||||
|
ORDER BY rc.CONSTRAINT_NAME, kcu.ORDINAL_POSITION
|
||||||
|
`,
|
||||||
|
values: [...schemaValues, tableName],
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Single-column FKs only (schema model is one FK per field)
|
||||||
|
const byName = new Map<string, LiveForeignKey>();
|
||||||
|
for (const row of rows) {
|
||||||
|
if (byName.has(row.CONSTRAINT_NAME)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
byName.set(row.CONSTRAINT_NAME, {
|
||||||
|
name: row.CONSTRAINT_NAME,
|
||||||
|
column: row.COLUMN_NAME,
|
||||||
|
refTable: row.REFERENCED_TABLE_NAME,
|
||||||
|
refColumn: row.REFERENCED_COLUMN_NAME,
|
||||||
|
deleteRule: row.DELETE_RULE,
|
||||||
|
updateRule: row.UPDATE_RULE,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...byName.values()];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dropForeignKey({
|
||||||
|
tableName,
|
||||||
|
constraintName,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
tableName: string;
|
||||||
|
constraintName: string;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void> {
|
||||||
|
console.log(
|
||||||
|
`Dropping foreign key: ${constraintName} on ${tableName}`,
|
||||||
|
);
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `ALTER TABLE ${MariaDBQuoteGen(tableName)} DROP FOREIGN KEY ${MariaDBQuoteGen(constraintName)}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dropForeignKeysOnColumns({
|
||||||
|
tableName,
|
||||||
|
columns,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
tableName: string;
|
||||||
|
columns: string[];
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void> {
|
||||||
|
if (columns.length === 0) return;
|
||||||
|
|
||||||
|
const live = await grabLiveForeignKeys({ tableName, config });
|
||||||
|
const colSet = new Set(columns);
|
||||||
|
|
||||||
|
for (const fk of live) {
|
||||||
|
if (colSet.has(fk.column)) {
|
||||||
|
await dropForeignKey({
|
||||||
|
tableName,
|
||||||
|
constraintName: fk.name,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop foreign keys that are removed from schema or need recreation
|
||||||
|
* (name/cascade/target changed). Run before index cleanup.
|
||||||
|
*/
|
||||||
|
export async function dropObsoleteForeignKeys({
|
||||||
|
table,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
table: BUN_MARIADB_TableSchemaType;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void> {
|
||||||
|
const desired = grabDesiredForeignKeys(table);
|
||||||
|
const live = await grabLiveForeignKeys({
|
||||||
|
tableName: table.tableName,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
const keepNames = new Set<string>();
|
||||||
|
const droppedNames = new Set<string>();
|
||||||
|
|
||||||
|
const dropOnce = async (name: string) => {
|
||||||
|
if (droppedNames.has(name)) return;
|
||||||
|
droppedNames.add(name);
|
||||||
|
await dropForeignKey({
|
||||||
|
tableName: table.tableName,
|
||||||
|
constraintName: name,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const want of desired) {
|
||||||
|
const byIdentity = live.find(
|
||||||
|
(existing) =>
|
||||||
|
!keepNames.has(existing.name) &&
|
||||||
|
!droppedNames.has(existing.name) &&
|
||||||
|
fkIdentityKey(existing) === fkIdentityKey(want),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
byIdentity &&
|
||||||
|
rulesMatch(byIdentity, want) &&
|
||||||
|
byIdentity.name === want.name
|
||||||
|
) {
|
||||||
|
keepNames.add(byIdentity.name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changed relationship → drop so ensureForeignKeys can recreate
|
||||||
|
if (byIdentity) {
|
||||||
|
await dropOnce(byIdentity.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
const byName = live.find((existing) => existing.name === want.name);
|
||||||
|
if (byName && byName !== byIdentity) {
|
||||||
|
await dropOnce(byName.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const existing of live) {
|
||||||
|
if (keepNames.has(existing.name) || droppedNames.has(existing.name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not in schema anymore
|
||||||
|
await dropOnce(existing.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure all desired foreign keys exist. Run after indexes/uniques.
|
||||||
|
*/
|
||||||
|
export async function ensureForeignKeys({
|
||||||
|
table,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
table: BUN_MARIADB_TableSchemaType;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void> {
|
||||||
|
const desired = grabDesiredForeignKeys(table);
|
||||||
|
const live = await grabLiveForeignKeys({
|
||||||
|
tableName: table.tableName,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const want of desired) {
|
||||||
|
const exists = live.some(
|
||||||
|
(existing) =>
|
||||||
|
fkIdentityKey(existing) === fkIdentityKey(want) &&
|
||||||
|
rulesMatch(existing, want) &&
|
||||||
|
existing.name === want.name,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Creating foreign key: ${want.name} (${want.column} → ${want.refTable}.${want.refColumn}) on ${table.tableName}`,
|
||||||
|
);
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} ADD ${buildForeignKeyConstraint(want.field, table.tableName)}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function syncForeignKeys({
|
||||||
|
table,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
table: BUN_MARIADB_TableSchemaType;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void> {
|
||||||
|
await dropObsoleteForeignKeys({ table, config });
|
||||||
|
await ensureForeignKeys({ table, config });
|
||||||
|
}
|
||||||
@@ -7,13 +7,13 @@ import isVectorField from "./is-vector-field";
|
|||||||
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
||||||
import schemaCondition from "./schema-condition";
|
import schemaCondition from "./schema-condition";
|
||||||
|
import { grabDesiredUniqueConstraints } from "./sync-unique-constraints";
|
||||||
|
|
||||||
function isVectorIndexDef(
|
function isVectorIndexDef(
|
||||||
index: BUN_MARIADB_IndexSchemaType,
|
index: BUN_MARIADB_IndexSchemaType,
|
||||||
table: BUN_MARIADB_TableSchemaType,
|
table: BUN_MARIADB_TableSchemaType,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (index.indexType === "VECTOR") return true;
|
if (index.indexType === "VECTOR") return true;
|
||||||
if (table.isVector) return true;
|
|
||||||
|
|
||||||
const firstFieldName = index.indexTableFields?.[0];
|
const firstFieldName = index.indexTableFields?.[0];
|
||||||
if (!firstFieldName) return false;
|
if (!firstFieldName) return false;
|
||||||
@@ -28,6 +28,87 @@ function vectorDistanceMetric(index: BUN_MARIADB_IndexSchemaType): string {
|
|||||||
return "euclidean";
|
return "euclidean";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Normalize schema index type vs information_schema.INDEX_TYPE */
|
||||||
|
function indexTypesMatch(
|
||||||
|
liveType: string,
|
||||||
|
schemaIndex: BUN_MARIADB_IndexSchemaType,
|
||||||
|
table: BUN_MARIADB_TableSchemaType,
|
||||||
|
): boolean {
|
||||||
|
const live = (liveType || "").toUpperCase();
|
||||||
|
const schemaType = schemaIndex.indexType?.toUpperCase();
|
||||||
|
|
||||||
|
if (isVectorIndexDef(schemaIndex, table)) {
|
||||||
|
// MariaDB may report VECTOR indexes as BTREE or VECTOR depending on version
|
||||||
|
return live === "VECTOR" || live === "BTREE" || live === "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!schemaType || schemaType === "BTREE") {
|
||||||
|
// Default / unspecified → BTREE
|
||||||
|
return live === "BTREE" || live === "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schemaType === "FULLTEXT") {
|
||||||
|
return live === "FULLTEXT";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schemaType === "SPATIAL") {
|
||||||
|
return live === "SPATIAL";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schemaType === "HASH") {
|
||||||
|
return live === "HASH";
|
||||||
|
}
|
||||||
|
|
||||||
|
return live === schemaType;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createIndex({
|
||||||
|
table,
|
||||||
|
index,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
table: BUN_MARIADB_TableSchemaType;
|
||||||
|
index: BUN_MARIADB_IndexSchemaType;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void> {
|
||||||
|
if (
|
||||||
|
!index.indexName ||
|
||||||
|
!index.indexTableFields ||
|
||||||
|
index.indexTableFields.length === 0
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVectorIndexDef(index, table)) {
|
||||||
|
console.log(`Creating Vector index: ${index.indexName}`);
|
||||||
|
const targetField = MariaDBQuoteGen(index.indexTableFields[0]!);
|
||||||
|
const distanceMetric = vectorDistanceMetric(index);
|
||||||
|
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} ADD VECTOR INDEX ${MariaDBQuoteGen(index.indexName)} (${targetField}) M=8 DISTANCE=${distanceMetric}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Creating standard index: ${index.indexName}`);
|
||||||
|
const fields = index.indexTableFields
|
||||||
|
.map((field) => MariaDBQuoteGen(field))
|
||||||
|
.join(", ");
|
||||||
|
const typeUpper = index.indexType?.toUpperCase();
|
||||||
|
const isSpecialType = typeUpper === "FULLTEXT" || typeUpper === "SPATIAL";
|
||||||
|
const indexPrefix = isSpecialType ? `${typeUpper} ` : "";
|
||||||
|
const indexSuffix =
|
||||||
|
!isSpecialType && (typeUpper === "BTREE" || typeUpper === "HASH")
|
||||||
|
? ` USING ${typeUpper}`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `CREATE ${indexPrefix}INDEX ${MariaDBQuoteGen(index.indexName)} ON ${MariaDBQuoteGen(table.tableName)} (${fields})${indexSuffix}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default async function syncIndexes({
|
export default async function syncIndexes({
|
||||||
table,
|
table,
|
||||||
config,
|
config,
|
||||||
@@ -62,16 +143,8 @@ export default async function syncIndexes({
|
|||||||
protectedConstraintRows.map((r) => r.CONSTRAINT_NAME),
|
protectedConstraintRows.map((r) => r.CONSTRAINT_NAME),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Column-level UNIQUE creates an index often named after the column
|
for (const constraint of grabDesiredUniqueConstraints(table)) {
|
||||||
for (const field of table.fields || []) {
|
protectedIndexNames.add(constraint.name);
|
||||||
if (field.unique && field.fieldName) {
|
|
||||||
protectedIndexNames.add(field.fieldName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const constraint of table.uniqueConstraints || []) {
|
|
||||||
if (constraint.constraintName) {
|
|
||||||
protectedIndexNames.add(constraint.constraintName);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingIndexesMap = new Map<
|
const existingIndexesMap = new Map<
|
||||||
@@ -94,7 +167,9 @@ export default async function syncIndexes({
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const schemaIndex = table.indexes?.find((i) => i.indexName === indexName);
|
const schemaIndex = table.indexes?.find(
|
||||||
|
(i) => i.indexName === indexName,
|
||||||
|
);
|
||||||
|
|
||||||
if (!schemaIndex) {
|
if (!schemaIndex) {
|
||||||
console.log(`Dropping index: ${indexName}`);
|
console.log(`Dropping index: ${indexName}`);
|
||||||
@@ -103,6 +178,7 @@ export default async function syncIndexes({
|
|||||||
query: `DROP INDEX ${MariaDBQuoteGen(indexName)} ON ${MariaDBQuoteGen(table.tableName)}`,
|
query: `DROP INDEX ${MariaDBQuoteGen(indexName)} ON ${MariaDBQuoteGen(table.tableName)}`,
|
||||||
config,
|
config,
|
||||||
});
|
});
|
||||||
|
existingIndexesMap.delete(indexName);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (
|
if (
|
||||||
String(err?.message || "").includes(
|
String(err?.message || "").includes(
|
||||||
@@ -121,8 +197,9 @@ export default async function syncIndexes({
|
|||||||
const columnsMatch =
|
const columnsMatch =
|
||||||
details.columns.length === schemaColumns.length &&
|
details.columns.length === schemaColumns.length &&
|
||||||
details.columns.every((col, idx) => col === schemaColumns[idx]);
|
details.columns.every((col, idx) => col === schemaColumns[idx]);
|
||||||
|
const typeMatch = indexTypesMatch(details.type, schemaIndex, table);
|
||||||
|
|
||||||
if (!columnsMatch) {
|
if (!columnsMatch || !typeMatch) {
|
||||||
console.log(`Recreating changed index: ${indexName}`);
|
console.log(`Recreating changed index: ${indexName}`);
|
||||||
await runSchemaQuery({
|
await runSchemaQuery({
|
||||||
query: `DROP INDEX ${MariaDBQuoteGen(indexName)} ON ${MariaDBQuoteGen(table.tableName)}`,
|
query: `DROP INDEX ${MariaDBQuoteGen(indexName)} ON ${MariaDBQuoteGen(table.tableName)}`,
|
||||||
@@ -143,35 +220,7 @@ export default async function syncIndexes({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!existingIndexesMap.has(index.indexName)) {
|
if (!existingIndexesMap.has(index.indexName)) {
|
||||||
if (isVectorIndexDef(index, table)) {
|
await createIndex({ table, index, config });
|
||||||
console.log(`Creating Vector index: ${index.indexName}`);
|
|
||||||
const targetField = MariaDBQuoteGen(index.indexTableFields[0]!);
|
|
||||||
const distanceMetric = vectorDistanceMetric(index);
|
|
||||||
|
|
||||||
await runSchemaQuery({
|
|
||||||
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} ADD VECTOR INDEX ${MariaDBQuoteGen(index.indexName)} (${targetField}) M=8 DISTANCE=${distanceMetric}`,
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log(`Creating standard index: ${index.indexName}`);
|
|
||||||
const fields = index.indexTableFields
|
|
||||||
.map((field) => MariaDBQuoteGen(field))
|
|
||||||
.join(", ");
|
|
||||||
const typeUpper = index.indexType?.toUpperCase();
|
|
||||||
const isSpecialType =
|
|
||||||
typeUpper === "FULLTEXT" || typeUpper === "SPATIAL";
|
|
||||||
const indexPrefix = isSpecialType ? `${typeUpper} ` : "";
|
|
||||||
const indexSuffix =
|
|
||||||
!isSpecialType &&
|
|
||||||
(typeUpper === "BTREE" || typeUpper === "HASH")
|
|
||||||
? ` USING ${typeUpper}`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
await runSchemaQuery({
|
|
||||||
query: `CREATE ${indexPrefix}INDEX ${MariaDBQuoteGen(index.indexName)} ON ${MariaDBQuoteGen(table.tableName)} (${fields})${indexSuffix}`,
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import type {
|
||||||
|
BUN_MARIADB_TableSchemaType,
|
||||||
|
BunMariaDBConfig,
|
||||||
|
} from "../../types";
|
||||||
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
|
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
||||||
|
import schemaCondition from "./schema-condition";
|
||||||
|
|
||||||
|
function columnsEqual(a: string[], b: string[]): boolean {
|
||||||
|
return a.length === b.length && a.every((col, i) => col === b[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function grabDesiredPrimaryKeyColumns(
|
||||||
|
table: BUN_MARIADB_TableSchemaType,
|
||||||
|
): string[] {
|
||||||
|
return (table.fields || [])
|
||||||
|
.filter((field) => field.primaryKey && field.fieldName)
|
||||||
|
.map((field) => field.fieldName!);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function grabLivePrimaryKeyColumns({
|
||||||
|
tableName,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
tableName: string;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<string[]> {
|
||||||
|
const schemaCond = schemaCondition(config);
|
||||||
|
const rows = await querySchemaRows<{
|
||||||
|
COLUMN_NAME: string;
|
||||||
|
ORDINAL_POSITION: number;
|
||||||
|
}>({
|
||||||
|
query: `
|
||||||
|
SELECT COLUMN_NAME, ORDINAL_POSITION
|
||||||
|
FROM information_schema.KEY_COLUMN_USAGE
|
||||||
|
WHERE ${schemaCond.where}
|
||||||
|
AND TABLE_NAME = ?
|
||||||
|
AND CONSTRAINT_NAME = 'PRIMARY'
|
||||||
|
ORDER BY ORDINAL_POSITION
|
||||||
|
`,
|
||||||
|
values: [...schemaCond.values, tableName],
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
return rows.map((row) => row.COLUMN_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function syncPrimaryKey({
|
||||||
|
table,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
table: BUN_MARIADB_TableSchemaType;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void> {
|
||||||
|
const desired = grabDesiredPrimaryKeyColumns(table);
|
||||||
|
const live = await grabLivePrimaryKeyColumns({
|
||||||
|
tableName: table.tableName,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (columnsEqual(desired, live)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (live.length > 0) {
|
||||||
|
console.log(`Dropping primary key on ${table.tableName}`);
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} DROP PRIMARY KEY`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (desired.length > 0) {
|
||||||
|
const cols = desired.map((col) => MariaDBQuoteGen(col)).join(", ");
|
||||||
|
console.log(
|
||||||
|
`Creating primary key (${desired.join(", ")}) on ${table.tableName}`,
|
||||||
|
);
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} ADD PRIMARY KEY (${cols})`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import type {
|
||||||
|
BUN_MARIADB_TableSchemaType,
|
||||||
|
BunMariaDBConfig,
|
||||||
|
} from "../../types";
|
||||||
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
|
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
||||||
|
import schemaCondition from "./schema-condition";
|
||||||
|
|
||||||
|
export default async function syncTableOptions({
|
||||||
|
table,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
table: BUN_MARIADB_TableSchemaType;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void> {
|
||||||
|
const schemaCond = schemaCondition(config);
|
||||||
|
const rows = await querySchemaRows<{
|
||||||
|
ENGINE: string;
|
||||||
|
TABLE_COLLATION: string;
|
||||||
|
}>({
|
||||||
|
query: `SELECT ENGINE, TABLE_COLLATION FROM information_schema.TABLES WHERE ${schemaCond.where} AND TABLE_NAME = ? AND TABLE_TYPE = 'BASE TABLE'`,
|
||||||
|
values: [...schemaCond.values, table.tableName],
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
const live = rows[0];
|
||||||
|
if (!live) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const alters: string[] = [];
|
||||||
|
|
||||||
|
if ((live.ENGINE || "").toUpperCase() !== "INNODB") {
|
||||||
|
alters.push("ENGINE=InnoDB");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (table.collation) {
|
||||||
|
const liveCollation = (live.TABLE_COLLATION || "").toLowerCase();
|
||||||
|
if (liveCollation !== table.collation.toLowerCase()) {
|
||||||
|
alters.push(
|
||||||
|
`CONVERT TO CHARACTER SET utf8mb4 COLLATE ${table.collation}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alters.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Updating table options on ${table.tableName}: ${alters.join(", ")}`,
|
||||||
|
);
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} ${alters.join(", ")}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
import type {
|
||||||
|
BUN_MARIADB_TableSchemaType,
|
||||||
|
BunMariaDBConfig,
|
||||||
|
} from "../../types";
|
||||||
|
import isVectorField from "./is-vector-field";
|
||||||
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
|
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
||||||
|
|
||||||
|
type UniqueConstraintLive = {
|
||||||
|
name: string;
|
||||||
|
columns: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type UniqueConstraintDesired = {
|
||||||
|
name: string;
|
||||||
|
columns: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function columnsKey(columns: string[]): string {
|
||||||
|
return columns.join("\0");
|
||||||
|
}
|
||||||
|
|
||||||
|
function columnsEqual(a: string[], b: string[]): boolean {
|
||||||
|
return a.length === b.length && a.every((col, i) => col === b[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultConstraintName(columns: string[]): string {
|
||||||
|
return `unique_${columns.join("_")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function grabDesiredUniqueConstraints(
|
||||||
|
table: BUN_MARIADB_TableSchemaType,
|
||||||
|
): UniqueConstraintDesired[] {
|
||||||
|
const desired: UniqueConstraintDesired[] = [];
|
||||||
|
const seenColumnSets = new Set<string>();
|
||||||
|
|
||||||
|
for (const constraint of table.uniqueConstraints || []) {
|
||||||
|
const columns = (constraint.constraintTableFields || [])
|
||||||
|
.map((field) => field.value)
|
||||||
|
.filter((value): value is string => Boolean(value));
|
||||||
|
|
||||||
|
if (columns.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = columnsKey(columns);
|
||||||
|
if (seenColumnSets.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seenColumnSets.add(key);
|
||||||
|
|
||||||
|
desired.push({
|
||||||
|
name: constraint.constraintName || defaultConstraintName(columns),
|
||||||
|
columns,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const field of table.fields || []) {
|
||||||
|
if (
|
||||||
|
!field.fieldName ||
|
||||||
|
!field.unique ||
|
||||||
|
field.primaryKey ||
|
||||||
|
isVectorField(field)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = [field.fieldName];
|
||||||
|
const key = columnsKey(columns);
|
||||||
|
if (seenColumnSets.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seenColumnSets.add(key);
|
||||||
|
|
||||||
|
// Matches MariaDB's default name for column-level UNIQUE
|
||||||
|
desired.push({
|
||||||
|
name: field.fieldName,
|
||||||
|
columns,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return desired;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function grabLiveUniqueConstraints({
|
||||||
|
tableName,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
tableName: string;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<UniqueConstraintLive[]> {
|
||||||
|
const databaseName = config?.db_name || global.CONFIG?.db_name;
|
||||||
|
const schemaWhere = databaseName
|
||||||
|
? "tc.TABLE_SCHEMA = ?"
|
||||||
|
: "tc.TABLE_SCHEMA = DATABASE()";
|
||||||
|
const schemaValues = databaseName ? [databaseName] : [];
|
||||||
|
|
||||||
|
const rows = await querySchemaRows<{
|
||||||
|
CONSTRAINT_NAME: string;
|
||||||
|
COLUMN_NAME: string;
|
||||||
|
ORDINAL_POSITION: number;
|
||||||
|
}>({
|
||||||
|
query: `
|
||||||
|
SELECT tc.CONSTRAINT_NAME, kcu.COLUMN_NAME, kcu.ORDINAL_POSITION
|
||||||
|
FROM information_schema.TABLE_CONSTRAINTS tc
|
||||||
|
INNER JOIN information_schema.KEY_COLUMN_USAGE kcu
|
||||||
|
ON tc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA
|
||||||
|
AND tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME
|
||||||
|
AND tc.TABLE_NAME = kcu.TABLE_NAME
|
||||||
|
WHERE ${schemaWhere}
|
||||||
|
AND tc.TABLE_NAME = ?
|
||||||
|
AND tc.CONSTRAINT_TYPE = 'UNIQUE'
|
||||||
|
ORDER BY tc.CONSTRAINT_NAME, kcu.ORDINAL_POSITION
|
||||||
|
`,
|
||||||
|
values: [...schemaValues, tableName],
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
const map = new Map<string, string[]>();
|
||||||
|
for (const row of rows) {
|
||||||
|
if (!map.has(row.CONSTRAINT_NAME)) {
|
||||||
|
map.set(row.CONSTRAINT_NAME, []);
|
||||||
|
}
|
||||||
|
map.get(row.CONSTRAINT_NAME)!.push(row.COLUMN_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...map.entries()].map(([name, columns]) => ({ name, columns }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function syncUniqueConstraints({
|
||||||
|
table,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
table: BUN_MARIADB_TableSchemaType;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void> {
|
||||||
|
const desired = grabDesiredUniqueConstraints(table);
|
||||||
|
const live = await grabLiveUniqueConstraints({
|
||||||
|
tableName: table.tableName,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
const matchedLiveNames = new Set<string>();
|
||||||
|
|
||||||
|
for (const want of desired) {
|
||||||
|
const byColumns = live.find(
|
||||||
|
(existing) =>
|
||||||
|
!matchedLiveNames.has(existing.name) &&
|
||||||
|
columnsEqual(existing.columns, want.columns),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (byColumns) {
|
||||||
|
matchedLiveNames.add(byColumns.name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const byName = live.find((existing) => existing.name === want.name);
|
||||||
|
if (byName && !matchedLiveNames.has(byName.name)) {
|
||||||
|
console.log(
|
||||||
|
`Recreating changed unique constraint: ${want.name} on ${table.tableName}`,
|
||||||
|
);
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} DROP INDEX ${MariaDBQuoteGen(byName.name)}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
matchedLiveNames.add(byName.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = want.columns.map((col) => MariaDBQuoteGen(col)).join(", ");
|
||||||
|
console.log(
|
||||||
|
`Creating unique constraint: ${want.name} (${want.columns.join(", ")}) on ${table.tableName}`,
|
||||||
|
);
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} ADD CONSTRAINT ${MariaDBQuoteGen(want.name)} UNIQUE (${fields})`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const existing of live) {
|
||||||
|
if (matchedLiveNames.has(existing.name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stillDesired = desired.some((want) =>
|
||||||
|
columnsEqual(want.columns, existing.columns),
|
||||||
|
);
|
||||||
|
if (stillDesired) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Dropping unique constraint: ${existing.name} on ${table.tableName}`,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} DROP INDEX ${MariaDBQuoteGen(existing.name)}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
if (
|
||||||
|
String(err?.message || "").includes(
|
||||||
|
"needed in a foreign key constraint",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
console.warn(
|
||||||
|
`Skipping drop of unique constraint ${existing.name}: required by a foreign key constraint`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+226
-36
@@ -3,15 +3,18 @@ import type {
|
|||||||
BUN_MARIADB_TableSchemaType,
|
BUN_MARIADB_TableSchemaType,
|
||||||
BunMariaDBConfig,
|
BunMariaDBConfig,
|
||||||
} from "../../types";
|
} from "../../types";
|
||||||
import buildColumnDefinition from "./build-column-definition";
|
import buildColumnDefinition, {
|
||||||
|
fieldRequiresNotNull,
|
||||||
|
} from "./build-column-definition";
|
||||||
import createTable from "./create-table";
|
import createTable from "./create-table";
|
||||||
import getTableColumns from "./get-table-columns";
|
import getTableColumns, { type ColumnInfoRow } from "./get-table-columns";
|
||||||
|
import getTableColumnsGemini from "./get-table-columns-gemnini";
|
||||||
import isVectorField from "./is-vector-field";
|
import isVectorField from "./is-vector-field";
|
||||||
import mapDataType from "./map-data-types";
|
import mapDataType from "./map-data-types";
|
||||||
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
import recreateTable from "./recreate-table";
|
|
||||||
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
||||||
import schemaCondition from "./schema-condition";
|
import schemaCondition from "./schema-condition";
|
||||||
|
import { dropForeignKeysOnColumns } from "./sync-foreign-keys";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compare live COLUMN_TYPE with schema-mapped type.
|
* Compare live COLUMN_TYPE with schema-mapped type.
|
||||||
@@ -54,6 +57,99 @@ function vectorTypeDiverged(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeDefault(value: string | null | undefined): string {
|
||||||
|
if (value == null) return "";
|
||||||
|
let v = String(value).trim();
|
||||||
|
// Strip surrounding quotes MariaDB may include
|
||||||
|
if (
|
||||||
|
(v.startsWith("'") && v.endsWith("'")) ||
|
||||||
|
(v.startsWith('"') && v.endsWith('"'))
|
||||||
|
) {
|
||||||
|
v = v.slice(1, -1);
|
||||||
|
}
|
||||||
|
return v.toLowerCase().replace(/\s+/g, " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectedDefault(field: BUN_MARIADB_FieldSchemaType): string | null {
|
||||||
|
if (field.defaultValue !== undefined) {
|
||||||
|
return String(field.defaultValue);
|
||||||
|
}
|
||||||
|
if (field.defaultValueLiteral) {
|
||||||
|
return field.defaultValueLiteral;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectedOnUpdate(field: BUN_MARIADB_FieldSchemaType): string | null {
|
||||||
|
if (field.onUpdate) return field.onUpdate;
|
||||||
|
if (field.onUpdateLiteral) return field.onUpdateLiteral;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultsMatch(liveDefault: string, expected: string): boolean {
|
||||||
|
if (liveDefault === expected) return true;
|
||||||
|
if (
|
||||||
|
expected.includes("current_timestamp") &&
|
||||||
|
liveDefault.includes("current_timestamp")
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function columnAttributesDiverged(
|
||||||
|
live: ColumnInfoRow,
|
||||||
|
field: BUN_MARIADB_FieldSchemaType,
|
||||||
|
): boolean {
|
||||||
|
const wantsNotNull = fieldRequiresNotNull(field);
|
||||||
|
if (wantsNotNull && live.isNullable) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!wantsNotNull && !live.isNullable && !field.primaryKey) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const liveExtra = (live.extra || "").toLowerCase();
|
||||||
|
const wantsAi = Boolean(field.autoIncrement);
|
||||||
|
const hasAi = liveExtra.includes("auto_increment");
|
||||||
|
if (wantsAi !== hasAi) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wantsDefault = expectedDefault(field);
|
||||||
|
if (wantsDefault != null) {
|
||||||
|
const liveDefault = normalizeDefault(live.columnDefault);
|
||||||
|
const expected = normalizeDefault(wantsDefault);
|
||||||
|
if (!defaultsMatch(liveDefault, expected)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const wantsOnUpdate = expectedOnUpdate(field);
|
||||||
|
const hasOnUpdate = /on update/i.test(live.extra || "");
|
||||||
|
if (wantsOnUpdate && !hasOnUpdate) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!wantsOnUpdate && hasOnUpdate) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (wantsOnUpdate && hasOnUpdate) {
|
||||||
|
const liveOnUpdate = normalizeDefault(
|
||||||
|
(live.extra.match(/on update\s+(.+)/i) || [])[1] || "",
|
||||||
|
);
|
||||||
|
const expectedOu = normalizeDefault(wantsOnUpdate);
|
||||||
|
if (
|
||||||
|
liveOnUpdate &&
|
||||||
|
expectedOu &&
|
||||||
|
!defaultsMatch(liveOnUpdate, expectedOu)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
async function addColumn({
|
async function addColumn({
|
||||||
tableName,
|
tableName,
|
||||||
field,
|
field,
|
||||||
@@ -64,7 +160,8 @@ async function addColumn({
|
|||||||
config?: BunMariaDBConfig;
|
config?: BunMariaDBConfig;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
console.log(`Adding column: ${tableName}.${field.fieldName}`);
|
console.log(`Adding column: ${tableName}.${field.fieldName}`);
|
||||||
const columnDef = buildColumnDefinition(field).trim();
|
// omitUnique — unique constraints are applied by syncUniqueConstraints
|
||||||
|
const columnDef = buildColumnDefinition(field, { omitUnique: true }).trim();
|
||||||
await runSchemaQuery({
|
await runSchemaQuery({
|
||||||
query: `ALTER TABLE ${MariaDBQuoteGen(tableName)} ADD COLUMN IF NOT EXISTS ${columnDef}`,
|
query: `ALTER TABLE ${MariaDBQuoteGen(tableName)} ADD COLUMN IF NOT EXISTS ${columnDef}`,
|
||||||
config,
|
config,
|
||||||
@@ -81,7 +178,7 @@ async function modifyColumn({
|
|||||||
config?: BunMariaDBConfig;
|
config?: BunMariaDBConfig;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
console.log(`Modifying column: ${tableName}.${field.fieldName}`);
|
console.log(`Modifying column: ${tableName}.${field.fieldName}`);
|
||||||
const columnDef = buildColumnDefinition(field).trim();
|
const columnDef = buildColumnDefinition(field, { omitUnique: true }).trim();
|
||||||
await runSchemaQuery({
|
await runSchemaQuery({
|
||||||
query: `ALTER TABLE ${MariaDBQuoteGen(tableName)} MODIFY COLUMN ${columnDef}`,
|
query: `ALTER TABLE ${MariaDBQuoteGen(tableName)} MODIFY COLUMN ${columnDef}`,
|
||||||
config,
|
config,
|
||||||
@@ -104,6 +201,76 @@ async function dropColumn({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop + re-add a column (values discarded). Used when VECTOR dimensions change —
|
||||||
|
* MODIFY cannot resize VECTOR, and a full table rebuild is unnecessary.
|
||||||
|
*/
|
||||||
|
async function recreateColumn({
|
||||||
|
tableName,
|
||||||
|
field,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
tableName: string;
|
||||||
|
field: BUN_MARIADB_FieldSchemaType;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void> {
|
||||||
|
if (!field.fieldName) return;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Recreating column: ${tableName}.${field.fieldName} (values will be cleared)`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await dropForeignKeysOnColumns({
|
||||||
|
tableName,
|
||||||
|
columns: [field.fieldName],
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
const schemaCond = schemaCondition(config);
|
||||||
|
const indexRows = await querySchemaRows<{
|
||||||
|
INDEX_NAME: string;
|
||||||
|
COLUMN_NAME: string;
|
||||||
|
}>({
|
||||||
|
query: `SELECT INDEX_NAME, COLUMN_NAME FROM information_schema.STATISTICS WHERE ${schemaCond.where} AND TABLE_NAME = ? AND INDEX_NAME <> 'PRIMARY'`,
|
||||||
|
values: [...schemaCond.values, tableName],
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
const indexesToDrop = new Set<string>();
|
||||||
|
for (const row of indexRows) {
|
||||||
|
if (row.COLUMN_NAME === field.fieldName) {
|
||||||
|
indexesToDrop.add(row.INDEX_NAME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const indexName of indexesToDrop) {
|
||||||
|
console.log(
|
||||||
|
`Dropping index ${indexName} because column ${field.fieldName} is being recreated`,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `DROP INDEX ${MariaDBQuoteGen(indexName)} ON ${MariaDBQuoteGen(tableName)}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
if (
|
||||||
|
String(err?.message || "").includes(
|
||||||
|
"needed in a foreign key constraint",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
console.warn(
|
||||||
|
`Skipping drop of index ${indexName}: required by a foreign key constraint`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await dropColumn({ tableName, fieldName: field.fieldName, config });
|
||||||
|
await addColumn({ tableName, field, config });
|
||||||
|
}
|
||||||
|
|
||||||
export default async function updateTable({
|
export default async function updateTable({
|
||||||
table,
|
table,
|
||||||
config,
|
config,
|
||||||
@@ -111,7 +278,7 @@ export default async function updateTable({
|
|||||||
table: BUN_MARIADB_TableSchemaType;
|
table: BUN_MARIADB_TableSchemaType;
|
||||||
config?: BunMariaDBConfig;
|
config?: BunMariaDBConfig;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const existingColumns = await getTableColumns({
|
const existingColumns = await getTableColumnsGemini({
|
||||||
tableName: table.tableName,
|
tableName: table.tableName,
|
||||||
config,
|
config,
|
||||||
});
|
});
|
||||||
@@ -122,10 +289,7 @@ export default async function updateTable({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const liveFieldsMap = new Map(
|
const liveFieldsMap = new Map(
|
||||||
existingColumns.map((col) => [
|
existingColumns.map((col) => [col.name, col]),
|
||||||
col.name,
|
|
||||||
{ type: col.type.toLowerCase(), comment: col.comment || "" },
|
|
||||||
]),
|
|
||||||
);
|
);
|
||||||
const codeFieldsMap = new Map(
|
const codeFieldsMap = new Map(
|
||||||
(table.fields || []).map((f) => [f.fieldName, f]),
|
(table.fields || []).map((f) => [f.fieldName, f]),
|
||||||
@@ -133,8 +297,8 @@ export default async function updateTable({
|
|||||||
|
|
||||||
const fieldsToAdd: BUN_MARIADB_FieldSchemaType[] = [];
|
const fieldsToAdd: BUN_MARIADB_FieldSchemaType[] = [];
|
||||||
const fieldsToModify: BUN_MARIADB_FieldSchemaType[] = [];
|
const fieldsToModify: BUN_MARIADB_FieldSchemaType[] = [];
|
||||||
|
const fieldsToRecreate: BUN_MARIADB_FieldSchemaType[] = [];
|
||||||
const fieldsToDrop: string[] = [];
|
const fieldsToDrop: string[] = [];
|
||||||
let needsVectorRecreate = false;
|
|
||||||
|
|
||||||
for (const field of table.fields || []) {
|
for (const field of table.fields || []) {
|
||||||
if (!field.fieldName) continue;
|
if (!field.fieldName) continue;
|
||||||
@@ -142,41 +306,37 @@ export default async function updateTable({
|
|||||||
const liveField = liveFieldsMap.get(field.fieldName);
|
const liveField = liveFieldsMap.get(field.fieldName);
|
||||||
|
|
||||||
if (!liveField) {
|
if (!liveField) {
|
||||||
// Adding a new vector column can require rebuild if VECTOR INDEX
|
|
||||||
// constraints conflict; still try surgical add first.
|
|
||||||
fieldsToAdd.push(field);
|
fieldsToAdd.push(field);
|
||||||
} else {
|
} else {
|
||||||
|
let mapped_data_type = mapDataType(field);
|
||||||
|
|
||||||
let typeDiverged = !columnTypesMatch(
|
let typeDiverged = !columnTypesMatch(
|
||||||
liveField.type,
|
liveField.type,
|
||||||
mapDataType(field),
|
mapped_data_type,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isVectorField(field)) {
|
if (isVectorField(field)) {
|
||||||
typeDiverged = vectorTypeDiverged(
|
typeDiverged = vectorTypeDiverged(
|
||||||
liveField.type,
|
liveField.type,
|
||||||
liveField.comment,
|
liveField.comment || "",
|
||||||
field,
|
field,
|
||||||
);
|
);
|
||||||
if (typeDiverged) {
|
if (typeDiverged) {
|
||||||
needsVectorRecreate = true;
|
// VECTOR dimensions / storage cannot be MODIFYed — drop + re-add column
|
||||||
|
fieldsToRecreate.push(field);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeDiverged) {
|
const attrsDiverged =
|
||||||
|
!typeDiverged && columnAttributesDiverged(liveField, field);
|
||||||
|
|
||||||
|
if (typeDiverged || attrsDiverged) {
|
||||||
fieldsToModify.push(field);
|
fieldsToModify.push(field);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vector dimension / storage type changes → full rebuild automatically
|
|
||||||
if (needsVectorRecreate) {
|
|
||||||
console.log(
|
|
||||||
`Vector column change detected on \`${table.tableName}\`; recreating table`,
|
|
||||||
);
|
|
||||||
await recreateTable({ table, config });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const col of existingColumns) {
|
for (const col of existingColumns) {
|
||||||
if (!codeFieldsMap.has(col.name)) {
|
if (!codeFieldsMap.has(col.name)) {
|
||||||
fieldsToDrop.push(col.name);
|
fieldsToDrop.push(col.name);
|
||||||
@@ -186,6 +346,7 @@ export default async function updateTable({
|
|||||||
if (
|
if (
|
||||||
fieldsToAdd.length === 0 &&
|
fieldsToAdd.length === 0 &&
|
||||||
fieldsToModify.length === 0 &&
|
fieldsToModify.length === 0 &&
|
||||||
|
fieldsToRecreate.length === 0 &&
|
||||||
fieldsToDrop.length === 0
|
fieldsToDrop.length === 0
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@@ -196,6 +357,13 @@ export default async function updateTable({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (fieldsToDrop.length > 0) {
|
if (fieldsToDrop.length > 0) {
|
||||||
|
// Drop FKs that reference columns being removed
|
||||||
|
await dropForeignKeysOnColumns({
|
||||||
|
tableName: table.tableName,
|
||||||
|
columns: fieldsToDrop,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
const schemaCond = schemaCondition(config);
|
const schemaCond = schemaCondition(config);
|
||||||
|
|
||||||
const pkRows = await querySchemaRows<{ COLUMN_NAME: string }>({
|
const pkRows = await querySchemaRows<{ COLUMN_NAME: string }>({
|
||||||
@@ -235,11 +403,36 @@ export default async function updateTable({
|
|||||||
console.log(
|
console.log(
|
||||||
`Dropping index ${indexName} because it contains a dropped column`,
|
`Dropping index ${indexName} because it contains a dropped column`,
|
||||||
);
|
);
|
||||||
|
try {
|
||||||
await runSchemaQuery({
|
await runSchemaQuery({
|
||||||
query: `DROP INDEX ${MariaDBQuoteGen(indexName)} ON ${MariaDBQuoteGen(table.tableName)}`,
|
query: `DROP INDEX ${MariaDBQuoteGen(indexName)} ON ${MariaDBQuoteGen(table.tableName)}`,
|
||||||
config,
|
config,
|
||||||
});
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
if (
|
||||||
|
String(err?.message || "").includes(
|
||||||
|
"needed in a foreign key constraint",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
console.warn(
|
||||||
|
`Skipping drop of index ${indexName}: required by a foreign key constraint`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop FKs on columns being modified (MODIFY can fail with FK present)
|
||||||
|
if (fieldsToModify.length > 0) {
|
||||||
|
await dropForeignKeysOnColumns({
|
||||||
|
tableName: table.tableName,
|
||||||
|
columns: fieldsToModify
|
||||||
|
.map((f) => f.fieldName)
|
||||||
|
.filter((n): n is string => Boolean(n)),
|
||||||
|
config,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const field of fieldsToAdd) {
|
for (const field of fieldsToAdd) {
|
||||||
@@ -247,18 +440,15 @@ export default async function updateTable({
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const field of fieldsToModify) {
|
for (const field of fieldsToModify) {
|
||||||
try {
|
|
||||||
await modifyColumn({ tableName: table.tableName, field, config });
|
await modifyColumn({ tableName: table.tableName, field, config });
|
||||||
} catch (err: any) {
|
|
||||||
if (isVectorField(field)) {
|
|
||||||
console.warn(
|
|
||||||
`[Vector Resize] Re-aligning dimension spaces natively via safe migration schema rebuild.`,
|
|
||||||
);
|
|
||||||
await recreateTable({ table, config });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const field of fieldsToRecreate) {
|
||||||
|
await recreateColumn({
|
||||||
|
tableName: table.tableName,
|
||||||
|
field,
|
||||||
|
config,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const fieldName of fieldsToDrop) {
|
for (const fieldName of fieldsToDrop) {
|
||||||
|
|||||||
+6
-6
@@ -96,10 +96,6 @@ export interface BUN_MARIADB_TableSchemaType {
|
|||||||
*/
|
*/
|
||||||
childTableDbId?: string | number;
|
childTableDbId?: string | number;
|
||||||
collation?: (typeof MariaDBCollations)[number];
|
collation?: (typeof MariaDBCollations)[number];
|
||||||
/**
|
|
||||||
* If this is a vector-oriented table (native MariaDB VECTOR columns/indexes)
|
|
||||||
*/
|
|
||||||
isVector?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -202,7 +198,10 @@ export type BUN_MARIADB_FieldSchemaType = {
|
|||||||
onDelete?: string;
|
onDelete?: string;
|
||||||
onDeleteLiteral?: string;
|
onDeleteLiteral?: string;
|
||||||
cssFiles?: string[];
|
cssFiles?: string[];
|
||||||
integerLength?: string | number;
|
/**
|
||||||
|
* Datatype length. Eg 255 for VARCHAR
|
||||||
|
*/
|
||||||
|
dataLength?: string | number;
|
||||||
decimals?: string | number;
|
decimals?: string | number;
|
||||||
code?: boolean;
|
code?: boolean;
|
||||||
options?: (string | number)[];
|
options?: (string | number)[];
|
||||||
@@ -916,7 +915,7 @@ export type ServerQueryParamsJoin<
|
|||||||
tableName: Table;
|
tableName: Table;
|
||||||
match?:
|
match?:
|
||||||
| ServerQueryParamsJoinMatchObject<Field>
|
| ServerQueryParamsJoinMatchObject<Field>
|
||||||
| ServerQueryParamsJoinMatchObject<Field>[];
|
| (ServerQueryParamsJoinMatchObject<Field> | undefined)[];
|
||||||
selectFields?: (keyof Field | SelectFieldObject<Field>)[];
|
selectFields?: (keyof Field | SelectFieldObject<Field>)[];
|
||||||
omitFields?: (
|
omitFields?: (
|
||||||
| keyof Field
|
| keyof Field
|
||||||
@@ -1619,6 +1618,7 @@ export type DBResponseObject<
|
|||||||
msg?: string;
|
msg?: string;
|
||||||
debug?: any;
|
debug?: any;
|
||||||
count?: number;
|
count?: number;
|
||||||
|
db_res?: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DBInsertReturn = {
|
export type DBInsertReturn = {
|
||||||
|
|||||||
+64
-28
@@ -2,6 +2,7 @@ import fs from "fs";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { AppData } from "../data/app-data";
|
import { AppData } from "../data/app-data";
|
||||||
import grabDirNames from "../data/grab-dir-names";
|
import grabDirNames from "../data/grab-dir-names";
|
||||||
|
import { isSchemaFileName } from "./resolve-and-load-data-file";
|
||||||
|
|
||||||
export const ExportArchiveMembers = {
|
export const ExportArchiveMembers = {
|
||||||
SqlFileName: "dump.sql",
|
SqlFileName: "dump.sql",
|
||||||
@@ -10,7 +11,9 @@ export const ExportArchiveMembers = {
|
|||||||
|
|
||||||
export type ExportArchiveContents = {
|
export type ExportArchiveContents = {
|
||||||
sql: string;
|
sql: string;
|
||||||
schemaTs: string;
|
schema: string;
|
||||||
|
/** Archive member basename, e.g. schema.ts / schema.json / schema.yaml */
|
||||||
|
schemaFileName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ARCHIVE_EXTENSIONS = [".tar.gz", ".tgz", ".tar", ".zip"] as const;
|
const ARCHIVE_EXTENSIONS = [".tar.gz", ".tgz", ".tar", ".zip"] as const;
|
||||||
@@ -25,7 +28,7 @@ export function isSqlPath(filePath: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a portable export archive (tar / tar.gz / zip) with dump.sql + schema.ts.
|
* Create a portable export archive (tar / tar.gz / zip) with dump.sql + schema file.
|
||||||
*/
|
*/
|
||||||
export async function writeExportArchive({
|
export async function writeExportArchive({
|
||||||
contents,
|
contents,
|
||||||
@@ -42,7 +45,7 @@ export async function writeExportArchive({
|
|||||||
|
|
||||||
const members = {
|
const members = {
|
||||||
[ExportArchiveMembers.SqlFileName]: contents.sql,
|
[ExportArchiveMembers.SqlFileName]: contents.sql,
|
||||||
[ExportArchiveMembers.SchemaFileName]: contents.schemaTs,
|
[contents.schemaFileName]: contents.schema,
|
||||||
};
|
};
|
||||||
|
|
||||||
const gzip = lower.endsWith(".gz") || lower.endsWith(".tgz");
|
const gzip = lower.endsWith(".gz") || lower.endsWith(".tgz");
|
||||||
@@ -73,12 +76,7 @@ export async function readExportArchive(
|
|||||||
(await readArchiveMember(files, ExportArchiveMembers.SqlFileName)) ??
|
(await readArchiveMember(files, ExportArchiveMembers.SqlFileName)) ??
|
||||||
(await readFirstMatching(files, (name) => name.endsWith(".sql")));
|
(await readFirstMatching(files, (name) => name.endsWith(".sql")));
|
||||||
|
|
||||||
const schemaTs =
|
const schemaEntry = await readFirstSchemaMember(files);
|
||||||
(await readArchiveMember(
|
|
||||||
files,
|
|
||||||
ExportArchiveMembers.SchemaFileName,
|
|
||||||
)) ??
|
|
||||||
(await readFirstMatching(files, (name) => name.endsWith("schema.ts")));
|
|
||||||
|
|
||||||
if (!sql) {
|
if (!sql) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -86,20 +84,23 @@ export async function readExportArchive(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!schemaTs) {
|
if (!schemaEntry) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Archive is missing schema TypeScript (expected \`${ExportArchiveMembers.SchemaFileName}\`)`,
|
`Archive is missing schema file (expected \`${AppData.DbSchemaFileName}.ts|json|yaml|yml\`)`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { sql, schemaTs };
|
return {
|
||||||
|
sql,
|
||||||
|
schema: schemaEntry.content,
|
||||||
|
schemaFileName: schemaEntry.fileName,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readArchiveMember(
|
async function readArchiveMember(
|
||||||
files: Map<string, File>,
|
files: Map<string, File>,
|
||||||
name: string,
|
name: string,
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
// Exact match, or basename match for nested paths
|
|
||||||
for (const [entry, file] of files) {
|
for (const [entry, file] of files) {
|
||||||
if (entry === name || path.basename(entry) === name) {
|
if (entry === name || path.basename(entry) === name) {
|
||||||
return await file.text();
|
return await file.text();
|
||||||
@@ -120,6 +121,21 @@ async function readFirstMatching(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function readFirstSchemaMember(
|
||||||
|
files: Map<string, File>,
|
||||||
|
): Promise<{ content: string; fileName: string } | null> {
|
||||||
|
for (const [entry, file] of files) {
|
||||||
|
const base = path.basename(entry);
|
||||||
|
if (isSchemaFileName(base)) {
|
||||||
|
return {
|
||||||
|
content: await file.text(),
|
||||||
|
fileName: base === AppData.DbSchemaFileName ? "schema.ts" : base,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
async function writeZipArchive({
|
async function writeZipArchive({
|
||||||
contents,
|
contents,
|
||||||
outPath,
|
outPath,
|
||||||
@@ -137,12 +153,9 @@ async function writeZipArchive({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const sqlPath = path.join(tempDir, ExportArchiveMembers.SqlFileName);
|
const sqlPath = path.join(tempDir, ExportArchiveMembers.SqlFileName);
|
||||||
const schemaPath = path.join(
|
const schemaPath = path.join(tempDir, contents.schemaFileName);
|
||||||
tempDir,
|
|
||||||
ExportArchiveMembers.SchemaFileName,
|
|
||||||
);
|
|
||||||
fs.writeFileSync(sqlPath, contents.sql, "utf-8");
|
fs.writeFileSync(sqlPath, contents.sql, "utf-8");
|
||||||
fs.writeFileSync(schemaPath, contents.schemaTs, "utf-8");
|
fs.writeFileSync(schemaPath, contents.schema, "utf-8");
|
||||||
|
|
||||||
const absOut = path.resolve(outPath);
|
const absOut = path.resolve(outPath);
|
||||||
const proc = Bun.spawn(
|
const proc = Bun.spawn(
|
||||||
@@ -152,7 +165,7 @@ async function writeZipArchive({
|
|||||||
"-j",
|
"-j",
|
||||||
absOut,
|
absOut,
|
||||||
ExportArchiveMembers.SqlFileName,
|
ExportArchiveMembers.SqlFileName,
|
||||||
ExportArchiveMembers.SchemaFileName,
|
contents.schemaFileName,
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
cwd: tempDir,
|
cwd: tempDir,
|
||||||
@@ -208,25 +221,24 @@ async function readZipArchive(
|
|||||||
const sql = findFileContents(tempDir, (name) =>
|
const sql = findFileContents(tempDir, (name) =>
|
||||||
name === ExportArchiveMembers.SqlFileName || name.endsWith(".sql"),
|
name === ExportArchiveMembers.SqlFileName || name.endsWith(".sql"),
|
||||||
);
|
);
|
||||||
const schemaTs = findFileContents(
|
const schemaHit = findSchemaFile(tempDir);
|
||||||
tempDir,
|
|
||||||
(name) =>
|
|
||||||
name === ExportArchiveMembers.SchemaFileName ||
|
|
||||||
name.endsWith("schema.ts"),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!sql) {
|
if (!sql) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Archive is missing SQL dump (expected \`${ExportArchiveMembers.SqlFileName}\`)`,
|
`Archive is missing SQL dump (expected \`${ExportArchiveMembers.SqlFileName}\`)`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!schemaTs) {
|
if (!schemaHit) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Archive is missing schema TypeScript (expected \`${ExportArchiveMembers.SchemaFileName}\`)`,
|
`Archive is missing schema file (expected \`${AppData.DbSchemaFileName}.ts|json|yaml|yml\`)`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { sql, schemaTs };
|
return {
|
||||||
|
sql,
|
||||||
|
schema: schemaHit.content,
|
||||||
|
schemaFileName: schemaHit.fileName,
|
||||||
|
};
|
||||||
} finally {
|
} finally {
|
||||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
@@ -250,3 +262,27 @@ function findFileContents(
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findSchemaFile(
|
||||||
|
dir: string,
|
||||||
|
): { content: string; fileName: string } | null {
|
||||||
|
const stack = [dir];
|
||||||
|
while (stack.length) {
|
||||||
|
const current = stack.pop()!;
|
||||||
|
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
||||||
|
const full = path.join(current, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
stack.push(full);
|
||||||
|
} else if (isSchemaFileName(entry.name)) {
|
||||||
|
return {
|
||||||
|
content: fs.readFileSync(full, "utf-8"),
|
||||||
|
fileName:
|
||||||
|
entry.name === AppData.DbSchemaFileName
|
||||||
|
? "schema.ts"
|
||||||
|
: entry.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { AppData } from "../data/app-data";
|
||||||
|
|
||||||
|
export type DataFileFormat = "ts" | "js" | "json" | "yaml";
|
||||||
|
|
||||||
|
export type ResolvedDataFile = {
|
||||||
|
path: string;
|
||||||
|
basename: string;
|
||||||
|
extension: (typeof AppData.SupportedDataFileExtensions)[number];
|
||||||
|
format: DataFileFormat;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LoadedDataFile<T> = ResolvedDataFile & {
|
||||||
|
data: T;
|
||||||
|
};
|
||||||
|
|
||||||
|
function extensionToFormat(
|
||||||
|
extension: (typeof AppData.SupportedDataFileExtensions)[number],
|
||||||
|
): DataFileFormat {
|
||||||
|
switch (extension) {
|
||||||
|
case ".ts":
|
||||||
|
return "ts";
|
||||||
|
case ".js":
|
||||||
|
return "js";
|
||||||
|
case ".json":
|
||||||
|
return "json";
|
||||||
|
case ".yaml":
|
||||||
|
case ".yml":
|
||||||
|
return "yaml";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a basename (no extension) to an existing file among supported formats.
|
||||||
|
* Priority: .ts > .js > .json > .yaml > .yml
|
||||||
|
* Errors if multiple matches exist.
|
||||||
|
*/
|
||||||
|
export function resolveDataFile(
|
||||||
|
dir: string,
|
||||||
|
baseName: string,
|
||||||
|
): ResolvedDataFile | null {
|
||||||
|
const matches: ResolvedDataFile[] = [];
|
||||||
|
|
||||||
|
for (const extension of AppData.SupportedDataFileExtensions) {
|
||||||
|
const filePath = path.join(dir, `${baseName}${extension}`);
|
||||||
|
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
||||||
|
matches.push({
|
||||||
|
path: filePath,
|
||||||
|
basename: `${baseName}${extension}`,
|
||||||
|
extension,
|
||||||
|
format: extensionToFormat(extension),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches.length > 1) {
|
||||||
|
const found = matches.map((m) => m.basename).join(", ");
|
||||||
|
throw new Error(
|
||||||
|
`Multiple \`${baseName}\` files found (${found}). Keep only one of: ${AppData.SupportedDataFileExtensions.join(", ")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches[0]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function supportedDataFileNames(baseName: string): string {
|
||||||
|
return AppData.SupportedDataFileExtensions.map(
|
||||||
|
(ext) => `\`${baseName}${ext}\``,
|
||||||
|
).join(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSchemaFileName(name: string): boolean {
|
||||||
|
const base = path.basename(name);
|
||||||
|
if (base === AppData.DbSchemaFileName) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return AppData.SupportedDataFileExtensions.some(
|
||||||
|
(ext) => base === `${AppData.DbSchemaFileName}${ext}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a data file as a plain object.
|
||||||
|
* - .ts / .js: require() and use default export (or module itself)
|
||||||
|
* - .json: JSON.parse
|
||||||
|
* - .yaml / .yml: Bun.YAML.parse
|
||||||
|
*/
|
||||||
|
export function loadDataFile<T>(resolved: ResolvedDataFile): T {
|
||||||
|
if (resolved.format === "ts" || resolved.format === "js") {
|
||||||
|
const imported = require(resolved.path);
|
||||||
|
const data =
|
||||||
|
imported && typeof imported === "object" && "default" in imported
|
||||||
|
? imported.default
|
||||||
|
: imported;
|
||||||
|
|
||||||
|
if (data == null) {
|
||||||
|
throw new Error(
|
||||||
|
`No default export from \`${resolved.path}\`. Please export a default module.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = fs.readFileSync(resolved.path, "utf-8");
|
||||||
|
|
||||||
|
if (resolved.format === "json") {
|
||||||
|
return JSON.parse(text) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Bun.YAML.parse(text) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveAndLoadDataFile<T>(
|
||||||
|
dir: string,
|
||||||
|
baseName: string,
|
||||||
|
): LoadedDataFile<T> | null {
|
||||||
|
const resolved = resolveDataFile(dir, baseName);
|
||||||
|
if (!resolved) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...resolved,
|
||||||
|
data: loadDataFile<T>(resolved),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isUndefined } from "lodash";
|
import _, { isUndefined } from "lodash";
|
||||||
import type { ServerQueryParam, TableSelectFieldsObject } from "../types";
|
import type { ServerQueryParam, TableSelectFieldsObject } from "../types";
|
||||||
import sqlGenGrabConcatStr from "./sql-generator-grab-concat-str";
|
import sqlGenGrabConcatStr from "./sql-generator-grab-concat-str";
|
||||||
import sqlGenGenJoinStr from "./sql-generator-gen-join-str";
|
import sqlGenGenJoinStr from "./sql-generator-gen-join-str";
|
||||||
@@ -195,6 +195,7 @@ export default function sqlGenGenQueryStr<
|
|||||||
return (
|
return (
|
||||||
"(" +
|
"(" +
|
||||||
join.match
|
join.match
|
||||||
|
.filter((mtch) => !_.isUndefined(mtch))
|
||||||
.map((mtch) => {
|
.map((mtch) => {
|
||||||
const { str, values } =
|
const { str, values } =
|
||||||
sqlGenGenJoinStr({
|
sqlGenGenJoinStr({
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export default function sqlInsertGenerator({
|
|||||||
? value
|
? value
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (!finalValue) {
|
if (!finalValue && typeof value !== "number") {
|
||||||
queryValues.push(null);
|
queryValues.push(null);
|
||||||
return "?";
|
return "?";
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user