Skip to content

Commit b191c90

Browse files
Merge pull request #251 from Sebastian-Webster/249-add-initsqlfilepath-option
Add initSQLFilePath option
2 parents 57d529d + 260ebe3 commit b191c90

File tree

5 files changed

+74
-9
lines changed

5 files changed

+74
-9
lines changed

README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,9 +180,13 @@ Description: The number of times to try to download a MySQL binary before giving
180180

181181
Default: ""
182182

183-
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)
183+
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.
184184

185-
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.
185+
- `initSQLFilePath: string`
186+
187+
Default: ""
188+
189+
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.
186190

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

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

197201
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.
202+
203+
### Init SQL file order of operations:
204+
205+
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:
206+
207+
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.
208+
2. The SQL queries provided to the ```initSQLString``` option
209+
3. The SQL queries provided to the ```initSQLFilePath``` option

src/constants.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { InternalServerOptions, OptionTypeChecks } from "../types";
22
import { valid as validSemver, coerce as coerceSemver } from "semver";
3+
import { existsSync } from "fs";
34

45
export const DEFAULT_OPTIONS: InternalServerOptions = {
56
version: undefined,
@@ -16,7 +17,8 @@ export const DEFAULT_OPTIONS: InternalServerOptions = {
1617
downloadRetries: 10,
1718
initSQLString: '',
1819
arch: process.arch,
19-
xEnabled: 'FORCE'
20+
xEnabled: 'FORCE',
21+
initSQLFilePath: ''
2022
} as const;
2123

2224
export const DEFAULT_OPTIONS_KEYS = Object.freeze(Object.keys(DEFAULT_OPTIONS))
@@ -113,7 +115,12 @@ export const OPTION_TYPE_CHECKS: OptionTypeChecks = {
113115
check: (opt: any) => opt === undefined || pluginActivationStates.includes(opt),
114116
errorMessage: `xEnabled must be either undefined or one of the following: ${pluginActivationStates.join(', ')}`,
115117
definedType: 'boolean'
116-
}
118+
},
119+
initSQLFilePath: {
120+
check: (opt: any) => opt === undefined || (typeof opt === 'string' && existsSync(opt)),
121+
errorMessage: 'Option initSQLFilePath must be either undefined or a filepath string that points to a file that exists.',
122+
definedType: 'string'
123+
},
117124
} as const;
118125

119126
export const MIN_SUPPORTED_MYSQL = '5.7.19';

src/libraries/Executor.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,33 @@ class Executor {
358358
})
359359
}
360360

361+
#streamAppendToFile(readPath: string, writePath: string): Promise<void> {
362+
return new Promise((resolve, reject) => {
363+
const rs = fs.createReadStream(readPath, {encoding: 'utf-8'})
364+
const ws = fs.createWriteStream(writePath, {flags: 'a', encoding: 'utf-8'})
365+
366+
rs.on('error', (e) => {
367+
ws.end();
368+
this.logger.error('Received error from streamAppendToFile read stream:', e)
369+
reject(e)
370+
})
371+
372+
ws.on('error', (e) => {
373+
rs.close();
374+
this.logger.error('Received error from streamAppendToFile write stream:', e)
375+
reject(e)
376+
})
377+
378+
rs.on('end', () => {
379+
rs.close();
380+
ws.close();
381+
resolve()
382+
})
383+
384+
rs.pipe(ws)
385+
})
386+
}
387+
361388
async #setupDataDirectories(options: InternalServerOptions, binary: DownloadedMySQLVersion, datadir: string, retry: boolean): Promise<void> {
362389
const binaryFilepath = binary.path
363390
this.logger.log('Created data directory for database at:', datadir)
@@ -494,14 +521,23 @@ class Executor {
494521
}
495522

496523
if (options.initSQLString.length > 0) {
497-
initText += `\n${options.initSQLString}`
524+
initText += `\n${options.initSQLString}\n`
498525
}
499526

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

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

504532
this.logger.log('Finished writing init file')
533+
534+
if (options.initSQLFilePath) {
535+
this.logger.log('Appending init.sql file with the contents of the file at path provided by options.initSQLFilePath.')
536+
537+
await this.#streamAppendToFile(options.initSQLFilePath, initFilePath)
538+
539+
this.logger.log('Successfully appended init.sql file with the contents of the file at path provided by options.initSQLFilePath.')
540+
}
505541
}
506542

507543
async startMySQL(options: InternalServerOptions, installedMySQLBinary: DownloadedMySQLVersion): Promise<MySQLDB> {

tests/versions.test.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { DOWNLOADABLE_MYSQL_VERSIONS } from '../src/constants';
88
import fs from 'fs'
99
import fsPromises from 'fs/promises'
1010
import os from 'os'
11+
import { randomUUID } from 'crypto';
1112

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

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

22+
const initSQLFilePath = `${os.tmpdir()}/mysqlmsn-init-file-${randomUUID()}`
23+
fs.writeFileSync(initSQLFilePath, 'CREATE DATABASE initfromsqlfilepath;', 'utf-8')
24+
2125
for (const version of DOWNLOADABLE_MYSQL_VERSIONS.filter(v => satisfies(v, versionRequirement))) {
2226
try {
2327
getBinaryURL(version, arch)
@@ -35,7 +39,8 @@ for (const version of DOWNLOADABLE_MYSQL_VERSIONS.filter(v => satisfies(v, versi
3539
logLevel: 'LOG',
3640
initSQLString: 'CREATE DATABASE mytestdb;',
3741
arch,
38-
xEnabled: process.env.X_OFF === 'true' ? 'OFF' : 'FORCE'
42+
xEnabled: process.env.X_OFF === 'true' ? 'OFF' : 'FORCE',
43+
initSQLFilePath
3944
}
4045

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

5055
//If this does not fail, it means initSQLString works as expected and the database was successfully created.
5156
await connection.query('USE mytestdb;')
57+
58+
//If this does not fail, it means initSQLFilePath works as expected and the database was successfully created.
59+
await connection.query('USE initfromsqlfilepath;')
5260

5361
await connection.end();
5462
await db.stop();

types/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ export type ServerOptions = {
1919
downloadRetries?: number | undefined,
2020
initSQLString?: string | undefined,
2121
arch?: "arm64" | "x64" | undefined,
22-
xEnabled?: PluginActivationState | undefined
22+
xEnabled?: PluginActivationState | undefined,
23+
initSQLFilePath?: string | undefined
2324
}
2425

2526
export type InternalServerOptions = {
@@ -37,7 +38,8 @@ export type InternalServerOptions = {
3738
downloadRetries: number,
3839
initSQLString: string,
3940
arch: string,
40-
xEnabled: PluginActivationState
41+
xEnabled: PluginActivationState,
42+
initSQLFilePath: string
4143
}
4244

4345
export type ExecuteFileReturn = {

0 commit comments

Comments
 (0)