From 4492a9c47e53fafc302b8506547deff2480a1c9c Mon Sep 17 00:00:00 2001 From: XuJiacheng Date: Tue, 3 Mar 2026 21:00:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=BC=BA=E5=88=B6?= =?UTF-8?q?=E7=83=AD=E8=A1=A8=E7=A9=BA=E9=97=B4=E7=9A=84=20SQL=20=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BC=98=E5=8C=96=E5=88=86?= =?UTF-8?q?=E5=8C=BA=E5=88=9B=E5=BB=BA=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bls-onoffline-backend/dist/index.js | 77 +++++++++++++++++- .../src/db/partitionManager.js | 80 ++++++++++++++++++- 2 files changed, 153 insertions(+), 4 deletions(-) diff --git a/bls-onoffline-backend/dist/index.js b/bls-onoffline-backend/dist/index.js index c43a110..43d3505 100644 --- a/bls-onoffline-backend/dist/index.js +++ b/bls-onoffline-backend/dist/index.js @@ -181,6 +181,69 @@ class PartitionManager { today.setHours(0, 0, 0, 0); return normalizedDate.getTime() >= today.getTime(); } + escapeSqlLiteral(value) { + return String(value).replace(/'/g, "''"); + } + buildForceHotTablespaceSql(schema, partitionName, hotTablespace = "ts_hot") { + const schemaLiteral = this.escapeSqlLiteral(schema); + const partitionLiteral = this.escapeSqlLiteral(partitionName); + const hotLiteral = this.escapeSqlLiteral(hotTablespace); + return ` +DO $$ +DECLARE + v_schema text := '${schemaLiteral}'; + v_partition text := '${partitionLiteral}'; + v_hot text := '${hotLiteral}'; + v_part_oid oid; + v_toast_oid oid; + r record; +BEGIN + SELECT c.oid INTO v_part_oid + FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace + WHERE n.nspname=v_schema AND c.relname=v_partition AND c.relkind='r'; + + IF v_part_oid IS NULL THEN + RAISE EXCEPTION 'partition %.% not found', v_schema, v_partition; + END IF; + + EXECUTE format('ALTER TABLE %I.%I SET TABLESPACE %I', v_schema, v_partition, v_hot); + + FOR r IN + SELECT idxn.nspname AS index_schema, i.relname AS index_name + FROM pg_index x + JOIN pg_class t ON t.oid=x.indrelid + JOIN pg_namespace nt ON nt.oid=t.relnamespace + JOIN pg_class i ON i.oid=x.indexrelid + JOIN pg_namespace idxn ON idxn.oid=i.relnamespace + LEFT JOIN pg_tablespace ts ON ts.oid=i.reltablespace + WHERE nt.nspname=v_schema + AND t.relname=v_partition + AND COALESCE(ts.spcname,'pg_default')<>v_hot + LOOP + EXECUTE format('ALTER INDEX %I.%I SET TABLESPACE %I', r.index_schema, r.index_name, v_hot); + END LOOP; + + SELECT reltoastrelid INTO v_toast_oid FROM pg_class WHERE oid=v_part_oid; + IF v_toast_oid IS NOT NULL AND v_toast_oid<>0 THEN + EXECUTE format('ALTER TABLE %s SET TABLESPACE %I', v_toast_oid::regclass, v_hot); + + FOR r IN + SELECT idxn.nspname AS index_schema, i.relname AS index_name + FROM pg_index x + JOIN pg_class i ON i.oid=x.indexrelid + JOIN pg_namespace idxn ON idxn.oid=i.relnamespace + LEFT JOIN pg_tablespace ts ON ts.oid=i.reltablespace + WHERE x.indrelid=v_toast_oid + AND COALESCE(ts.spcname,'pg_default')<>v_hot + LOOP + EXECUTE format('ALTER INDEX %I.%I SET TABLESPACE %I', r.index_schema, r.index_name, v_hot); + END LOOP; + END IF; + + EXECUTE format('ANALYZE %I.%I', v_schema, v_partition); +END $$; +`; + } /** * Calculate the start and end timestamps (milliseconds) for a given date. * @param {Date} date - The date to calculate for. @@ -225,13 +288,18 @@ class PartitionManager { if (!checkRes.rows[0].exists) { logger.info(`Creating partition ${partitionName} for range [${startMs}, ${endMs})`); console.log(`Creating partition ${partitionName} for range [${startMs}, ${endMs})`); - const tablespaceClause = this.isCurrentOrFutureDate(targetDate) ? " TABLESPACE ts_hot" : ""; + const shouldUseHotTablespace = this.isCurrentOrFutureDate(targetDate); + const tablespaceClause = shouldUseHotTablespace ? " TABLESPACE ts_hot" : ""; + const partitionTableName = `${table}_${partitionSuffix}`; const createSql = ` CREATE TABLE IF NOT EXISTS ${partitionName} PARTITION OF ${schema}.${table} FOR VALUES FROM (${startMs}) TO (${endMs})${tablespaceClause}; `; await client.query(createSql); + if (shouldUseHotTablespace) { + await client.query(this.buildForceHotTablespaceSql(schema, partitionTableName)); + } } } logger.info("Partition check completed."); @@ -271,12 +339,17 @@ class PartitionManager { const checkRes = await client.query(`SELECT to_regclass($1) as exists;`, [partitionName]); if (!checkRes.rows[0].exists) { logger.info(`Creating partition ${partitionName} for range [${startMs}, ${endMs})`); - const tablespaceClause = this.isCurrentOrFutureDate(targetDate) ? " TABLESPACE ts_hot" : ""; + const shouldUseHotTablespace = this.isCurrentOrFutureDate(targetDate); + const tablespaceClause = shouldUseHotTablespace ? " TABLESPACE ts_hot" : ""; + const partitionTableName = `${table}_${partitionSuffix}`; await client.query(` CREATE TABLE IF NOT EXISTS ${partitionName} PARTITION OF ${schema}.${table} FOR VALUES FROM (${startMs}) TO (${endMs})${tablespaceClause}; `); + if (shouldUseHotTablespace) { + await client.query(this.buildForceHotTablespaceSql(schema, partitionTableName)); + } } } } finally { diff --git a/bls-onoffline-backend/src/db/partitionManager.js b/bls-onoffline-backend/src/db/partitionManager.js index 7a3651e..83bd94f 100644 --- a/bls-onoffline-backend/src/db/partitionManager.js +++ b/bls-onoffline-backend/src/db/partitionManager.js @@ -13,6 +13,72 @@ class PartitionManager { return normalizedDate.getTime() >= today.getTime(); } + escapeSqlLiteral(value) { + return String(value).replace(/'/g, "''"); + } + + buildForceHotTablespaceSql(schema, partitionName, hotTablespace = 'ts_hot') { + const schemaLiteral = this.escapeSqlLiteral(schema); + const partitionLiteral = this.escapeSqlLiteral(partitionName); + const hotLiteral = this.escapeSqlLiteral(hotTablespace); + + return ` +DO $$ +DECLARE + v_schema text := '${schemaLiteral}'; + v_partition text := '${partitionLiteral}'; + v_hot text := '${hotLiteral}'; + v_part_oid oid; + v_toast_oid oid; + r record; +BEGIN + SELECT c.oid INTO v_part_oid + FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace + WHERE n.nspname=v_schema AND c.relname=v_partition AND c.relkind='r'; + + IF v_part_oid IS NULL THEN + RAISE EXCEPTION 'partition %.% not found', v_schema, v_partition; + END IF; + + EXECUTE format('ALTER TABLE %I.%I SET TABLESPACE %I', v_schema, v_partition, v_hot); + + FOR r IN + SELECT idxn.nspname AS index_schema, i.relname AS index_name + FROM pg_index x + JOIN pg_class t ON t.oid=x.indrelid + JOIN pg_namespace nt ON nt.oid=t.relnamespace + JOIN pg_class i ON i.oid=x.indexrelid + JOIN pg_namespace idxn ON idxn.oid=i.relnamespace + LEFT JOIN pg_tablespace ts ON ts.oid=i.reltablespace + WHERE nt.nspname=v_schema + AND t.relname=v_partition + AND COALESCE(ts.spcname,'pg_default')<>v_hot + LOOP + EXECUTE format('ALTER INDEX %I.%I SET TABLESPACE %I', r.index_schema, r.index_name, v_hot); + END LOOP; + + SELECT reltoastrelid INTO v_toast_oid FROM pg_class WHERE oid=v_part_oid; + IF v_toast_oid IS NOT NULL AND v_toast_oid<>0 THEN + EXECUTE format('ALTER TABLE %s SET TABLESPACE %I', v_toast_oid::regclass, v_hot); + + FOR r IN + SELECT idxn.nspname AS index_schema, i.relname AS index_name + FROM pg_index x + JOIN pg_class i ON i.oid=x.indexrelid + JOIN pg_namespace idxn ON idxn.oid=i.relnamespace + LEFT JOIN pg_tablespace ts ON ts.oid=i.reltablespace + WHERE x.indrelid=v_toast_oid + AND COALESCE(ts.spcname,'pg_default')<>v_hot + LOOP + EXECUTE format('ALTER INDEX %I.%I SET TABLESPACE %I', r.index_schema, r.index_name, v_hot); + END LOOP; + END IF; + + EXECUTE format('ANALYZE %I.%I', v_schema, v_partition); +END $$; +`; + } + /** * Calculate the start and end timestamps (milliseconds) for a given date. * @param {Date} date - The date to calculate for. @@ -66,13 +132,18 @@ class PartitionManager { if (!checkRes.rows[0].exists) { logger.info(`Creating partition ${partitionName} for range [${startMs}, ${endMs})`); console.log(`Creating partition ${partitionName} for range [${startMs}, ${endMs})`); - const tablespaceClause = this.isCurrentOrFutureDate(targetDate) ? ' TABLESPACE ts_hot' : ''; + const shouldUseHotTablespace = this.isCurrentOrFutureDate(targetDate); + const tablespaceClause = shouldUseHotTablespace ? ' TABLESPACE ts_hot' : ''; + const partitionTableName = `${table}_${partitionSuffix}`; const createSql = ` CREATE TABLE IF NOT EXISTS ${partitionName} PARTITION OF ${schema}.${table} FOR VALUES FROM (${startMs}) TO (${endMs})${tablespaceClause}; `; await client.query(createSql); + if (shouldUseHotTablespace) { + await client.query(this.buildForceHotTablespaceSql(schema, partitionTableName)); + } } } logger.info('Partition check completed.'); @@ -119,12 +190,17 @@ class PartitionManager { const checkRes = await client.query(`SELECT to_regclass($1) as exists;`, [partitionName]); if (!checkRes.rows[0].exists) { logger.info(`Creating partition ${partitionName} for range [${startMs}, ${endMs})`); - const tablespaceClause = this.isCurrentOrFutureDate(targetDate) ? ' TABLESPACE ts_hot' : ''; + const shouldUseHotTablespace = this.isCurrentOrFutureDate(targetDate); + const tablespaceClause = shouldUseHotTablespace ? ' TABLESPACE ts_hot' : ''; + const partitionTableName = `${table}_${partitionSuffix}`; await client.query(` CREATE TABLE IF NOT EXISTS ${partitionName} PARTITION OF ${schema}.${table} FOR VALUES FROM (${startMs}) TO (${endMs})${tablespaceClause}; `); + if (shouldUseHotTablespace) { + await client.query(this.buildForceHotTablespaceSql(schema, partitionTableName)); + } } } } finally {