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 {