@noego/proper 0.0.9 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/runner.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../framework/lib/runner.ts","../framework/SQLRunner.ts"],"sourcesContent":["export { ISQLRunner, BaseSQLRunner, SQLRunner, SQLiteRunner } from '../SQLRunner';\n","\nimport * as sqlite from 'sqlite';\nimport { Statement } from 'sqlite3';\nimport mysql from 'mysql2/promise';\n\n\n\ntype QueryResult = {\n stmt: Statement;\n lastID: number;\n changes: number;\n}\n\nexport interface ISQLRunner {\n query(sql: string, params?: any[]): Promise<any>;\n execute(sql: string, params?: any[]): Promise<any>;\n end(): Promise<void>;\n}\n\n/**\n * Base class that implements the common contract and helper utilities that are\n * shared between the different dialect runners. The concrete subclasses only\n * need to implement the three primitive methods `_query`, `_execute` and\n * `_end` that perform the actual driver-specific interaction. Everything else\n * – such as ensuring a uniform return shape – is handled here once.\n */\nexport abstract class BaseSQLRunner implements ISQLRunner {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n abstract _query(sql: string, params?: any[]): Promise<any>;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n abstract _execute(sql: string, params?: any[]): Promise<any>;\n abstract _end(): Promise<void>;\n\n /**\n * Ensures that both MySQL and SQLite return the same tuple shape that callers\n * expect: `[rowsOrResult, extra]`. For SQLite there is no `extra` metadata\n * comparable to MySQL's `FieldPacket[]`, so we just use `undefined`.\n */\n async query(sql: string, params: any[] = []): Promise<any> {\n const result = await this._query(sql, params);\n\n // MySQL already returns the desired tuple. For SQLite we need to wrap.\n return Array.isArray(result) && result.length === 2 ? result : [result, undefined];\n }\n\n async execute(sql: string, params: any[] = []): Promise<any> {\n const result = await this._execute(sql, params);\n return Array.isArray(result) && result.length === 2 ? result : [result, undefined];\n }\n\n async end(): Promise<void> {\n await this._end();\n }\n}\n\nfunction isPromiseLike<T>(value: unknown): value is Promise<T> {\n return !!value && typeof (value as Promise<T>).then === \"function\";\n}\n\nexport class SQLRunner extends BaseSQLRunner {\n constructor(private connection: mysql.Connection) {\n super();\n }\n\n // The MySQL driver already returns the correct tuple shapes.\n async _query(sql: string, params: any[] = []) {\n return await this.connection.query(sql, params);\n }\n\n async _execute(sql: string, params: any[] = []) {\n return await this.connection.execute(sql, params);\n }\n\n async _end() {\n if (this.connection) {\n await this.connection.end();\n }\n }\n}\n\nexport class SQLiteRunner extends BaseSQLRunner {\n constructor(private connection: sqlite.Database | any) {\n super();\n }\n\n private async prepareStatement(sql: string) {\n if (typeof this.connection.prepare !== \"function\") {\n throw new Error(\"SQLite connection does not support prepare()\");\n }\n const stmt = this.connection.prepare(sql);\n return isPromiseLike(stmt) ? await stmt : stmt;\n }\n\n private async finalizeStatement(stmt: any) {\n if (!stmt || typeof stmt.finalize !== \"function\") return;\n const result = stmt.finalize();\n if (isPromiseLike(result)) {\n await result;\n }\n }\n\n private async statementAll(stmt: any, params: any[]) {\n if (typeof stmt.all !== \"function\") {\n throw new Error(\"SQLite statement does not support all()\");\n }\n // sqlite3 callback-style API\n if (stmt.all.length >= 2) {\n return await new Promise((resolve, reject) => {\n const callback = (err: Error | null, rows: any[]) => {\n if (err) return reject(err);\n resolve(rows || []);\n };\n try {\n if (params.length > 0) {\n stmt.all(params, callback);\n } else {\n stmt.all(callback);\n }\n } catch (error) {\n reject(error);\n }\n });\n }\n const result = stmt.all(...params);\n return isPromiseLike(result) ? await result : result;\n }\n\n private async statementRun(stmt: any, params: any[]) {\n if (typeof stmt.run !== \"function\") {\n throw new Error(\"SQLite statement does not support run()\");\n }\n if (stmt.run.length >= 2) {\n return await new Promise((resolve, reject) => {\n const callback = function (this: any, err: Error | null) {\n if (err) return reject(err);\n resolve({ changes: this?.changes ?? 0, lastID: this?.lastID });\n };\n try {\n if (params.length > 0) {\n stmt.run(params, callback);\n } else {\n stmt.run(callback);\n }\n } catch (error) {\n reject(error);\n }\n });\n }\n const result = stmt.run(...params);\n return isPromiseLike(result) ? await result : result;\n }\n\n async _query(sql: string, params: any[] = []) {\n const stmt = await this.prepareStatement(sql);\n try {\n const rows = await this.statementAll(stmt, params);\n return rows;\n } finally {\n await this.finalizeStatement(stmt);\n }\n }\n\n async _execute(sql: string, params: any[] = []) {\n // Handle empty or comment-only SQL gracefully\n if (this.isEmptySQL(sql)) {\n return { changes: 0, lastID: 0 }; // No-op result\n }\n\n if (this.isMultiStatement(sql)) {\n return await this.executeMultiStatement(sql, params)\n .catch((err) => {\n console.error((\n \"Error executing multi-statement SQL:\\n\"+\n `${sql}\\n`\n ), err);\n throw err;\n });\n }\n\n const stmt = await this.prepareStatement(sql);\n try {\n const info = await this.statementRun(stmt, params);\n return info;\n } finally {\n await this.finalizeStatement(stmt);\n }\n }\n\n /**\n * Checks if SQL is empty or contains only comments/whitespace.\n * Returns true if there is no actual SQL to execute.\n */\n private isEmptySQL(sql: string): boolean {\n // Remove all comments and whitespace\n const withoutComments = sql\n .replace(/--.*$/gm, '') // Remove single-line comments\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, '') // Remove multi-line comments\n .trim();\n return withoutComments.length === 0;\n }\n\n private isMultiStatement(sql: string): boolean {\n // SQLite does not support multiple statements in a single query.\n return sql.split(';').length > 1;\n }\n\n private async executeMultiStatement(sql: string, params: any[] = []) {\n const statements = sql.split(';')\n .map((s) => s.split('\\n')\n .map(\n s=>this.removeComments(s))\n .filter(s=>s.trim()!='')\n .join('\\n')\n )\n .filter((s) => s.trim() !== '');\n \n const infos = await statements.reduce(async (prev,statement) => {\n const infos = await prev;\n const stmt = await this.prepareStatement(`${statement};`);\n try {\n const info = await this.statementRun(stmt, params);\n infos.push(info);\n return infos;\n } finally {\n await this.finalizeStatement(stmt);\n }\n },Promise.resolve([null] as any[]))\n .then((infos:QueryResult[]) => {\n return infos.filter((info) => info !== null);\n })\n return infos.reduce((acc,latest)=>{\n if(latest){\n acc.stmt = latest.stmt;\n acc.lastID = latest.lastID;\n acc.changes += latest.changes;\n }\n return acc\n })\n }\n\n private removeComments(sql: string): string {\n // Remove single-line comments\n sql = sql.replace(/--.*$/gm, '');\n return sql;\n }\n\n\n async _end() {\n if (this.connection) {\n await this.connection.close();\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC0BO,IAAe,gBAAf,MAAmD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYlD,MAAM,IAA+C;AAAA,+CAA/C,KAAa,SAAgB,CAAC,GAAiB;AACzD,YAAM,SAAS,MAAM,KAAK,OAAO,KAAK,MAAM;AAG5C,aAAO,MAAM,QAAQ,MAAM,KAAK,OAAO,WAAW,IAAI,SAAS,CAAC,QAAQ,MAAS;AAAA,IACnF;AAAA;AAAA,EAEM,QAAQ,IAA+C;AAAA,+CAA/C,KAAa,SAAgB,CAAC,GAAiB;AAC3D,YAAM,SAAS,MAAM,KAAK,SAAS,KAAK,MAAM;AAC9C,aAAO,MAAM,QAAQ,MAAM,KAAK,OAAO,WAAW,IAAI,SAAS,CAAC,QAAQ,MAAS;AAAA,IACnF;AAAA;AAAA,EAEM,MAAqB;AAAA;AACzB,YAAM,KAAK,KAAK;AAAA,IAClB;AAAA;AACF;AAEA,SAAS,cAAiB,OAAqC;AAC7D,SAAO,CAAC,CAAC,SAAS,OAAQ,MAAqB,SAAS;AAC1D;AAEO,IAAM,YAAN,cAAwB,cAAc;AAAA,EAC3C,YAAoB,YAA8B;AAChD,UAAM;AADY;AAAA,EAEpB;AAAA;AAAA,EAGM,OAAO,IAAiC;AAAA,+CAAjC,KAAa,SAAgB,CAAC,GAAG;AAC5C,aAAO,MAAM,KAAK,WAAW,MAAM,KAAK,MAAM;AAAA,IAChD;AAAA;AAAA,EAEM,SAAS,IAAiC;AAAA,+CAAjC,KAAa,SAAgB,CAAC,GAAG;AAC9C,aAAO,MAAM,KAAK,WAAW,QAAQ,KAAK,MAAM;AAAA,IAClD;AAAA;AAAA,EAEM,OAAO;AAAA;AACX,UAAI,KAAK,YAAY;AACnB,cAAM,KAAK,WAAW,IAAI;AAAA,MAC5B;AAAA,IACF;AAAA;AACF;AAEO,IAAM,eAAN,cAA2B,cAAc;AAAA,EAC9C,YAAoB,YAAmC;AACrD,UAAM;AADY;AAAA,EAEpB;AAAA,EAEc,iBAAiB,KAAa;AAAA;AAC1C,UAAI,OAAO,KAAK,WAAW,YAAY,YAAY;AACjD,cAAM,IAAI,MAAM,8CAA8C;AAAA,MAChE;AACA,YAAM,OAAO,KAAK,WAAW,QAAQ,GAAG;AACxC,aAAO,cAAc,IAAI,IAAI,MAAM,OAAO;AAAA,IAC5C;AAAA;AAAA,EAEc,kBAAkB,MAAW;AAAA;AACzC,UAAI,CAAC,QAAQ,OAAO,KAAK,aAAa,WAAY;AAClD,YAAM,SAAS,KAAK,SAAS;AAC7B,UAAI,cAAc,MAAM,GAAG;AACzB,cAAM;AAAA,MACR;AAAA,IACF;AAAA;AAAA,EAEc,aAAa,MAAW,QAAe;AAAA;AACnD,UAAI,OAAO,KAAK,QAAQ,YAAY;AAClC,cAAM,IAAI,MAAM,yCAAyC;AAAA,MAC3D;AAEA,UAAI,KAAK,IAAI,UAAU,GAAG;AACxB,eAAO,MAAM,IAAI,QAAQ,CAAC,SAAS,WAAW;AAC5C,gBAAM,WAAW,CAAC,KAAmB,SAAgB;AACnD,gBAAI,IAAK,QAAO,OAAO,GAAG;AAC1B,oBAAQ,QAAQ,CAAC,CAAC;AAAA,UACpB;AACA,cAAI;AACF,gBAAI,OAAO,SAAS,GAAG;AACrB,mBAAK,IAAI,QAAQ,QAAQ;AAAA,YAC3B,OAAO;AACL,mBAAK,IAAI,QAAQ;AAAA,YACnB;AAAA,UACF,SAAS,OAAO;AACd,mBAAO,KAAK;AAAA,UACd;AAAA,QACF,CAAC;AAAA,MACH;AACA,YAAM,SAAS,KAAK,IAAI,GAAG,MAAM;AACjC,aAAO,cAAc,MAAM,IAAI,MAAM,SAAS;AAAA,IAChD;AAAA;AAAA,EAEc,aAAa,MAAW,QAAe;AAAA;AACnD,UAAI,OAAO,KAAK,QAAQ,YAAY;AAClC,cAAM,IAAI,MAAM,yCAAyC;AAAA,MAC3D;AACA,UAAI,KAAK,IAAI,UAAU,GAAG;AACxB,eAAO,MAAM,IAAI,QAAQ,CAAC,SAAS,WAAW;AAC5C,gBAAM,WAAW,SAAqB,KAAmB;AArIjE;AAsIU,gBAAI,IAAK,QAAO,OAAO,GAAG;AAC1B,oBAAQ,EAAE,UAAS,kCAAM,YAAN,YAAiB,GAAG,QAAQ,6BAAM,OAAO,CAAC;AAAA,UAC/D;AACA,cAAI;AACF,gBAAI,OAAO,SAAS,GAAG;AACrB,mBAAK,IAAI,QAAQ,QAAQ;AAAA,YAC3B,OAAO;AACL,mBAAK,IAAI,QAAQ;AAAA,YACnB;AAAA,UACF,SAAS,OAAO;AACd,mBAAO,KAAK;AAAA,UACd;AAAA,QACF,CAAC;AAAA,MACH;AACA,YAAM,SAAS,KAAK,IAAI,GAAG,MAAM;AACjC,aAAO,cAAc,MAAM,IAAI,MAAM,SAAS;AAAA,IAChD;AAAA;AAAA,EAEM,OAAO,IAAiC;AAAA,+CAAjC,KAAa,SAAgB,CAAC,GAAG;AAC5C,YAAM,OAAO,MAAM,KAAK,iBAAiB,GAAG;AAC5C,UAAI;AACF,cAAM,OAAO,MAAM,KAAK,aAAa,MAAM,MAAM;AACjD,eAAO;AAAA,MACT,UAAE;AACA,cAAM,KAAK,kBAAkB,IAAI;AAAA,MACnC;AAAA,IACF;AAAA;AAAA,EAEM,SAAS,IAAiC;AAAA,+CAAjC,KAAa,SAAgB,CAAC,GAAG;AAE9C,UAAI,KAAK,WAAW,GAAG,GAAG;AACxB,eAAO,EAAE,SAAS,GAAG,QAAQ,EAAE;AAAA,MACjC;AAEA,UAAI,KAAK,iBAAiB,GAAG,GAAG;AAC9B,eAAO,MAAM,KAAK,sBAAsB,KAAK,MAAM,EAClD,MAAM,CAAC,QAAQ;AACd,kBAAQ,MACN;AAAA,EACG,GAAG;AAAA,GACL,GAAG;AACN,gBAAM;AAAA,QACR,CAAC;AAAA,MACH;AAEA,YAAM,OAAO,MAAM,KAAK,iBAAiB,GAAG;AAC5C,UAAI;AACF,cAAM,OAAO,MAAM,KAAK,aAAa,MAAM,MAAM;AACjD,eAAO;AAAA,MACT,UAAE;AACA,cAAM,KAAK,kBAAkB,IAAI;AAAA,MACnC;AAAA,IACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,WAAW,KAAsB;AAEvC,UAAM,kBAAkB,IACrB,QAAQ,WAAW,EAAE,EACrB,QAAQ,qBAAqB,EAAE,EAC/B,KAAK;AACR,WAAO,gBAAgB,WAAW;AAAA,EACpC;AAAA,EAEQ,iBAAiB,KAAsB;AAE7C,WAAO,IAAI,MAAM,GAAG,EAAE,SAAS;AAAA,EACjC;AAAA,EAEc,sBAAsB,IAAiC;AAAA,+CAAjC,KAAa,SAAgB,CAAC,GAAG;AACnE,YAAM,aAAa,IAAI,MAAM,GAAG,EAC/B;AAAA,QAAI,CAAC,MAAM,EAAE,MAAM,IAAI,EACvB;AAAA,UACC,CAAAA,OAAG,KAAK,eAAeA,EAAC;AAAA,QAAC,EACxB,OAAO,CAAAA,OAAGA,GAAE,KAAK,KAAG,EAAE,EACtB,KAAK,IAAI;AAAA,MACZ,EACC,OAAO,CAAC,MAAM,EAAE,KAAK,MAAM,EAAE;AAE9B,YAAM,QAAQ,MAAM,WAAW,OAAO,CAAO,MAAK,cAAc;AAC9D,cAAMC,SAAQ,MAAM;AACpB,cAAM,OAAO,MAAM,KAAK,iBAAiB,GAAG,SAAS,GAAG;AACxD,YAAI;AACF,gBAAM,OAAO,MAAM,KAAK,aAAa,MAAM,MAAM;AACjD,UAAAA,OAAM,KAAK,IAAI;AACf,iBAAOA;AAAA,QACT,UAAE;AACA,gBAAM,KAAK,kBAAkB,IAAI;AAAA,QACnC;AAAA,MACF,IAAE,QAAQ,QAAQ,CAAC,IAAI,CAAU,CAAC,EACjC,KAAK,CAACA,WAAwB;AAC7B,eAAOA,OAAM,OAAO,CAAC,SAAS,SAAS,IAAI;AAAA,MAC7C,CAAC;AACD,aAAO,MAAM,OAAO,CAAC,KAAI,WAAS;AAChC,YAAG,QAAO;AACR,cAAI,OAAO,OAAO;AAClB,cAAI,SAAS,OAAO;AACpB,cAAI,WAAW,OAAO;AAAA,QACxB;AACA,eAAO;AAAA,MACT,CAAC;AAAA,IACH;AAAA;AAAA,EAEQ,eAAe,KAAqB;AAE1C,UAAM,IAAI,QAAQ,WAAW,EAAE;AAC/B,WAAO;AAAA,EACT;AAAA,EAGM,OAAO;AAAA;AACX,UAAI,KAAK,YAAY;AACnB,cAAM,KAAK,WAAW,MAAM;AAAA,MAC9B;AAAA,IACF;AAAA;AACF;","names":["s","infos"]}
1
+ {"version":3,"sources":["../framework/lib/runner.ts","../framework/SQLRunner.ts"],"sourcesContent":["export { ISQLRunner, BaseSQLRunner, SQLRunner, SQLiteRunner } from '../SQLRunner';\n","\nimport type * as sqlite from 'sqlite';\nimport type { Statement } from 'sqlite3';\nimport type mysql from 'mysql2/promise';\n\n\n\ntype QueryResult = {\n stmt: Statement;\n lastID: number;\n changes: number;\n}\n\nexport interface ISQLRunner {\n query(sql: string, params?: any[]): Promise<any>;\n execute(sql: string, params?: any[]): Promise<any>;\n end(): Promise<void>;\n}\n\n/**\n * Base class that implements the common contract and helper utilities that are\n * shared between the different dialect runners. The concrete subclasses only\n * need to implement the three primitive methods `_query`, `_execute` and\n * `_end` that perform the actual driver-specific interaction. Everything else\n * – such as ensuring a uniform return shape – is handled here once.\n */\nexport abstract class BaseSQLRunner implements ISQLRunner {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n abstract _query(sql: string, params?: any[]): Promise<any>;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n abstract _execute(sql: string, params?: any[]): Promise<any>;\n abstract _end(): Promise<void>;\n\n /**\n * Ensures that both MySQL and SQLite return the same tuple shape that callers\n * expect: `[rowsOrResult, extra]`. For SQLite there is no `extra` metadata\n * comparable to MySQL's `FieldPacket[]`, so we just use `undefined`.\n */\n async query(sql: string, params: any[] = []): Promise<any> {\n const result = await this._query(sql, params);\n\n // MySQL already returns the desired tuple. For SQLite we need to wrap.\n return Array.isArray(result) && result.length === 2 ? result : [result, undefined];\n }\n\n async execute(sql: string, params: any[] = []): Promise<any> {\n const result = await this._execute(sql, params);\n return Array.isArray(result) && result.length === 2 ? result : [result, undefined];\n }\n\n async end(): Promise<void> {\n await this._end();\n }\n}\n\nfunction isPromiseLike<T>(value: unknown): value is Promise<T> {\n return !!value && typeof (value as Promise<T>).then === \"function\";\n}\n\nexport class SQLRunner extends BaseSQLRunner {\n constructor(private connection: mysql.Connection) {\n super();\n }\n\n // The MySQL driver already returns the correct tuple shapes.\n async _query(sql: string, params: any[] = []) {\n return await this.connection.query(sql, params);\n }\n\n async _execute(sql: string, params: any[] = []) {\n return await this.connection.execute(sql, params);\n }\n\n async _end() {\n if (this.connection) {\n await this.connection.end();\n }\n }\n}\n\nexport class SQLiteRunner extends BaseSQLRunner {\n constructor(private connection: sqlite.Database | any) {\n super();\n }\n\n private async prepareStatement(sql: string) {\n if (typeof this.connection.prepare !== \"function\") {\n throw new Error(\"SQLite connection does not support prepare()\");\n }\n const stmt = this.connection.prepare(sql);\n return isPromiseLike(stmt) ? await stmt : stmt;\n }\n\n private async finalizeStatement(stmt: any) {\n if (!stmt || typeof stmt.finalize !== \"function\") return;\n const result = stmt.finalize();\n if (isPromiseLike(result)) {\n await result;\n }\n }\n\n private async statementAll(stmt: any, params: any[]) {\n if (typeof stmt.all !== \"function\") {\n throw new Error(\"SQLite statement does not support all()\");\n }\n // sqlite3 callback-style API\n if (stmt.all.length >= 2) {\n return await new Promise((resolve, reject) => {\n const callback = (err: Error | null, rows: any[]) => {\n if (err) return reject(err);\n resolve(rows || []);\n };\n try {\n if (params.length > 0) {\n stmt.all(params, callback);\n } else {\n stmt.all(callback);\n }\n } catch (error) {\n reject(error);\n }\n });\n }\n const result = stmt.all(...params);\n return isPromiseLike(result) ? await result : result;\n }\n\n private async statementRun(stmt: any, params: any[]) {\n if (typeof stmt.run !== \"function\") {\n throw new Error(\"SQLite statement does not support run()\");\n }\n if (stmt.run.length >= 2) {\n return await new Promise((resolve, reject) => {\n const callback = function (this: any, err: Error | null) {\n if (err) return reject(err);\n resolve({ changes: this?.changes ?? 0, lastID: this?.lastID });\n };\n try {\n if (params.length > 0) {\n stmt.run(params, callback);\n } else {\n stmt.run(callback);\n }\n } catch (error) {\n reject(error);\n }\n });\n }\n const result = stmt.run(...params);\n return isPromiseLike(result) ? await result : result;\n }\n\n async _query(sql: string, params: any[] = []) {\n const stmt = await this.prepareStatement(sql);\n try {\n const rows = await this.statementAll(stmt, params);\n return rows;\n } finally {\n await this.finalizeStatement(stmt);\n }\n }\n\n async _execute(sql: string, params: any[] = []) {\n // Handle empty or comment-only SQL gracefully\n if (this.isEmptySQL(sql)) {\n return { changes: 0, lastID: 0 }; // No-op result\n }\n\n if (this.isMultiStatement(sql)) {\n return await this.executeMultiStatement(sql, params)\n .catch((err) => {\n console.error((\n \"Error executing multi-statement SQL:\\n\"+\n `${sql}\\n`\n ), err);\n throw err;\n });\n }\n\n const stmt = await this.prepareStatement(sql);\n try {\n const info = await this.statementRun(stmt, params);\n return info;\n } finally {\n await this.finalizeStatement(stmt);\n }\n }\n\n /**\n * Checks if SQL is empty or contains only comments/whitespace.\n * Returns true if there is no actual SQL to execute.\n */\n private isEmptySQL(sql: string): boolean {\n // Remove all comments and whitespace\n const withoutComments = sql\n .replace(/--.*$/gm, '') // Remove single-line comments\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, '') // Remove multi-line comments\n .trim();\n return withoutComments.length === 0;\n }\n\n private isMultiStatement(sql: string): boolean {\n // SQLite does not support multiple statements in a single query.\n return sql.split(';').length > 1;\n }\n\n private async executeMultiStatement(sql: string, params: any[] = []) {\n const statements = sql.split(';')\n .map((s) => s.split('\\n')\n .map(\n s=>this.removeComments(s))\n .filter(s=>s.trim()!='')\n .join('\\n')\n )\n .filter((s) => s.trim() !== '');\n \n const infos = await statements.reduce(async (prev,statement) => {\n const infos = await prev;\n const stmt = await this.prepareStatement(`${statement};`);\n try {\n const info = await this.statementRun(stmt, params);\n infos.push(info);\n return infos;\n } finally {\n await this.finalizeStatement(stmt);\n }\n },Promise.resolve([null] as any[]))\n .then((infos:QueryResult[]) => {\n return infos.filter((info) => info !== null);\n })\n return infos.reduce((acc,latest)=>{\n if(latest){\n acc.stmt = latest.stmt;\n acc.lastID = latest.lastID;\n acc.changes += latest.changes;\n }\n return acc\n })\n }\n\n private removeComments(sql: string): string {\n // Remove single-line comments\n sql = sql.replace(/--.*$/gm, '');\n return sql;\n }\n\n\n async _end() {\n if (this.connection) {\n await this.connection.close();\n }\n }\n}\n\n/**\n * Minimal structural type for a `pg` Client or Pool (or anything shaped like\n * one, e.g. a Hyperdrive/Neon client). We only rely on `query()` and `end()`.\n */\nexport interface PgQueryable {\n query(text: string, values?: any[]): Promise<{ rows: any[]; rowCount: number | null }>;\n end?(): Promise<void>;\n}\n\n/**\n * PostgreSQL runner.\n *\n * Proper's internal bookkeeping statements use MySQL-style `?` placeholders;\n * Postgres wants `$1..$n`, so parameterised statements are rewritten here.\n * Migration files themselves are executed verbatim with no parameters — a\n * parameter-less `query()` goes through the simple protocol, which allows\n * multiple `;`-separated statements in one call, so no client-side splitting\n * (as SQLite needs) is required.\n */\nexport class PgRunner extends BaseSQLRunner {\n constructor(private connection: PgQueryable) {\n super();\n }\n\n /** `?` -> `$1`, `$2`, ... outside of string literals / comments. */\n static toPositional(sql: string): string {\n let out = '';\n let n = 0;\n let inSingle = false;\n let inDouble = false;\n let inLineComment = false;\n let inBlockComment = false;\n for (let i = 0; i < sql.length; i++) {\n const ch = sql[i];\n const next = sql[i + 1];\n if (inLineComment) {\n out += ch;\n if (ch === '\\n') inLineComment = false;\n continue;\n }\n if (inBlockComment) {\n out += ch;\n if (ch === '*' && next === '/') { out += next; i++; inBlockComment = false; }\n continue;\n }\n if (inSingle) {\n out += ch;\n if (ch === \"'\") inSingle = false;\n continue;\n }\n if (inDouble) {\n out += ch;\n if (ch === '\"') inDouble = false;\n continue;\n }\n if (ch === '-' && next === '-') { out += ch; inLineComment = true; continue; }\n if (ch === '/' && next === '*') { out += ch + next; i++; inBlockComment = true; continue; }\n if (ch === \"'\") { out += ch; inSingle = true; continue; }\n if (ch === '\"') { out += ch; inDouble = true; continue; }\n if (ch === '?') { out += `$${++n}`; continue; }\n out += ch;\n }\n return out;\n }\n\n private async run(sql: string, params: any[]) {\n if (params.length > 0) {\n return await this.connection.query(PgRunner.toPositional(sql), params);\n }\n return await this.connection.query(sql);\n }\n\n async _query(sql: string, params: any[] = []) {\n const result = await this.run(sql, params);\n return [result.rows, result];\n }\n\n async _execute(sql: string, params: any[] = []) {\n const result = await this.run(sql, params);\n return [{ changes: result.rowCount ?? 0, lastID: undefined }, result];\n }\n\n async _end() {\n if (this.connection && typeof this.connection.end === 'function') {\n await this.connection.end();\n }\n }\n}\n"],"mappings":"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","names":["s","infos"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../framework/SQLRunner.ts"],"sourcesContent":["\nimport * as sqlite from 'sqlite';\nimport { Statement } from 'sqlite3';\nimport mysql from 'mysql2/promise';\n\n\n\ntype QueryResult = {\n stmt: Statement;\n lastID: number;\n changes: number;\n}\n\nexport interface ISQLRunner {\n query(sql: string, params?: any[]): Promise<any>;\n execute(sql: string, params?: any[]): Promise<any>;\n end(): Promise<void>;\n}\n\n/**\n * Base class that implements the common contract and helper utilities that are\n * shared between the different dialect runners. The concrete subclasses only\n * need to implement the three primitive methods `_query`, `_execute` and\n * `_end` that perform the actual driver-specific interaction. Everything else\n * – such as ensuring a uniform return shape – is handled here once.\n */\nexport abstract class BaseSQLRunner implements ISQLRunner {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n abstract _query(sql: string, params?: any[]): Promise<any>;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n abstract _execute(sql: string, params?: any[]): Promise<any>;\n abstract _end(): Promise<void>;\n\n /**\n * Ensures that both MySQL and SQLite return the same tuple shape that callers\n * expect: `[rowsOrResult, extra]`. For SQLite there is no `extra` metadata\n * comparable to MySQL's `FieldPacket[]`, so we just use `undefined`.\n */\n async query(sql: string, params: any[] = []): Promise<any> {\n const result = await this._query(sql, params);\n\n // MySQL already returns the desired tuple. For SQLite we need to wrap.\n return Array.isArray(result) && result.length === 2 ? result : [result, undefined];\n }\n\n async execute(sql: string, params: any[] = []): Promise<any> {\n const result = await this._execute(sql, params);\n return Array.isArray(result) && result.length === 2 ? result : [result, undefined];\n }\n\n async end(): Promise<void> {\n await this._end();\n }\n}\n\nfunction isPromiseLike<T>(value: unknown): value is Promise<T> {\n return !!value && typeof (value as Promise<T>).then === \"function\";\n}\n\nexport class SQLRunner extends BaseSQLRunner {\n constructor(private connection: mysql.Connection) {\n super();\n }\n\n // The MySQL driver already returns the correct tuple shapes.\n async _query(sql: string, params: any[] = []) {\n return await this.connection.query(sql, params);\n }\n\n async _execute(sql: string, params: any[] = []) {\n return await this.connection.execute(sql, params);\n }\n\n async _end() {\n if (this.connection) {\n await this.connection.end();\n }\n }\n}\n\nexport class SQLiteRunner extends BaseSQLRunner {\n constructor(private connection: sqlite.Database | any) {\n super();\n }\n\n private async prepareStatement(sql: string) {\n if (typeof this.connection.prepare !== \"function\") {\n throw new Error(\"SQLite connection does not support prepare()\");\n }\n const stmt = this.connection.prepare(sql);\n return isPromiseLike(stmt) ? await stmt : stmt;\n }\n\n private async finalizeStatement(stmt: any) {\n if (!stmt || typeof stmt.finalize !== \"function\") return;\n const result = stmt.finalize();\n if (isPromiseLike(result)) {\n await result;\n }\n }\n\n private async statementAll(stmt: any, params: any[]) {\n if (typeof stmt.all !== \"function\") {\n throw new Error(\"SQLite statement does not support all()\");\n }\n // sqlite3 callback-style API\n if (stmt.all.length >= 2) {\n return await new Promise((resolve, reject) => {\n const callback = (err: Error | null, rows: any[]) => {\n if (err) return reject(err);\n resolve(rows || []);\n };\n try {\n if (params.length > 0) {\n stmt.all(params, callback);\n } else {\n stmt.all(callback);\n }\n } catch (error) {\n reject(error);\n }\n });\n }\n const result = stmt.all(...params);\n return isPromiseLike(result) ? await result : result;\n }\n\n private async statementRun(stmt: any, params: any[]) {\n if (typeof stmt.run !== \"function\") {\n throw new Error(\"SQLite statement does not support run()\");\n }\n if (stmt.run.length >= 2) {\n return await new Promise((resolve, reject) => {\n const callback = function (this: any, err: Error | null) {\n if (err) return reject(err);\n resolve({ changes: this?.changes ?? 0, lastID: this?.lastID });\n };\n try {\n if (params.length > 0) {\n stmt.run(params, callback);\n } else {\n stmt.run(callback);\n }\n } catch (error) {\n reject(error);\n }\n });\n }\n const result = stmt.run(...params);\n return isPromiseLike(result) ? await result : result;\n }\n\n async _query(sql: string, params: any[] = []) {\n const stmt = await this.prepareStatement(sql);\n try {\n const rows = await this.statementAll(stmt, params);\n return rows;\n } finally {\n await this.finalizeStatement(stmt);\n }\n }\n\n async _execute(sql: string, params: any[] = []) {\n // Handle empty or comment-only SQL gracefully\n if (this.isEmptySQL(sql)) {\n return { changes: 0, lastID: 0 }; // No-op result\n }\n\n if (this.isMultiStatement(sql)) {\n return await this.executeMultiStatement(sql, params)\n .catch((err) => {\n console.error((\n \"Error executing multi-statement SQL:\\n\"+\n `${sql}\\n`\n ), err);\n throw err;\n });\n }\n\n const stmt = await this.prepareStatement(sql);\n try {\n const info = await this.statementRun(stmt, params);\n return info;\n } finally {\n await this.finalizeStatement(stmt);\n }\n }\n\n /**\n * Checks if SQL is empty or contains only comments/whitespace.\n * Returns true if there is no actual SQL to execute.\n */\n private isEmptySQL(sql: string): boolean {\n // Remove all comments and whitespace\n const withoutComments = sql\n .replace(/--.*$/gm, '') // Remove single-line comments\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, '') // Remove multi-line comments\n .trim();\n return withoutComments.length === 0;\n }\n\n private isMultiStatement(sql: string): boolean {\n // SQLite does not support multiple statements in a single query.\n return sql.split(';').length > 1;\n }\n\n private async executeMultiStatement(sql: string, params: any[] = []) {\n const statements = sql.split(';')\n .map((s) => s.split('\\n')\n .map(\n s=>this.removeComments(s))\n .filter(s=>s.trim()!='')\n .join('\\n')\n )\n .filter((s) => s.trim() !== '');\n \n const infos = await statements.reduce(async (prev,statement) => {\n const infos = await prev;\n const stmt = await this.prepareStatement(`${statement};`);\n try {\n const info = await this.statementRun(stmt, params);\n infos.push(info);\n return infos;\n } finally {\n await this.finalizeStatement(stmt);\n }\n },Promise.resolve([null] as any[]))\n .then((infos:QueryResult[]) => {\n return infos.filter((info) => info !== null);\n })\n return infos.reduce((acc,latest)=>{\n if(latest){\n acc.stmt = latest.stmt;\n acc.lastID = latest.lastID;\n acc.changes += latest.changes;\n }\n return acc\n })\n }\n\n private removeComments(sql: string): string {\n // Remove single-line comments\n sql = sql.replace(/--.*$/gm, '');\n return sql;\n }\n\n\n async _end() {\n if (this.connection) {\n await this.connection.close();\n }\n }\n}\n"],"mappings":"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","names":["s","infos"]}
1
+ {"version":3,"sources":["../framework/SQLRunner.ts"],"sourcesContent":["\nimport type * as sqlite from 'sqlite';\nimport type { Statement } from 'sqlite3';\nimport type mysql from 'mysql2/promise';\n\n\n\ntype QueryResult = {\n stmt: Statement;\n lastID: number;\n changes: number;\n}\n\nexport interface ISQLRunner {\n query(sql: string, params?: any[]): Promise<any>;\n execute(sql: string, params?: any[]): Promise<any>;\n end(): Promise<void>;\n}\n\n/**\n * Base class that implements the common contract and helper utilities that are\n * shared between the different dialect runners. The concrete subclasses only\n * need to implement the three primitive methods `_query`, `_execute` and\n * `_end` that perform the actual driver-specific interaction. Everything else\n * – such as ensuring a uniform return shape – is handled here once.\n */\nexport abstract class BaseSQLRunner implements ISQLRunner {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n abstract _query(sql: string, params?: any[]): Promise<any>;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n abstract _execute(sql: string, params?: any[]): Promise<any>;\n abstract _end(): Promise<void>;\n\n /**\n * Ensures that both MySQL and SQLite return the same tuple shape that callers\n * expect: `[rowsOrResult, extra]`. For SQLite there is no `extra` metadata\n * comparable to MySQL's `FieldPacket[]`, so we just use `undefined`.\n */\n async query(sql: string, params: any[] = []): Promise<any> {\n const result = await this._query(sql, params);\n\n // MySQL already returns the desired tuple. For SQLite we need to wrap.\n return Array.isArray(result) && result.length === 2 ? result : [result, undefined];\n }\n\n async execute(sql: string, params: any[] = []): Promise<any> {\n const result = await this._execute(sql, params);\n return Array.isArray(result) && result.length === 2 ? result : [result, undefined];\n }\n\n async end(): Promise<void> {\n await this._end();\n }\n}\n\nfunction isPromiseLike<T>(value: unknown): value is Promise<T> {\n return !!value && typeof (value as Promise<T>).then === \"function\";\n}\n\nexport class SQLRunner extends BaseSQLRunner {\n constructor(private connection: mysql.Connection) {\n super();\n }\n\n // The MySQL driver already returns the correct tuple shapes.\n async _query(sql: string, params: any[] = []) {\n return await this.connection.query(sql, params);\n }\n\n async _execute(sql: string, params: any[] = []) {\n return await this.connection.execute(sql, params);\n }\n\n async _end() {\n if (this.connection) {\n await this.connection.end();\n }\n }\n}\n\nexport class SQLiteRunner extends BaseSQLRunner {\n constructor(private connection: sqlite.Database | any) {\n super();\n }\n\n private async prepareStatement(sql: string) {\n if (typeof this.connection.prepare !== \"function\") {\n throw new Error(\"SQLite connection does not support prepare()\");\n }\n const stmt = this.connection.prepare(sql);\n return isPromiseLike(stmt) ? await stmt : stmt;\n }\n\n private async finalizeStatement(stmt: any) {\n if (!stmt || typeof stmt.finalize !== \"function\") return;\n const result = stmt.finalize();\n if (isPromiseLike(result)) {\n await result;\n }\n }\n\n private async statementAll(stmt: any, params: any[]) {\n if (typeof stmt.all !== \"function\") {\n throw new Error(\"SQLite statement does not support all()\");\n }\n // sqlite3 callback-style API\n if (stmt.all.length >= 2) {\n return await new Promise((resolve, reject) => {\n const callback = (err: Error | null, rows: any[]) => {\n if (err) return reject(err);\n resolve(rows || []);\n };\n try {\n if (params.length > 0) {\n stmt.all(params, callback);\n } else {\n stmt.all(callback);\n }\n } catch (error) {\n reject(error);\n }\n });\n }\n const result = stmt.all(...params);\n return isPromiseLike(result) ? await result : result;\n }\n\n private async statementRun(stmt: any, params: any[]) {\n if (typeof stmt.run !== \"function\") {\n throw new Error(\"SQLite statement does not support run()\");\n }\n if (stmt.run.length >= 2) {\n return await new Promise((resolve, reject) => {\n const callback = function (this: any, err: Error | null) {\n if (err) return reject(err);\n resolve({ changes: this?.changes ?? 0, lastID: this?.lastID });\n };\n try {\n if (params.length > 0) {\n stmt.run(params, callback);\n } else {\n stmt.run(callback);\n }\n } catch (error) {\n reject(error);\n }\n });\n }\n const result = stmt.run(...params);\n return isPromiseLike(result) ? await result : result;\n }\n\n async _query(sql: string, params: any[] = []) {\n const stmt = await this.prepareStatement(sql);\n try {\n const rows = await this.statementAll(stmt, params);\n return rows;\n } finally {\n await this.finalizeStatement(stmt);\n }\n }\n\n async _execute(sql: string, params: any[] = []) {\n // Handle empty or comment-only SQL gracefully\n if (this.isEmptySQL(sql)) {\n return { changes: 0, lastID: 0 }; // No-op result\n }\n\n if (this.isMultiStatement(sql)) {\n return await this.executeMultiStatement(sql, params)\n .catch((err) => {\n console.error((\n \"Error executing multi-statement SQL:\\n\"+\n `${sql}\\n`\n ), err);\n throw err;\n });\n }\n\n const stmt = await this.prepareStatement(sql);\n try {\n const info = await this.statementRun(stmt, params);\n return info;\n } finally {\n await this.finalizeStatement(stmt);\n }\n }\n\n /**\n * Checks if SQL is empty or contains only comments/whitespace.\n * Returns true if there is no actual SQL to execute.\n */\n private isEmptySQL(sql: string): boolean {\n // Remove all comments and whitespace\n const withoutComments = sql\n .replace(/--.*$/gm, '') // Remove single-line comments\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, '') // Remove multi-line comments\n .trim();\n return withoutComments.length === 0;\n }\n\n private isMultiStatement(sql: string): boolean {\n // SQLite does not support multiple statements in a single query.\n return sql.split(';').length > 1;\n }\n\n private async executeMultiStatement(sql: string, params: any[] = []) {\n const statements = sql.split(';')\n .map((s) => s.split('\\n')\n .map(\n s=>this.removeComments(s))\n .filter(s=>s.trim()!='')\n .join('\\n')\n )\n .filter((s) => s.trim() !== '');\n \n const infos = await statements.reduce(async (prev,statement) => {\n const infos = await prev;\n const stmt = await this.prepareStatement(`${statement};`);\n try {\n const info = await this.statementRun(stmt, params);\n infos.push(info);\n return infos;\n } finally {\n await this.finalizeStatement(stmt);\n }\n },Promise.resolve([null] as any[]))\n .then((infos:QueryResult[]) => {\n return infos.filter((info) => info !== null);\n })\n return infos.reduce((acc,latest)=>{\n if(latest){\n acc.stmt = latest.stmt;\n acc.lastID = latest.lastID;\n acc.changes += latest.changes;\n }\n return acc\n })\n }\n\n private removeComments(sql: string): string {\n // Remove single-line comments\n sql = sql.replace(/--.*$/gm, '');\n return sql;\n }\n\n\n async _end() {\n if (this.connection) {\n await this.connection.close();\n }\n }\n}\n\n/**\n * Minimal structural type for a `pg` Client or Pool (or anything shaped like\n * one, e.g. a Hyperdrive/Neon client). We only rely on `query()` and `end()`.\n */\nexport interface PgQueryable {\n query(text: string, values?: any[]): Promise<{ rows: any[]; rowCount: number | null }>;\n end?(): Promise<void>;\n}\n\n/**\n * PostgreSQL runner.\n *\n * Proper's internal bookkeeping statements use MySQL-style `?` placeholders;\n * Postgres wants `$1..$n`, so parameterised statements are rewritten here.\n * Migration files themselves are executed verbatim with no parameters — a\n * parameter-less `query()` goes through the simple protocol, which allows\n * multiple `;`-separated statements in one call, so no client-side splitting\n * (as SQLite needs) is required.\n */\nexport class PgRunner extends BaseSQLRunner {\n constructor(private connection: PgQueryable) {\n super();\n }\n\n /** `?` -> `$1`, `$2`, ... outside of string literals / comments. */\n static toPositional(sql: string): string {\n let out = '';\n let n = 0;\n let inSingle = false;\n let inDouble = false;\n let inLineComment = false;\n let inBlockComment = false;\n for (let i = 0; i < sql.length; i++) {\n const ch = sql[i];\n const next = sql[i + 1];\n if (inLineComment) {\n out += ch;\n if (ch === '\\n') inLineComment = false;\n continue;\n }\n if (inBlockComment) {\n out += ch;\n if (ch === '*' && next === '/') { out += next; i++; inBlockComment = false; }\n continue;\n }\n if (inSingle) {\n out += ch;\n if (ch === \"'\") inSingle = false;\n continue;\n }\n if (inDouble) {\n out += ch;\n if (ch === '\"') inDouble = false;\n continue;\n }\n if (ch === '-' && next === '-') { out += ch; inLineComment = true; continue; }\n if (ch === '/' && next === '*') { out += ch + next; i++; inBlockComment = true; continue; }\n if (ch === \"'\") { out += ch; inSingle = true; continue; }\n if (ch === '\"') { out += ch; inDouble = true; continue; }\n if (ch === '?') { out += `$${++n}`; continue; }\n out += ch;\n }\n return out;\n }\n\n private async run(sql: string, params: any[]) {\n if (params.length > 0) {\n return await this.connection.query(PgRunner.toPositional(sql), params);\n }\n return await this.connection.query(sql);\n }\n\n async _query(sql: string, params: any[] = []) {\n const result = await this.run(sql, params);\n return [result.rows, result];\n }\n\n async _execute(sql: string, params: any[] = []) {\n const result = await this.run(sql, params);\n return [{ changes: result.rowCount ?? 0, lastID: undefined }, result];\n }\n\n async _end() {\n if (this.connection && typeof this.connection.end === 'function') {\n await this.connection.end();\n }\n }\n}\n"],"mappings":"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","names":["s","infos"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noego/proper",
3
- "version": "0.0.9",
3
+ "version": "0.2.0",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "module": "bin/index.mjs",
@@ -41,6 +41,7 @@
41
41
  "devDependencies": {
42
42
  "@types/jest": "^29.5.12",
43
43
  "@types/node": "^20.11.25",
44
+ "@types/pg": "^8.23.1",
44
45
  "jest": "^29.7.0",
45
46
  "jest-extended": "^4.0.2",
46
47
  "ts-jest": "^29.2.5",
@@ -51,7 +52,6 @@
51
52
  "typesync": "^0.13.0"
52
53
  },
53
54
  "dependencies": {
54
- "tsx": "^4.19.4",
55
55
  "@types/argparse": "^2.0.15",
56
56
  "@types/minimist": "^1.2.5",
57
57
  "ajv": "^8.17.1",
@@ -60,8 +60,11 @@
60
60
  "console-table-printer": "^2.12.0",
61
61
  "minimist": "^1.2.8",
62
62
  "mysql2": "^3.9.2",
63
+ "pg": "^8.23.0",
63
64
  "source-map-support": "^0.5.21",
64
65
  "sqlite": "^5.1.1",
65
- "sqlite3": "^5.1.7"
66
+ "sqlite3": "^5.1.7",
67
+ "tsx": "^4.19.4",
68
+ "yaml": "^2.9.0"
66
69
  }
67
70
  }
package/readme.md CHANGED
@@ -266,6 +266,98 @@ describe('Database Tests', () => {
266
266
 
267
267
  ---
268
268
 
269
+ ## Ledger Patches (`proper patch`)
270
+
271
+ Ledger patches are one-shot, versioned YAML files that repair `proper_migrations`
272
+ history **without executing schema SQL**. Use them when applied migration files
273
+ were renamed after installation, when a ledger row is missing for schema that
274
+ already exists, or when a ledger row wrongly claims success.
275
+
276
+ Patches are discovered and applied automatically before Proper reads migration
277
+ completion state (CLI and `MigrationRunnerFactory.create()` alike), are
278
+ transactional, are applied at most once per configured migration ledger, and
279
+ work identically for MySQL/MariaDB, SQLite, and PostgreSQL.
280
+
281
+ ### Creating a patch
282
+
283
+ ```bash
284
+ proper patch fix_renamed_keys
285
+ proper patch --name fix_renamed_keys
286
+ ```
287
+
288
+ This scaffolds `<patch_folder>/<timestamp>_fix_renamed_keys.yaml` without
289
+ opening a database. The scaffold's `operations: []` is intentionally not
290
+ runnable — complete the file before running another database-backed command.
291
+
292
+ ### Configuration
293
+
294
+ ```jsonc
295
+ {
296
+ "patch_folder": "patches", // default: a `patches` sibling of migration_folder
297
+ "patch_table": "proper_patches" // default
298
+ }
299
+ ```
300
+
301
+ ### Patch file format (version 1)
302
+
303
+ ```yaml
304
+ version: 1
305
+ description: fix hand-stamped migration names
306
+ operations:
307
+ - rename_migration:
308
+ from: 1787905000000_create_conversation_run_lifecycle
309
+ to: 1787941600000_create_conversation_run_lifecycle
310
+ - mark_applied:
311
+ key: 1787941700000_existing_schema_baseline
312
+ - unmark_applied:
313
+ key: 1787941800000_incorrect_ledger_row
314
+ ```
315
+
316
+ Operations run in file order, then operation order. Each is a conditional
317
+ bookkeeping repair:
318
+
319
+ - `rename_migration` — moves a ledger row from `from` to `to`; a no-op when the
320
+ database already converged; a hard conflict when both keys exist.
321
+ - `mark_applied` — inserts a ledger row without running `up` SQL (you are
322
+ responsible for verifying the schema already matches).
323
+ - `unmark_applied` — deletes a ledger row without running `down` SQL, making
324
+ the migration pending again.
325
+
326
+ Parsing is strict: unknown fields/verbs, duplicate keys, YAML anchors/aliases,
327
+ merge keys, and custom tags are rejected. There is no raw SQL escape hatch.
328
+
329
+ ### Immutability
330
+
331
+ **Patch files are permanent, append-only migration history.** Once any database
332
+ records a patch, the file must never be edited, renamed, or deleted — Proper
333
+ stores a SHA-256 checksum at application time and refuses to run migrations if
334
+ an applied patch's file is missing or changed. A correction is a new, later
335
+ patch (`A -> B -> C` and `A -> B -> A` chains are supported and validated
336
+ against the current migration files).
337
+
338
+ ### Deployment and rollback warning
339
+
340
+ A history rename must ship the renamed migration files, the patch file(s), and
341
+ a patch-aware Proper version **in the same application artifact**. Do not run
342
+ old and new application versions' migration startup concurrently during the
343
+ first deployment of a rename. After a rename patch commits, rolling back to an
344
+ artifact that only contains the old migration filenames is unsafe — that old
345
+ runner can attempt the old migration again. Roll back only to artifacts
346
+ consistent with the repaired ledger, or restore the database separately.
347
+
348
+ `proper down` and `proper reset` never touch patch history; destructive
349
+ `terminate()` drops both the migration table and the patch table.
350
+
351
+ ### Programmatic API
352
+
353
+ ```ts
354
+ const runner = await MigrationRunnerFactory.create("proper.json"); // patches already applied
355
+ const results: PatchApplyResult[] = await runner.applyPendingPatches(); // idempotent
356
+ const path: string = runner.createPatch("fix_renamed_keys"); // scaffold, no DB
357
+ ```
358
+
359
+ ---
360
+
269
361
  ## Seeding
270
362
 
271
363
  SQL Proper supports a powerful seeding feature that allows you to populate your database with initial data (reference data, test users, etc.). Seeds can be SQL files or JavaScript/TypeScript modules.