forge-sql-orm 2.0.17 → 2.0.19
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/README.md +95 -4
- package/dist/ForgeSQLORM.js +382 -60
- package/dist/ForgeSQLORM.js.map +1 -1
- package/dist/ForgeSQLORM.mjs +382 -60
- package/dist/ForgeSQLORM.mjs.map +1 -1
- package/dist/core/ForgeSQLAnalyseOperations.d.ts +250 -0
- package/dist/core/ForgeSQLAnalyseOperations.d.ts.map +1 -0
- package/dist/core/ForgeSQLCrudOperations.d.ts +1 -1
- package/dist/core/ForgeSQLCrudOperations.d.ts.map +1 -1
- package/dist/core/ForgeSQLORM.d.ts +12 -2
- package/dist/core/ForgeSQLORM.d.ts.map +1 -1
- package/dist/core/ForgeSQLQueryBuilder.d.ts +112 -21
- package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
- package/dist/core/ForgeSQLSelectOperations.d.ts.map +1 -1
- package/dist/core/SystemTables.d.ts +167 -0
- package/dist/core/SystemTables.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/utils/forgeDriverProxy.d.ts +11 -0
- package/dist/utils/forgeDriverProxy.d.ts.map +1 -0
- package/dist/utils/sqlHints.d.ts +21 -0
- package/dist/utils/sqlHints.d.ts.map +1 -0
- package/dist/utils/sqlUtils.d.ts +2 -8
- package/dist/utils/sqlUtils.d.ts.map +1 -1
- package/dist/webtriggers/applyMigrationsWebTrigger.d.ts.map +1 -1
- package/dist/webtriggers/dropMigrationWebTrigger.d.ts +2 -4
- package/dist/webtriggers/dropMigrationWebTrigger.d.ts.map +1 -1
- package/package.json +4 -12
- package/src/core/ForgeSQLAnalyseOperations.ts +461 -0
- package/src/core/ForgeSQLCrudOperations.ts +15 -8
- package/src/core/ForgeSQLORM.ts +46 -9
- package/src/core/ForgeSQLQueryBuilder.ts +129 -32
- package/src/core/ForgeSQLSelectOperations.ts +4 -6
- package/src/core/SystemTables.ts +175 -0
- package/src/index.ts +1 -0
- package/src/utils/forgeDriverProxy.ts +27 -0
- package/src/utils/sqlHints.ts +63 -0
- package/src/utils/sqlUtils.ts +36 -32
- package/src/webtriggers/applyMigrationsWebTrigger.ts +32 -16
- package/src/webtriggers/dropMigrationWebTrigger.ts +5 -6
- package/src/webtriggers/fetchSchemaWebTrigger.ts +2 -10
package/dist/ForgeSQLORM.mjs
CHANGED
|
@@ -7,16 +7,23 @@ import { customType, mysqlTable, timestamp, varchar, bigint } from "drizzle-orm/
|
|
|
7
7
|
import moment$1 from "moment/moment.js";
|
|
8
8
|
import { getTableName } from "drizzle-orm/table";
|
|
9
9
|
const parseDateTime = (value, format) => {
|
|
10
|
+
let result;
|
|
10
11
|
const m = moment(value, format, true);
|
|
11
12
|
if (!m.isValid()) {
|
|
12
|
-
|
|
13
|
+
const momentDate = moment(value);
|
|
14
|
+
if (momentDate.isValid()) {
|
|
15
|
+
result = momentDate.toDate();
|
|
16
|
+
} else {
|
|
17
|
+
result = new Date(value);
|
|
18
|
+
}
|
|
19
|
+
} else {
|
|
20
|
+
result = m.toDate();
|
|
21
|
+
}
|
|
22
|
+
if (isNaN(result.getTime())) {
|
|
23
|
+
result = new Date(value);
|
|
13
24
|
}
|
|
14
|
-
return
|
|
25
|
+
return result;
|
|
15
26
|
};
|
|
16
|
-
function extractAlias(query) {
|
|
17
|
-
const match = query.match(/\bas\s+(['"`]?)([\w*]+)\1$/i);
|
|
18
|
-
return match ? match[2] : query;
|
|
19
|
-
}
|
|
20
27
|
function getPrimaryKeys(table) {
|
|
21
28
|
const { columns, primaryKeys } = getTableMetadata(table);
|
|
22
29
|
const columnPrimaryKeys = Object.entries(columns).filter(([, column]) => column.primary);
|
|
@@ -55,7 +62,7 @@ function processForeignKeys(table, foreignKeysSymbol, extraSymbol) {
|
|
|
55
62
|
const configBuilderData = extraConfigBuilder(table);
|
|
56
63
|
if (configBuilderData) {
|
|
57
64
|
const configBuilders = Array.isArray(configBuilderData) ? configBuilderData : Object.values(configBuilderData).map(
|
|
58
|
-
(item) => item.value
|
|
65
|
+
(item) => item.value ?? item
|
|
59
66
|
);
|
|
60
67
|
configBuilders.forEach((builder) => {
|
|
61
68
|
if (!builder?.constructor) return;
|
|
@@ -90,7 +97,7 @@ function getTableMetadata(table) {
|
|
|
90
97
|
const configBuilderData = extraConfigBuilder(table);
|
|
91
98
|
if (configBuilderData) {
|
|
92
99
|
const configBuilders = Array.isArray(configBuilderData) ? configBuilderData : Object.values(configBuilderData).map(
|
|
93
|
-
(item) => item.value
|
|
100
|
+
(item) => item.value ?? item
|
|
94
101
|
);
|
|
95
102
|
configBuilders.forEach((builder) => {
|
|
96
103
|
if (!builder?.constructor) return;
|
|
@@ -120,13 +127,9 @@ function getTableMetadata(table) {
|
|
|
120
127
|
}
|
|
121
128
|
function generateDropTableStatements(tables) {
|
|
122
129
|
const dropStatements = [];
|
|
123
|
-
tables.forEach((
|
|
124
|
-
|
|
125
|
-
if (tableMetadata.tableName) {
|
|
126
|
-
dropStatements.push(`DROP TABLE IF EXISTS \`${tableMetadata.tableName}\`;`);
|
|
127
|
-
}
|
|
130
|
+
tables.forEach((tableName) => {
|
|
131
|
+
dropStatements.push(`DROP TABLE IF EXISTS \`${tableName}\`;`);
|
|
128
132
|
});
|
|
129
|
-
dropStatements.push(`DELETE FROM __migrations;`);
|
|
130
133
|
return dropStatements;
|
|
131
134
|
}
|
|
132
135
|
function mapSelectTableToAlias(table, uniqPrefix, aliasMap) {
|
|
@@ -184,9 +187,9 @@ function getAliasFromDrizzleAlias(value) {
|
|
|
184
187
|
const aliasNameChunk = queryChunks[queryChunks.length - 2];
|
|
185
188
|
if (isSQLWrapper(aliasNameChunk) && "queryChunks" in aliasNameChunk) {
|
|
186
189
|
const aliasNameChunkSql = aliasNameChunk;
|
|
187
|
-
if (aliasNameChunkSql
|
|
190
|
+
if (aliasNameChunkSql.queryChunks?.length === 1 && aliasNameChunkSql.queryChunks[0]) {
|
|
188
191
|
const queryChunksStringChunc = aliasNameChunkSql.queryChunks[0];
|
|
189
|
-
if (
|
|
192
|
+
if ("value" in queryChunksStringChunc) {
|
|
190
193
|
const values = queryChunksStringChunc.value;
|
|
191
194
|
if (values && values.length === 1) {
|
|
192
195
|
return values[0];
|
|
@@ -238,7 +241,10 @@ function applyFromDriverTransform(rows, selections, aliasMap) {
|
|
|
238
241
|
});
|
|
239
242
|
}
|
|
240
243
|
function processNullBranches(obj) {
|
|
241
|
-
if (obj === null || typeof obj !== "object"
|
|
244
|
+
if (obj === null || typeof obj !== "object") {
|
|
245
|
+
return obj;
|
|
246
|
+
}
|
|
247
|
+
if (obj.constructor && obj.constructor.name !== "Object") {
|
|
242
248
|
return obj;
|
|
243
249
|
}
|
|
244
250
|
const result = {};
|
|
@@ -248,7 +254,7 @@ function processNullBranches(obj) {
|
|
|
248
254
|
result[key] = null;
|
|
249
255
|
continue;
|
|
250
256
|
}
|
|
251
|
-
if (typeof value === "object"
|
|
257
|
+
if (typeof value === "object") {
|
|
252
258
|
const processed = processNullBranches(value);
|
|
253
259
|
result[key] = processed;
|
|
254
260
|
if (processed !== null) {
|
|
@@ -436,7 +442,7 @@ class ForgeSQLCrudOperations {
|
|
|
436
442
|
let fieldName = versionMetadata.fieldName;
|
|
437
443
|
let versionField = columns[versionMetadata.fieldName];
|
|
438
444
|
if (!versionField) {
|
|
439
|
-
const find = Object.entries(columns).find(([
|
|
445
|
+
const find = Object.entries(columns).find(([, c]) => c.name === versionMetadata.fieldName);
|
|
440
446
|
if (find) {
|
|
441
447
|
fieldName = find[0];
|
|
442
448
|
versionField = find[1];
|
|
@@ -498,12 +504,20 @@ class ForgeSQLCrudOperations {
|
|
|
498
504
|
*/
|
|
499
505
|
prepareModelWithVersion(model, versionMetadata, columns) {
|
|
500
506
|
if (!versionMetadata || !columns) return model;
|
|
501
|
-
|
|
507
|
+
let fieldName = versionMetadata.fieldName;
|
|
508
|
+
let versionField = columns[versionMetadata.fieldName];
|
|
509
|
+
if (!versionField) {
|
|
510
|
+
const find = Object.entries(columns).find(([, c]) => c.name === versionMetadata.fieldName);
|
|
511
|
+
if (find) {
|
|
512
|
+
fieldName = find[0];
|
|
513
|
+
versionField = find[1];
|
|
514
|
+
}
|
|
515
|
+
}
|
|
502
516
|
if (!versionField) return model;
|
|
503
517
|
const modelWithVersion = { ...model };
|
|
504
518
|
const fieldType = versionField.getSQLType();
|
|
505
519
|
const versionValue = fieldType === "datetime" || fieldType === "timestamp" ? /* @__PURE__ */ new Date() : 1;
|
|
506
|
-
modelWithVersion[
|
|
520
|
+
modelWithVersion[fieldName] = versionValue;
|
|
507
521
|
return modelWithVersion;
|
|
508
522
|
}
|
|
509
523
|
/**
|
|
@@ -590,9 +604,8 @@ class ForgeSQLSelectOperations {
|
|
|
590
604
|
*/
|
|
591
605
|
async executeRawSQL(query, params) {
|
|
592
606
|
if (this.options.logRawSqlQuery) {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
);
|
|
607
|
+
const paramsStr = params ? `, with params: ${JSON.stringify(params)}` : "";
|
|
608
|
+
console.debug(`Executing with SQL ${query}${paramsStr}`);
|
|
596
609
|
}
|
|
597
610
|
const sqlStatement = sql$1.prepare(query);
|
|
598
611
|
if (params) {
|
|
@@ -614,7 +627,7 @@ class ForgeSQLSelectOperations {
|
|
|
614
627
|
}
|
|
615
628
|
if (this.options.logRawSqlQuery) {
|
|
616
629
|
console.debug(
|
|
617
|
-
`Executing Update with SQL ${query}` + params ? `, with params: ${JSON.stringify(params)}` : ""
|
|
630
|
+
`Executing Update with SQL ${query}` + (params ? `, with params: ${JSON.stringify(params)}` : "")
|
|
618
631
|
);
|
|
619
632
|
}
|
|
620
633
|
const updateQueryResponseResults = await sqlStatement.execute();
|
|
@@ -646,6 +659,45 @@ const forgeDriver = async (query, params, method) => {
|
|
|
646
659
|
throw error;
|
|
647
660
|
}
|
|
648
661
|
};
|
|
662
|
+
function injectSqlHints(query, hints) {
|
|
663
|
+
if (!hints) {
|
|
664
|
+
return query;
|
|
665
|
+
}
|
|
666
|
+
const normalizedQuery = query.trim().toUpperCase();
|
|
667
|
+
let queryHints;
|
|
668
|
+
if (normalizedQuery.startsWith("SELECT")) {
|
|
669
|
+
queryHints = hints.select;
|
|
670
|
+
} else if (normalizedQuery.startsWith("INSERT")) {
|
|
671
|
+
queryHints = hints.insert;
|
|
672
|
+
} else if (normalizedQuery.startsWith("UPDATE")) {
|
|
673
|
+
queryHints = hints.update;
|
|
674
|
+
} else if (normalizedQuery.startsWith("DELETE")) {
|
|
675
|
+
queryHints = hints.delete;
|
|
676
|
+
}
|
|
677
|
+
if (!queryHints || queryHints.length === 0) {
|
|
678
|
+
return query;
|
|
679
|
+
}
|
|
680
|
+
const hintsString = queryHints.join(" ");
|
|
681
|
+
if (normalizedQuery.startsWith("SELECT")) {
|
|
682
|
+
return `SELECT /*+ ${hintsString} */ ${query.substring(6)}`;
|
|
683
|
+
} else if (normalizedQuery.startsWith("INSERT")) {
|
|
684
|
+
return `INSERT /*+ ${hintsString} */ ${query.substring(6)}`;
|
|
685
|
+
} else if (normalizedQuery.startsWith("UPDATE")) {
|
|
686
|
+
return `UPDATE /*+ ${hintsString} */ ${query.substring(6)}`;
|
|
687
|
+
} else if (normalizedQuery.startsWith("DELETE")) {
|
|
688
|
+
return `DELETE /*+ ${hintsString} */ ${query.substring(6)}`;
|
|
689
|
+
}
|
|
690
|
+
return query;
|
|
691
|
+
}
|
|
692
|
+
function createForgeDriverProxy(options, logRawSqlQuery) {
|
|
693
|
+
return async (query, params, method) => {
|
|
694
|
+
const modifiedQuery = injectSqlHints(query, options);
|
|
695
|
+
if (options && logRawSqlQuery && modifiedQuery !== query) {
|
|
696
|
+
console.warn("modified query: " + modifiedQuery);
|
|
697
|
+
}
|
|
698
|
+
return forgeDriver(modifiedQuery, params, method);
|
|
699
|
+
};
|
|
700
|
+
}
|
|
649
701
|
function createAliasedSelectBuilder(db, fields, selectFn) {
|
|
650
702
|
const { selections, aliasMap } = mapSelectFieldsWithAlias(fields);
|
|
651
703
|
const builder = selectFn(selections);
|
|
@@ -689,11 +741,239 @@ function patchDbWithSelectAliased(db) {
|
|
|
689
741
|
};
|
|
690
742
|
return db;
|
|
691
743
|
}
|
|
744
|
+
class ForgeSQLAnalyseOperation {
|
|
745
|
+
forgeOperations;
|
|
746
|
+
/**
|
|
747
|
+
* Creates a new instance of ForgeSQLAnalizeOperation.
|
|
748
|
+
* @param {ForgeSqlOperation} forgeOperations - The ForgeSQL operations instance
|
|
749
|
+
*/
|
|
750
|
+
constructor(forgeOperations) {
|
|
751
|
+
this.forgeOperations = forgeOperations;
|
|
752
|
+
this.mapToCamelCaseClusterStatement = this.mapToCamelCaseClusterStatement.bind(this);
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Executes EXPLAIN on a raw SQL query.
|
|
756
|
+
* @param {string} query - The SQL query to analyze
|
|
757
|
+
* @param {unknown[]} bindParams - The query parameters
|
|
758
|
+
* @returns {Promise<ExplainAnalyzeRow[]>} The execution plan analysis results
|
|
759
|
+
*/
|
|
760
|
+
async explainRaw(query, bindParams) {
|
|
761
|
+
const results = await this.forgeOperations.fetch().executeRawSQL(`EXPLAIN ${query}`, bindParams);
|
|
762
|
+
return results.map((row) => ({
|
|
763
|
+
id: row.id,
|
|
764
|
+
estRows: row.estRows,
|
|
765
|
+
actRows: row.actRows,
|
|
766
|
+
task: row.task,
|
|
767
|
+
accessObject: row["access object"],
|
|
768
|
+
executionInfo: row["execution info"],
|
|
769
|
+
operatorInfo: row["operator info"],
|
|
770
|
+
memory: row.memory,
|
|
771
|
+
disk: row.disk
|
|
772
|
+
}));
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Executes EXPLAIN on a Drizzle query.
|
|
776
|
+
* @param {{ toSQL: () => Query }} query - The Drizzle query to analyze
|
|
777
|
+
* @returns {Promise<ExplainAnalyzeRow[]>} The execution plan analysis results
|
|
778
|
+
*/
|
|
779
|
+
async explain(query) {
|
|
780
|
+
const { sql: sql2, params } = query.toSQL();
|
|
781
|
+
return this.explainRaw(sql2, params);
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Executes EXPLAIN ANALYZE on a raw SQL query.
|
|
785
|
+
* @param {string} query - The SQL query to analyze
|
|
786
|
+
* @param {unknown[]} bindParams - The query parameters
|
|
787
|
+
* @returns {Promise<ExplainAnalyzeRow[]>} The execution plan analysis results
|
|
788
|
+
*/
|
|
789
|
+
async explainAnalyzeRaw(query, bindParams) {
|
|
790
|
+
const results = await this.forgeOperations.fetch().executeRawSQL(`EXPLAIN ANALYZE ${query}`, bindParams);
|
|
791
|
+
return results.map((row) => ({
|
|
792
|
+
id: row.id,
|
|
793
|
+
estRows: row.estRows,
|
|
794
|
+
actRows: row.actRows,
|
|
795
|
+
task: row.task,
|
|
796
|
+
accessObject: row["access object"],
|
|
797
|
+
executionInfo: row["execution info"],
|
|
798
|
+
operatorInfo: row["operator info"],
|
|
799
|
+
memory: row.memory,
|
|
800
|
+
disk: row.disk
|
|
801
|
+
}));
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Executes EXPLAIN ANALYZE on a Drizzle query.
|
|
805
|
+
* @param {{ toSQL: () => Query }} query - The Drizzle query to analyze
|
|
806
|
+
* @returns {Promise<ExplainAnalyzeRow[]>} The execution plan analysis results
|
|
807
|
+
*/
|
|
808
|
+
async explainAnalyze(query) {
|
|
809
|
+
const { sql: sql2, params } = query.toSQL();
|
|
810
|
+
return this.explainAnalyzeRaw(sql2, params);
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Decodes a query execution plan from its string representation.
|
|
814
|
+
* @param {string} input - The raw execution plan string
|
|
815
|
+
* @returns {ExplainAnalyzeRow[]} The decoded execution plan rows
|
|
816
|
+
*/
|
|
817
|
+
decodedPlan(input) {
|
|
818
|
+
if (!input) {
|
|
819
|
+
return [];
|
|
820
|
+
}
|
|
821
|
+
const lines = input.trim().split("\n");
|
|
822
|
+
if (lines.length < 2) return [];
|
|
823
|
+
const headersRaw = lines[0].split(" ").map((h) => h.trim()).filter(Boolean);
|
|
824
|
+
const headers = headersRaw.map((h) => {
|
|
825
|
+
return h.replace(/\s+/g, " ").replace(/[-\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^./, (s) => s.toLowerCase());
|
|
826
|
+
});
|
|
827
|
+
return lines.slice(1).map((line) => {
|
|
828
|
+
const values = line.split(" ").map((s) => s.trim()).filter(Boolean);
|
|
829
|
+
const row = {};
|
|
830
|
+
headers.forEach((key, i) => {
|
|
831
|
+
row[key] = values[i] ?? "";
|
|
832
|
+
});
|
|
833
|
+
return row;
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Normalizes a raw slow query row into a more structured format.
|
|
838
|
+
* @param {SlowQueryRaw} row - The raw slow query data
|
|
839
|
+
* @returns {SlowQueryNormalized} The normalized slow query data
|
|
840
|
+
*/
|
|
841
|
+
normalizeSlowQuery(row) {
|
|
842
|
+
return {
|
|
843
|
+
time: row.Time,
|
|
844
|
+
txnStartTs: row.Txn_start_ts,
|
|
845
|
+
user: row.User,
|
|
846
|
+
host: row.Host,
|
|
847
|
+
connId: row.Conn_ID,
|
|
848
|
+
db: row.DB,
|
|
849
|
+
query: row.Query,
|
|
850
|
+
digest: row.Digest,
|
|
851
|
+
queryTime: row.Query_time,
|
|
852
|
+
compileTime: row.Compile_time,
|
|
853
|
+
optimizeTime: row.Optimize_time,
|
|
854
|
+
processTime: row.Process_time,
|
|
855
|
+
waitTime: row.Wait_time,
|
|
856
|
+
parseTime: row.Parse_time,
|
|
857
|
+
rewriteTime: row.Rewrite_time,
|
|
858
|
+
copTime: row.Cop_time,
|
|
859
|
+
copProcAvg: row.Cop_proc_avg,
|
|
860
|
+
copProcMax: row.Cop_proc_max,
|
|
861
|
+
copProcP90: row.Cop_proc_p90,
|
|
862
|
+
copProcAddr: row.Cop_proc_addr,
|
|
863
|
+
copWaitAvg: row.Cop_wait_avg,
|
|
864
|
+
copWaitMax: row.Cop_wait_max,
|
|
865
|
+
copWaitP90: row.Cop_wait_p90,
|
|
866
|
+
copWaitAddr: row.Cop_wait_addr,
|
|
867
|
+
memMax: row.Mem_max,
|
|
868
|
+
diskMax: row.Disk_max,
|
|
869
|
+
totalKeys: row.Total_keys,
|
|
870
|
+
processKeys: row.Process_keys,
|
|
871
|
+
requestCount: row.Request_count,
|
|
872
|
+
kvTotal: row.KV_total,
|
|
873
|
+
pdTotal: row.PD_total,
|
|
874
|
+
resultRows: row.Result_rows,
|
|
875
|
+
rocksdbBlockCacheHitCount: row.Rocksdb_block_cache_hit_count,
|
|
876
|
+
rocksdbBlockReadCount: row.Rocksdb_block_read_count,
|
|
877
|
+
rocksdbBlockReadByte: row.Rocksdb_block_read_byte,
|
|
878
|
+
plan: row.Plan,
|
|
879
|
+
binaryPlan: row.Binary_plan,
|
|
880
|
+
planDigest: row.Plan_digest,
|
|
881
|
+
parsedPlan: this.decodedPlan(row.Plan)
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Builds a SQL query for retrieving cluster statement history.
|
|
886
|
+
* @param {string[]} tables - The tables to analyze
|
|
887
|
+
* @param {Date} [from] - The start date for the analysis
|
|
888
|
+
* @param {Date} [to] - The end date for the analysis
|
|
889
|
+
* @returns {string} The SQL query for cluster statement history
|
|
890
|
+
*/
|
|
891
|
+
buildClusterStatementQuery(tables, from, to) {
|
|
892
|
+
const formatDateTime = (date) => moment(date).format("YYYY-MM-DDTHH:mm:ss.SSS");
|
|
893
|
+
const tableConditions = tables.map((table) => `TABLE_NAMES LIKE CONCAT(SCHEMA_NAME, '.', '%', '${table}', '%')`).join(" OR ");
|
|
894
|
+
const timeConditions = [];
|
|
895
|
+
if (from) {
|
|
896
|
+
timeConditions.push(`SUMMARY_BEGIN_TIME >= '${formatDateTime(from)}'`);
|
|
897
|
+
}
|
|
898
|
+
if (to) {
|
|
899
|
+
timeConditions.push(`SUMMARY_END_TIME <= '${formatDateTime(to)}'`);
|
|
900
|
+
}
|
|
901
|
+
let whereClauses;
|
|
902
|
+
if (tableConditions?.length) {
|
|
903
|
+
whereClauses = [tableConditions ? `(${tableConditions})` : "", ...timeConditions];
|
|
904
|
+
} else {
|
|
905
|
+
whereClauses = timeConditions;
|
|
906
|
+
}
|
|
907
|
+
return `
|
|
908
|
+
SELECT *
|
|
909
|
+
FROM (
|
|
910
|
+
SELECT * FROM INFORMATION_SCHEMA.CLUSTER_STATEMENTS_SUMMARY
|
|
911
|
+
UNION ALL
|
|
912
|
+
SELECT * FROM INFORMATION_SCHEMA.CLUSTER_STATEMENTS_SUMMARY_HISTORY
|
|
913
|
+
) AS combined
|
|
914
|
+
${whereClauses?.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : ""}
|
|
915
|
+
`;
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Retrieves and analyzes slow queries from the database.
|
|
919
|
+
* @returns {Promise<SlowQueryNormalized[]>} The normalized slow query data
|
|
920
|
+
*/
|
|
921
|
+
async analyzeSlowQueries() {
|
|
922
|
+
const results = await this.forgeOperations.fetch().executeRawSQL(`
|
|
923
|
+
SELECT *
|
|
924
|
+
FROM information_schema.slow_query
|
|
925
|
+
ORDER BY time DESC
|
|
926
|
+
`);
|
|
927
|
+
return results.map((row) => this.normalizeSlowQuery(row));
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Converts a cluster statement row to camelCase format.
|
|
931
|
+
* @param {Record<string, any>} input - The input row data
|
|
932
|
+
* @returns {ClusterStatementRowCamelCase} The converted row data
|
|
933
|
+
*/
|
|
934
|
+
mapToCamelCaseClusterStatement(input) {
|
|
935
|
+
if (!input) {
|
|
936
|
+
return {};
|
|
937
|
+
}
|
|
938
|
+
const result = {};
|
|
939
|
+
result.parsedPlan = this.decodedPlan(input["PLAN"] ?? "");
|
|
940
|
+
for (const key in input) {
|
|
941
|
+
const camelKey = key.toLowerCase().replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
942
|
+
result[camelKey] = input[key];
|
|
943
|
+
}
|
|
944
|
+
return result;
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Analyzes query history for specific tables using raw table names.
|
|
948
|
+
* @param {string[]} tables - The table names to analyze
|
|
949
|
+
* @param {Date} [fromDate] - The start date for the analysis
|
|
950
|
+
* @param {Date} [toDate] - The end date for the analysis
|
|
951
|
+
* @returns {Promise<ClusterStatementRowCamelCase[]>} The analyzed query history
|
|
952
|
+
*/
|
|
953
|
+
async analyzeQueriesHistoryRaw(tables, fromDate, toDate) {
|
|
954
|
+
const results = await this.forgeOperations.fetch().executeRawSQL(
|
|
955
|
+
this.buildClusterStatementQuery(tables ?? [], fromDate, toDate)
|
|
956
|
+
);
|
|
957
|
+
return results.map((r) => this.mapToCamelCaseClusterStatement(r));
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Analyzes query history for specific tables using Drizzle table objects.
|
|
961
|
+
* @param {AnyMySqlTable[]} tables - The Drizzle table objects to analyze
|
|
962
|
+
* @param {Date} [fromDate] - The start date for the analysis
|
|
963
|
+
* @param {Date} [toDate] - The end date for the analysis
|
|
964
|
+
* @returns {Promise<ClusterStatementRowCamelCase[]>} The analyzed query history
|
|
965
|
+
*/
|
|
966
|
+
async analyzeQueriesHistory(tables, fromDate, toDate) {
|
|
967
|
+
const tableNames = tables?.map((table) => getTableName(table)) ?? [];
|
|
968
|
+
return this.analyzeQueriesHistoryRaw(tableNames, fromDate, toDate);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
692
971
|
class ForgeSQLORMImpl {
|
|
693
972
|
static instance = null;
|
|
694
973
|
drizzle;
|
|
695
974
|
crudOperations;
|
|
696
975
|
fetchOperations;
|
|
976
|
+
analyzeOperations;
|
|
697
977
|
/**
|
|
698
978
|
* Private constructor to enforce singleton behavior.
|
|
699
979
|
* @param options - Options for configuring ForgeSQL ORM behavior.
|
|
@@ -707,25 +987,32 @@ class ForgeSQLORMImpl {
|
|
|
707
987
|
if (newOptions.logRawSqlQuery) {
|
|
708
988
|
console.debug("Initializing ForgeSQLORM...");
|
|
709
989
|
}
|
|
990
|
+
const proxiedDriver = createForgeDriverProxy(newOptions.hints, newOptions.logRawSqlQuery);
|
|
710
991
|
this.drizzle = patchDbWithSelectAliased(
|
|
711
|
-
drizzle(
|
|
992
|
+
drizzle(proxiedDriver, { logger: newOptions.logRawSqlQuery })
|
|
712
993
|
);
|
|
713
994
|
this.crudOperations = new ForgeSQLCrudOperations(this, newOptions);
|
|
714
995
|
this.fetchOperations = new ForgeSQLSelectOperations(newOptions);
|
|
996
|
+
this.analyzeOperations = new ForgeSQLAnalyseOperation(this);
|
|
715
997
|
} catch (error) {
|
|
716
998
|
console.error("ForgeSQLORM initialization failed:", error);
|
|
717
999
|
throw error;
|
|
718
1000
|
}
|
|
719
1001
|
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Create the modify operations instance.
|
|
1004
|
+
* @returns modify operations.
|
|
1005
|
+
*/
|
|
1006
|
+
modify() {
|
|
1007
|
+
return this.crudOperations;
|
|
1008
|
+
}
|
|
720
1009
|
/**
|
|
721
1010
|
* Returns the singleton instance of ForgeSQLORMImpl.
|
|
722
1011
|
* @param options - Options for configuring ForgeSQL ORM behavior.
|
|
723
1012
|
* @returns The singleton instance of ForgeSQLORMImpl.
|
|
724
1013
|
*/
|
|
725
1014
|
static getInstance(options) {
|
|
726
|
-
|
|
727
|
-
ForgeSQLORMImpl.instance = new ForgeSQLORMImpl(options);
|
|
728
|
-
}
|
|
1015
|
+
ForgeSQLORMImpl.instance ??= new ForgeSQLORMImpl(options);
|
|
729
1016
|
return ForgeSQLORMImpl.instance;
|
|
730
1017
|
}
|
|
731
1018
|
/**
|
|
@@ -733,7 +1020,7 @@ class ForgeSQLORMImpl {
|
|
|
733
1020
|
* @returns CRUD operations.
|
|
734
1021
|
*/
|
|
735
1022
|
crud() {
|
|
736
|
-
return this.
|
|
1023
|
+
return this.modify();
|
|
737
1024
|
}
|
|
738
1025
|
/**
|
|
739
1026
|
* Retrieves the fetch operations instance.
|
|
@@ -742,6 +1029,9 @@ class ForgeSQLORMImpl {
|
|
|
742
1029
|
fetch() {
|
|
743
1030
|
return this.fetchOperations;
|
|
744
1031
|
}
|
|
1032
|
+
analyze() {
|
|
1033
|
+
return this.analyzeOperations;
|
|
1034
|
+
}
|
|
745
1035
|
/**
|
|
746
1036
|
* Returns a Drizzle query builder instance.
|
|
747
1037
|
*
|
|
@@ -829,7 +1119,7 @@ class ForgeSQLORM {
|
|
|
829
1119
|
*
|
|
830
1120
|
* @template TSelection - The type of the selected fields
|
|
831
1121
|
* @param {TSelection} fields - Object containing the fields to select, with table schemas as values
|
|
832
|
-
* @returns {MySqlSelectBuilder<TSelection,
|
|
1122
|
+
* @returns {MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>} A distinct select query builder with unique field aliases
|
|
833
1123
|
* @throws {Error} If fields parameter is empty
|
|
834
1124
|
* @example
|
|
835
1125
|
* ```typescript
|
|
@@ -847,7 +1137,14 @@ class ForgeSQLORM {
|
|
|
847
1137
|
* @returns CRUD operations.
|
|
848
1138
|
*/
|
|
849
1139
|
crud() {
|
|
850
|
-
return this.ormInstance.
|
|
1140
|
+
return this.ormInstance.modify();
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Proxies the `modify` method from `ForgeSQLORMImpl`.
|
|
1144
|
+
* @returns Modify operations.
|
|
1145
|
+
*/
|
|
1146
|
+
modify() {
|
|
1147
|
+
return this.ormInstance.modify();
|
|
851
1148
|
}
|
|
852
1149
|
/**
|
|
853
1150
|
* Proxies the `fetch` method from `ForgeSQLORMImpl`.
|
|
@@ -856,6 +1153,13 @@ class ForgeSQLORM {
|
|
|
856
1153
|
fetch() {
|
|
857
1154
|
return this.ormInstance.fetch();
|
|
858
1155
|
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Provides query analysis capabilities including EXPLAIN ANALYZE and slow query analysis.
|
|
1158
|
+
* @returns {SchemaAnalyzeForgeSql} Interface for analyzing query performance
|
|
1159
|
+
*/
|
|
1160
|
+
analyze() {
|
|
1161
|
+
return this.ormInstance.analyze();
|
|
1162
|
+
}
|
|
859
1163
|
/**
|
|
860
1164
|
* Returns a Drizzle query builder instance.
|
|
861
1165
|
*
|
|
@@ -886,7 +1190,7 @@ const forgeTimestampString = customType({
|
|
|
886
1190
|
return "timestamp";
|
|
887
1191
|
},
|
|
888
1192
|
toDriver(value) {
|
|
889
|
-
return moment$1(value).format("YYYY-MM-DDTHH:mm:ss.SSS");
|
|
1193
|
+
return moment$1(new Date(value)).format("YYYY-MM-DDTHH:mm:ss.SSS");
|
|
890
1194
|
},
|
|
891
1195
|
fromDriver(value) {
|
|
892
1196
|
const format = "YYYY-MM-DDTHH:mm:ss.SSS";
|
|
@@ -916,8 +1220,19 @@ const forgeTimeString = customType({
|
|
|
916
1220
|
return parseDateTime(value, "HH:mm:ss.SSS");
|
|
917
1221
|
}
|
|
918
1222
|
});
|
|
919
|
-
|
|
1223
|
+
const migrations = mysqlTable("__migrations", {
|
|
1224
|
+
id: bigint("id", { mode: "number" }).primaryKey().autoincrement(),
|
|
1225
|
+
name: varchar("name", { length: 255 }).notNull(),
|
|
1226
|
+
migratedAt: timestamp("migratedAt").defaultNow().notNull()
|
|
1227
|
+
});
|
|
1228
|
+
async function getTables() {
|
|
1229
|
+
const tables = await sql$1.executeDDL("SHOW TABLES");
|
|
1230
|
+
return tables.rows.flatMap((tableInfo) => Object.values(tableInfo));
|
|
1231
|
+
}
|
|
1232
|
+
const forgeSystemTables = [migrations];
|
|
1233
|
+
async function dropSchemaMigrations() {
|
|
920
1234
|
try {
|
|
1235
|
+
const tables = await getTables();
|
|
921
1236
|
const dropStatements = generateDropTableStatements(tables);
|
|
922
1237
|
for (const statement of dropStatements) {
|
|
923
1238
|
console.warn(statement);
|
|
@@ -928,32 +1243,41 @@ async function dropSchemaMigrations(tables) {
|
|
|
928
1243
|
"⚠️ All data in these tables has been permanently deleted. This operation cannot be undone."
|
|
929
1244
|
);
|
|
930
1245
|
} catch (error) {
|
|
1246
|
+
console.error(error);
|
|
931
1247
|
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
932
1248
|
return getHttpResponse(500, errorMessage);
|
|
933
1249
|
}
|
|
934
1250
|
}
|
|
935
1251
|
const applySchemaMigrations = async (migration) => {
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1252
|
+
try {
|
|
1253
|
+
if (typeof migration !== "function") {
|
|
1254
|
+
throw new Error("migration is not a function");
|
|
1255
|
+
}
|
|
1256
|
+
console.log("Provisioning the database");
|
|
1257
|
+
await sql$1._provision();
|
|
1258
|
+
console.info("Running schema migrations");
|
|
1259
|
+
const migrations2 = await migration(migrationRunner);
|
|
1260
|
+
const successfulMigrations = await migrations2.run();
|
|
1261
|
+
console.info("Migrations applied:", successfulMigrations);
|
|
1262
|
+
const migrationList = await migrationRunner.list();
|
|
1263
|
+
const migrationHistory = Array.isArray(migrationList) && migrationList.length > 0 ? migrationList.map((y) => `${y.id}, ${y.name}, ${y.migratedAt.toUTCString()}`).join("\n") : "No migrations found";
|
|
1264
|
+
console.info("Migrations history:\nid, name, migrated_at\n", migrationHistory);
|
|
1265
|
+
return {
|
|
1266
|
+
headers: { "Content-Type": ["application/json"] },
|
|
1267
|
+
statusCode: 200,
|
|
1268
|
+
statusText: "OK",
|
|
1269
|
+
body: "Migrations successfully executed"
|
|
1270
|
+
};
|
|
1271
|
+
} catch (error) {
|
|
1272
|
+
console.error("Error during migration:", error);
|
|
1273
|
+
return {
|
|
1274
|
+
headers: { "Content-Type": ["application/json"] },
|
|
1275
|
+
statusCode: 500,
|
|
1276
|
+
statusText: "Internal Server Error",
|
|
1277
|
+
body: error instanceof Error ? error.message : "Unknown error during migration"
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
950
1280
|
};
|
|
951
|
-
const migrations = mysqlTable("__migrations", {
|
|
952
|
-
id: bigint("id", { mode: "number" }).primaryKey().autoincrement(),
|
|
953
|
-
name: varchar("name", { length: 255 }).notNull(),
|
|
954
|
-
migratedAt: timestamp("migratedAt").defaultNow().notNull()
|
|
955
|
-
});
|
|
956
|
-
const forgeSystemTables = [migrations];
|
|
957
1281
|
async function fetchSchemaWebTrigger() {
|
|
958
1282
|
try {
|
|
959
1283
|
const tables = await getTables();
|
|
@@ -966,14 +1290,10 @@ async function fetchSchemaWebTrigger() {
|
|
|
966
1290
|
return getHttpResponse(500, errorMessage);
|
|
967
1291
|
}
|
|
968
1292
|
}
|
|
969
|
-
async function getTables() {
|
|
970
|
-
const tables = await sql$1.executeDDL("SHOW TABLES");
|
|
971
|
-
return tables.rows.flatMap((tableInfo) => Object.values(tableInfo));
|
|
972
|
-
}
|
|
973
1293
|
async function generateCreateTableStatements(tables) {
|
|
974
1294
|
const statements = [];
|
|
975
1295
|
for (const table of tables) {
|
|
976
|
-
const createTableResult = await sql$1.executeDDL(`SHOW CREATE TABLE ${table}`);
|
|
1296
|
+
const createTableResult = await sql$1.executeDDL(`SHOW CREATE TABLE "${table}"`);
|
|
977
1297
|
const createTableStatements = createTableResult.rows.filter((row) => !isSystemTable(row.Table)).map((row) => formatCreateTableStatement(row["Create Table"]));
|
|
978
1298
|
statements.push(...createTableStatements);
|
|
979
1299
|
}
|
|
@@ -1009,19 +1329,21 @@ export {
|
|
|
1009
1329
|
applySchemaMigrations,
|
|
1010
1330
|
ForgeSQLORM as default,
|
|
1011
1331
|
dropSchemaMigrations,
|
|
1012
|
-
extractAlias,
|
|
1013
1332
|
fetchSchemaWebTrigger,
|
|
1014
1333
|
forgeDateString,
|
|
1015
1334
|
forgeDateTimeString,
|
|
1016
1335
|
forgeDriver,
|
|
1336
|
+
forgeSystemTables,
|
|
1017
1337
|
forgeTimeString,
|
|
1018
1338
|
forgeTimestampString,
|
|
1019
1339
|
generateDropTableStatements,
|
|
1020
1340
|
getHttpResponse,
|
|
1021
1341
|
getPrimaryKeys,
|
|
1022
1342
|
getTableMetadata,
|
|
1343
|
+
getTables,
|
|
1023
1344
|
mapSelectAllFieldsToAlias,
|
|
1024
1345
|
mapSelectFieldsWithAlias,
|
|
1346
|
+
migrations,
|
|
1025
1347
|
parseDateTime,
|
|
1026
1348
|
patchDbWithSelectAliased
|
|
1027
1349
|
};
|