englefly commented on code in PR #69322:
URL: https://github.com/apache/doris/pull/69322#discussion_r4347226758


##########
fe/fe-core/src/main/java/org/apache/doris/nereids/stats/HboStatisticsStore.java:
##########
@@ -0,0 +1,295 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.nereids.stats;
+
+import org.apache.doris.catalog.Database;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.catalog.TableIf;
+import org.apache.doris.common.Config;
+import org.apache.doris.common.FeConstants;
+import org.apache.doris.datasource.InternalCatalog;
+import org.apache.doris.statistics.repository.ResultRow;
+import org.apache.doris.statistics.util.StatisticsUtil;
+
+import com.google.common.collect.ImmutableList;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Persistence of manually injected (pinned) hbo statistics into the internal 
database
+ * ({@code __internal_schema.hbo_statistics}), controlled by
+ * {@code Config.hbo_persist_pinned_to_internal_db}.
+ *
+ * <p>The in-memory {@link HboPlanStatisticsManager} stays authoritative on 
the read path;
+ * SET/DELETE are written through synchronously (best effort, failures only 
logged) and a FE
+ * loads the whole table into memory lazily on first use, so pinned entries 
survive a FE
+ * restart. All FEs share the table through the backend storage.
+ */
+public class HboStatisticsStore {
+    private static final Logger LOG = 
LogManager.getLogger(HboStatisticsStore.class);
+
+    private static final String INTERNAL_DB = FeConstants.INTERNAL_DB_NAME;
+    private static final String TABLE = "hbo_statistics";
+    private static final String FULL_QUALIFIED =
+            InternalCatalog.INTERNAL_CATALOG_NAME + "." + INTERNAL_DB + "." + 
TABLE;
+    /** max length (bytes) of the struct_info varchar column */
+    private static final int STRUCT_MAX_BYTES = 65533;
+
+    /** creation time of an entry, as a datetime(3) column like the other 
internal tables */
+    private static final String CREATE_TIME_COLUMN = "create_time";
+    /** fan-out factor of a PinnedType.JOIN_EXPANSION entry, NULL for a row 
count entry */
+    private static final String EXPANSION_COLUMN = "expansion";
+    /** which struct info the entry is keyed by (with_literal / no_literal) */
+    private static final String LITERAL_MODE_COLUMN = "literal_mode";
+    /** the columns this version writes and reads, checked once per FE (see 
verifySchema) */
+    private static final List<String> COLUMNS = 
ImmutableList.of("fingerprint", "row_count", "stats_type",
+            LITERAL_MODE_COLUMN, "struct_info", EXPANSION_COLUMN, 
CREATE_TIME_COLUMN);
+    // the DDL and the schema check are done once per FE: the table can only 
change by a FE upgrade,
+    // and both cost a round trip to the internal table (a forwarded DDL on a 
follower FE)
+    private static volatile boolean tableCreated = false;
+    private static volatile boolean schemaVerified = false;
+
+    private HboStatisticsStore() {
+    }
+
+    /**
+     * Whether a canonical struct info fits the {@code struct_info} column 
without truncation.
+     * A truncated struct info would keep its fingerprint but lose its scan 
tokens (the baseline is
+     * printed after the conditions), which silently disables both the 
freshness verdict and the
+     * relation pre filter for that entry, so such an entry must not be 
accepted at all.
+     */
+    public static boolean fitsStructColumn(String structCanonical) {
+        return structCanonical == null
+                || 
structCanonical.getBytes(java.nio.charset.StandardCharsets.UTF_8).length <= 
STRUCT_MAX_BYTES;
+    }
+
+    /** Create the table if it does not exist, and check that its schema is 
the current one. */
+    public static void ensureTable() throws Exception {
+        if (!tableCreated) {
+            StatisticsUtil.execUpdate(createDdl());
+            tableCreated = true;
+        }
+        if (!schemaVerified) {
+            verifySchema();
+        }
+    }
+
+    /**
+     * Check that the table has every column this version reads and writes, by 
looking at its metadata
+     * in the FE catalog. A query would be cheaper to write but wrong to 
interpret: it fails both when
+     * the table has an old schema and when the internal schema is simply not 
ready yet, and the two
+     * need opposite reactions (drop the table vs wait).
+     *
+     * <p>A table created by an older FE version (e.g. with a {@code 
create_time_ms} column) cannot be
+     * written by this version, and because it is created with {@code CREATE 
TABLE IF NOT EXISTS} it
+     * stays broken; the check therefore logs the fix and forgets that the 
table was "created", so
+     * that the DDL runs again - and recreates the table - after the operator 
dropped it. Without
+     * that, persistence would stay dead until the FE restarts.
+     */
+    private static void verifySchema() throws Exception {
+        Database db = 
Env.getCurrentEnv().getInternalCatalog().getDbNullable(INTERNAL_DB);
+        TableIf table = db == null ? null : db.getTableNullable(TABLE);
+        if (table == null) {
+            // the DDL succeeded but the table is not visible yet (a follower 
waiting for the journal,
+            // or an internal schema which is not ready): retry later, do not 
blame the table
+            throw new IllegalStateException("the internal table " + 
FULL_QUALIFIED + " is not visible yet");
+        }
+        List<String> missing = new ArrayList<>();
+        for (String column : COLUMNS) {
+            if (table.getColumn(column) == null) {
+                missing.add(column);
+            }
+        }
+        if (!missing.isEmpty()) {
+            tableCreated = false;
+            LOG.warn("the internal table {} has no {} column: it was created 
by an older FE version and"
+                    + " has to be dropped so that it is recreated (DROP TABLE 
{})",
+                    FULL_QUALIFIED, missing, FULL_QUALIFIED);
+            throw new IllegalStateException("the internal table " + 
FULL_QUALIFIED + " has an old schema");
+        }
+        schemaVerified = true;
+    }
+
+    /**
+     * Upsert one pinned entry (UNIQUE KEY fingerprint replaces on conflict).
+     */
+    public static void persist(String fingerprint, long rows,
+            HboPlanStatisticsManager.PinnedType type, String structCanonical, 
double expansion,
+            HboPlanStatisticsManager.LiteralMode literalMode, long 
createTimeMs) {
+        try {
+            ensureTable();
+            HboPlanStatisticsManager.PinnedType statsType = type == null
+                    ? HboPlanStatisticsManager.PinnedType.EXACT : type;
+            // an empty struct_info records no data state: the freshness 
comparison reports unknown for such
+            // an entry and the pinned lookup index has no relation key for 
it, which is why the pinned scope
+            // of HBO SET requires the canonical string (see 
HboStatisticsCommand)
+            String struct = truncateUtf8(structCanonical == null ? "" : 
structCanonical, STRUCT_MAX_BYTES);
+            // the fingerprint kind is only known after the entry was applied 
once, so the stored
+            // value stays UNKNOWN (it is reported by HBO SHOW from the 
in-memory entry)
+            String sql = "INSERT INTO " + FULL_QUALIFIED
+                    + " (`fingerprint`, `row_count`, `stats_type`, `" + 
LITERAL_MODE_COLUMN + "`, `struct_info`,"
+                    + " `" + EXPANSION_COLUMN + "`, `" + CREATE_TIME_COLUMN + 
"`) VALUES ('"
+                    + StatisticsUtil.escapeSQL(fingerprint) + "', " + rows + 
", '"
+                    + 
StatisticsUtil.escapeSQL(statsType.name().toLowerCase(java.util.Locale.ROOT)) + 
"', '"
+                    + StatisticsUtil.escapeSQL((literalMode == null
+                            ? HboPlanStatisticsManager.LiteralMode.NO_LITERAL 
: literalMode)
+                            .name().toLowerCase(java.util.Locale.ROOT))
+                    + "', '" + StatisticsUtil.escapeSQL(struct) + "', "
+                    + (expansion > 0 ? String.valueOf(expansion) : "NULL")
+                    + ", '" + createTimeLiteral(createTimeMs) + "')";
+            StatisticsUtil.execUpdate(sql);
+        } catch (Exception t) {
+            LOG.warn("failed to persist hbo pinned statistics for fingerprint 
{}", fingerprint, t);
+        }
+    }
+
+    /**
+     * Remove the pinned entry a removal judged: the row is deleted only while 
its create time is not
+     * newer than {@code generation}, so a row another FE wrote after the 
judge was read survives.
+     *
+     * @return true when the row was removed (or the table had no such row), 
false when the
+     *         best-effort removal failed (e.g. internal schema not ready yet)
+     */
+    public static boolean delete(String fingerprint, long generation) {
+        try {
+            ensureTable();
+            // only the row the removal judged is deleted: a row another FE 
wrote afterwards is a newer
+            // SET and must survive, because this FE cannot order its removal 
against that write
+            String sql = "DELETE FROM " + FULL_QUALIFIED + " WHERE 
`fingerprint` = '"
+                    + StatisticsUtil.escapeSQL(fingerprint) + "' AND `" + 
CREATE_TIME_COLUMN
+                    + "` <= '" + createTimeLiteral(generation) + "'";
+            StatisticsUtil.execUpdate(sql);

Review Comment:
   修改del的顺序, 先删除table中的行, 成功再删除cache中的行. 去掉tombstoneCovers 机制



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to