Skip to content
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,13 @@ Description: The number of times to try to download a MySQL binary before giving

Default: ""

Description: A string with MySQL queries to run before the database starts to accept connections. This option can be used for things like initialising tables without having to first connect to the database to do that. The queries in the string get executed after ```mysql-memory-server```'s queries run. Uses the ```--init-file``` MySQL server option under the hood. Learn more at the [--init-file MySQL Documentation](https://dev.mysql.com/doc/refman/8.4/en/server-system-variables.html#sysvar_init_file)
Description: A string with SQL queries to run before the database starts to accept connections. This option can be used for things like initialising tables without having to first connect to the database to do that. Check the [Init SQL file order of operations](#init-sql-file-order-of-operations) to learn more about what SQL queries are ran and in what order before the database starts accepting connections.

The internal queries that are ran before the queries in ```initSQLString``` are creating the MySQL user with ```options.username``` username if the option's value is not ```root```, and creating a database with the ```options.dbName``` name.
- `initSQLFilePath: string`

Default: ""

Description: A path to a UTF-8 .sql file with SQL queries to run before the database starts to accept connections. This option is like the ```initSQLString``` option, instead taking a filepath for SQL statements to execute rather than a string of SQL statements - great for when you need to execute more than just a few SQL statements. Check the [Init SQL file order of operations](#init-sql-file-order-of-operations) to learn more about what SQL queries are ran and in what order before the database starts accepting connections. If a filepath is defined and reading the file fails, then the database creation will fail. The database creation process will not begin if ```initSQLFilePath``` is defined but the path specified does not exist.

- `arch: "arm64" | "x64"`

Expand All @@ -195,3 +199,11 @@ Description: The MySQL binary architecture to execute. MySQL does not offer serv
Default: "FORCE"

Description: This option follows the convention set out by the [MySQL Documentation](https://dev.mysql.com/doc/refman/en/plugin-loading.html). If set to "OFF", the MySQL X Plugin will not initialise. If set to "FORCE", the MySQL Server will either start up with the MySQL X Plugin guaranteed to have successfully initialised, or if initialisation fails, the server will fail to start up.

### Init SQL file order of operations:

There are some SQL queries executed on the database before the database is ready to be used. This is handled under the hood using the ```--init-file``` MySQL server option. Learn more at the [--init-file MySQL Documentation](https://dev.mysql.com/doc/refman/8.4/en/server-system-variables.html#sysvar_init_file). The following is the order in which the SQL queries are executed in, ordered from first executed to last executed:

1. ```mysql-memory-server``` internal SQL queries - The internal queries that are executed are creating the MySQL user with ```options.username``` username if the option's value is not ```root```, and creating a database with the ```options.dbName``` name.
2. The SQL queries provided to the ```initSQLString``` option
3. The SQL queries provided to the ```initSQLFilePath``` option
11 changes: 9 additions & 2 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { InternalServerOptions, OptionTypeChecks } from "../types";
import { valid as validSemver, coerce as coerceSemver } from "semver";
import { existsSync } from "fs";

export const DEFAULT_OPTIONS: InternalServerOptions = {
version: undefined,
Expand All @@ -16,7 +17,8 @@ export const DEFAULT_OPTIONS: InternalServerOptions = {
downloadRetries: 10,
initSQLString: '',
arch: process.arch,
xEnabled: 'FORCE'
xEnabled: 'FORCE',
initSQLFilePath: ''
} as const;

export const DEFAULT_OPTIONS_KEYS = Object.freeze(Object.keys(DEFAULT_OPTIONS))
Expand Down Expand Up @@ -113,7 +115,12 @@ export const OPTION_TYPE_CHECKS: OptionTypeChecks = {
check: (opt: any) => opt === undefined || pluginActivationStates.includes(opt),
errorMessage: `xEnabled must be either undefined or one of the following: ${pluginActivationStates.join(', ')}`,
definedType: 'boolean'
}
},
initSQLFilePath: {
check: (opt: any) => opt === undefined || (typeof opt === 'string' && existsSync(opt)),
errorMessage: 'Option initSQLFilePath must be either undefined or a filepath string that points to a file that exists.',
definedType: 'string'
},
} as const;

export const MIN_SUPPORTED_MYSQL = '5.7.19';
Expand Down
40 changes: 38 additions & 2 deletions src/libraries/Executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,33 @@ class Executor {
})
}

#streamAppendToFile(readPath: string, writePath: string): Promise<void> {
return new Promise((resolve, reject) => {
const rs = fs.createReadStream(readPath, {encoding: 'utf-8'})
const ws = fs.createWriteStream(writePath, {flags: 'a', encoding: 'utf-8'})

rs.on('error', (e) => {
ws.end();
this.logger.error('Received error from streamAppendToFile read stream:', e)
reject(e)
})

ws.on('error', (e) => {
rs.close();
this.logger.error('Received error from streamAppendToFile write stream:', e)
reject(e)
})

rs.on('end', () => {
rs.close();
ws.close();
resolve()
})

rs.pipe(ws)
})
}

async #setupDataDirectories(options: InternalServerOptions, binary: DownloadedMySQLVersion, datadir: string, retry: boolean): Promise<void> {
const binaryFilepath = binary.path
this.logger.log('Created data directory for database at:', datadir)
Expand Down Expand Up @@ -494,14 +521,23 @@ class Executor {
}

if (options.initSQLString.length > 0) {
initText += `\n${options.initSQLString}`
initText += `\n${options.initSQLString}\n`
}

this.logger.log('Writing init file')

await fsPromises.writeFile(`${this.databasePath}/init.sql`, initText, {encoding: 'utf8'})
const initFilePath = `${this.databasePath}/init.sql`
await fsPromises.writeFile(initFilePath, initText, {encoding: 'utf8'})

this.logger.log('Finished writing init file')

if (options.initSQLFilePath) {
this.logger.log('Appending init.sql file with the contents of the file at path provided by options.initSQLFilePath.')

await this.#streamAppendToFile(options.initSQLFilePath, initFilePath)

this.logger.log('Successfully appended init.sql file with the contents of the file at path provided by options.initSQLFilePath.')
}
}

async startMySQL(options: InternalServerOptions, installedMySQLBinary: DownloadedMySQLVersion): Promise<MySQLDB> {
Expand Down
10 changes: 9 additions & 1 deletion tests/versions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { DOWNLOADABLE_MYSQL_VERSIONS } from '../src/constants';
import fs from 'fs'
import fsPromises from 'fs/promises'
import os from 'os'
import { randomUUID } from 'crypto';

const usernames = ['root', 'dbuser']

Expand All @@ -18,6 +19,9 @@ const arch = process.arch === 'x64' || (process.platform === 'win32' && process.
const versionRequirement = process.env.VERSION_REQUIREMENT || '>0.0.0'
console.log('Running versions test with versionRequirement:', versionRequirement)

const initSQLFilePath = `${os.tmpdir()}/mysqlmsn-init-file-${randomUUID()}`
fs.writeFileSync(initSQLFilePath, 'CREATE DATABASE initfromsqlfilepath;', 'utf-8')

for (const version of DOWNLOADABLE_MYSQL_VERSIONS.filter(v => satisfies(v, versionRequirement))) {
try {
getBinaryURL(version, arch)
Expand All @@ -35,7 +39,8 @@ for (const version of DOWNLOADABLE_MYSQL_VERSIONS.filter(v => satisfies(v, versi
logLevel: 'LOG',
initSQLString: 'CREATE DATABASE mytestdb;',
arch,
xEnabled: process.env.X_OFF === 'true' ? 'OFF' : 'FORCE'
xEnabled: process.env.X_OFF === 'true' ? 'OFF' : 'FORCE',
initSQLFilePath
}

const db = await createDB(options)
Expand All @@ -49,6 +54,9 @@ for (const version of DOWNLOADABLE_MYSQL_VERSIONS.filter(v => satisfies(v, versi

//If this does not fail, it means initSQLString works as expected and the database was successfully created.
await connection.query('USE mytestdb;')

//If this does not fail, it means initSQLFilePath works as expected and the database was successfully created.
await connection.query('USE initfromsqlfilepath;')

await connection.end();
await db.stop();
Expand Down
6 changes: 4 additions & 2 deletions types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ export type ServerOptions = {
downloadRetries?: number | undefined,
initSQLString?: string | undefined,
arch?: "arm64" | "x64" | undefined,
xEnabled?: PluginActivationState | undefined
xEnabled?: PluginActivationState | undefined,
initSQLFilePath?: string | undefined
}

export type InternalServerOptions = {
Expand All @@ -37,7 +38,8 @@ export type InternalServerOptions = {
downloadRetries: number,
initSQLString: string,
arch: string,
xEnabled: PluginActivationState
xEnabled: PluginActivationState,
initSQLFilePath: string
}

export type ExecuteFileReturn = {
Expand Down