diff --git a/pixels-cli/src/main/java/io/pixelsdb/pixels/cli/Main.java b/pixels-cli/src/main/java/io/pixelsdb/pixels/cli/Main.java
index e138325037..376b82a862 100644
--- a/pixels-cli/src/main/java/io/pixelsdb/pixels/cli/Main.java
+++ b/pixels-cli/src/main/java/io/pixelsdb/pixels/cli/Main.java
@@ -72,6 +72,9 @@
*
* STAT -s tpch -t region
*
+ *
+ * INIT-META
+ *
*/
public class Main
{
@@ -140,7 +143,8 @@ public static void main(String[] args)
"STAT\n" +
"QUERY\n" +
"COPY\n" +
- "FILE_META");
+ "FILE_META\n" +
+ "INIT-META");
System.out.println("{command} -h to show the usage of a command.\nexit / quit / -q to exit.\n");
continue;
}
@@ -366,6 +370,33 @@ public static void main(String[] args)
}
}
+ if (command.equals("INIT-META"))
+ {
+ ArgumentParser argumentParser = ArgumentParsers.newArgumentParser("Pixels INIT-META")
+ .defaultHelp(true);
+
+ Namespace ns;
+ try
+ {
+ String argsLine = inputStr.substring(command.length()).trim();
+ ns = argumentParser.parseArgs(argsLine.isEmpty() ? new String[0] : argsLine.split("\\s+"));
+ } catch (ArgumentParserException e)
+ {
+ argumentParser.handleError(e);
+ continue;
+ }
+
+ try
+ {
+ InitMetaExecutor initMetaExecutor = new InitMetaExecutor();
+ initMetaExecutor.execute(ns, command);
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+
if (command.equals("FILE_META"))
{
ArgumentParser argumentParser = ArgumentParsers.newArgumentParser("Pixels File Metadata Explorer")
@@ -401,7 +432,8 @@ public static void main(String[] args)
!command.equals("COMPACT") &&
!command.equals("STAT") &&
!command.equals("IMPORT") &&
- !command.equals("FILE_META"))
+ !command.equals("FILE_META") &&
+ !command.equals("INIT-META"))
{
System.out.println("Command '" + command + "' not found");
}
diff --git a/pixels-cli/src/main/java/io/pixelsdb/pixels/cli/executor/InitMetaExecutor.java b/pixels-cli/src/main/java/io/pixelsdb/pixels/cli/executor/InitMetaExecutor.java
new file mode 100644
index 0000000000..6781143214
--- /dev/null
+++ b/pixels-cli/src/main/java/io/pixelsdb/pixels/cli/executor/InitMetaExecutor.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * This file is part of Pixels.
+ *
+ * Pixels is free software: you can redistribute it and/or modify
+ * it under the terms of the Affero GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * Pixels is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * Affero GNU General Public License for more details.
+ *
+ * You should have received a copy of the Affero GNU General Public
+ * License along with Pixels. If not, see
+ * .
+ */
+package io.pixelsdb.pixels.cli.executor;
+
+import io.pixelsdb.pixels.cli.initmeta.MetadataSchemaInitializer;
+import io.pixelsdb.pixels.common.metadata.MetadataDbType;
+import io.pixelsdb.pixels.common.utils.ConfigFactory;
+import net.sourceforge.argparse4j.inf.Namespace;
+
+/**
+ * Create metadata tables in the configured metadata database.
+ *
+ * @author hank
+ * @create 2026-09-02
+ */
+public class InitMetaExecutor implements CommandExecutor
+{
+ @Override
+ public void execute(Namespace ns, String command) throws Exception
+ {
+ ConfigFactory config = ConfigFactory.Instance();
+ String driver = config.getProperty("metadata.db.driver");
+ String url = config.getProperty("metadata.db.url");
+ MetadataDbType dbType = MetadataDbType.from(driver, url);
+ System.out.println("Initializing metadata tables in " + dbType + " database...");
+ System.out.println("JDBC URL: " + url);
+ int executed = MetadataSchemaInitializer.initialize();
+ System.out.println("INIT-META finished, executed " + executed +
+ " statement(s) from " + dbType.getSchemaResource());
+ }
+}
diff --git a/pixels-cli/src/main/java/io/pixelsdb/pixels/cli/initmeta/MetadataSchemaInitializer.java b/pixels-cli/src/main/java/io/pixelsdb/pixels/cli/initmeta/MetadataSchemaInitializer.java
new file mode 100644
index 0000000000..a39db7a848
--- /dev/null
+++ b/pixels-cli/src/main/java/io/pixelsdb/pixels/cli/initmeta/MetadataSchemaInitializer.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * This file is part of Pixels.
+ *
+ * Pixels is free software: you can redistribute it and/or modify
+ * it under the terms of the Affero GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * Pixels is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * Affero GNU General Public License for more details.
+ *
+ * You should have received a copy of the Affero GNU General Public
+ * License along with Pixels. If not, see
+ * .
+ */
+package io.pixelsdb.pixels.cli.initmeta;
+
+import io.pixelsdb.pixels.common.metadata.MetadataDbType;
+import io.pixelsdb.pixels.common.utils.ConfigFactory;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Creates metadata tables in the configured metadata database.
+ *
+ * @author hank
+ * @create 2026-09-02
+ */
+public class MetadataSchemaInitializer
+{
+ private static final Logger log = LogManager.getLogger(MetadataSchemaInitializer.class);
+
+ private MetadataSchemaInitializer() { }
+
+ /**
+ * Detect the metadata database type from {@code pixels.properties} and execute
+ * the matching CREATE TABLE script.
+ *
+ * @return the number of SQL statements that were executed successfully
+ */
+ public static int initialize() throws Exception
+ {
+ ConfigFactory config = ConfigFactory.Instance();
+ String driver = config.getProperty("metadata.db.driver");
+ String url = config.getProperty("metadata.db.url");
+ String user = config.getProperty("metadata.db.user");
+ String pass = config.getProperty("metadata.db.password");
+ MetadataDbType dbType = MetadataDbType.from(driver, url);
+ return initialize(dbType, driver, url, user, pass);
+ }
+
+ /**
+ * Execute the CREATE TABLE script of the given metadata database type.
+ *
+ * @return the number of SQL statements that were executed successfully
+ */
+ public static int initialize(MetadataDbType dbType, String driver, String url,
+ String user, String pass) throws Exception
+ {
+ if (driver == null || driver.isEmpty())
+ {
+ throw new IllegalArgumentException("metadata.db.driver is not set");
+ }
+ if (url == null || url.isEmpty())
+ {
+ throw new IllegalArgumentException("metadata.db.url is not set");
+ }
+
+ Class.forName(driver);
+ List statements = loadStatements(dbType.getSchemaResource());
+ int executed = 0;
+ try (Connection conn = DriverManager.getConnection(url, user, pass);
+ Statement stmt = conn.createStatement())
+ {
+ for (String sql : statements)
+ {
+ try
+ {
+ stmt.execute(sql);
+ executed++;
+ }
+ catch (SQLException e)
+ {
+ if (isAlreadyExists(e))
+ {
+ log.warn("skip existing metadata object: {}", summarize(sql));
+ }
+ else
+ {
+ throw new SQLException("failed to execute: " + summarize(sql), e);
+ }
+ }
+ }
+ }
+ return executed;
+ }
+
+ private static List loadStatements(String resource) throws IOException
+ {
+ InputStream in = MetadataSchemaInitializer.class.getClassLoader().getResourceAsStream(resource);
+ if (in == null)
+ {
+ throw new IOException("metadata schema resource not found: " + resource);
+ }
+
+ StringBuilder current = new StringBuilder();
+ List statements = new ArrayList<>();
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)))
+ {
+ String line;
+ while ((line = reader.readLine()) != null)
+ {
+ String trimmed = stripLineComment(line).trim();
+ if (trimmed.isEmpty())
+ {
+ continue;
+ }
+ current.append(trimmed).append(' ');
+ if (trimmed.endsWith(";"))
+ {
+ String sql = current.toString().trim();
+ sql = sql.substring(0, sql.length() - 1).trim();
+ if (!sql.isEmpty())
+ {
+ statements.add(sql);
+ }
+ current.setLength(0);
+ }
+ }
+ }
+ String remaining = current.toString().trim();
+ if (!remaining.isEmpty())
+ {
+ statements.add(remaining);
+ }
+ return statements;
+ }
+
+ private static String stripLineComment(String line)
+ {
+ int comment = line.indexOf("--");
+ if (comment < 0)
+ {
+ return line;
+ }
+ return line.substring(0, comment);
+ }
+
+ private static boolean isAlreadyExists(SQLException e)
+ {
+ String state = e.getSQLState();
+ if ("42S01".equals(state) || "42S11".equals(state) || "X0Y32".equals(state))
+ {
+ return true;
+ }
+ String message = e.getMessage();
+ return message != null && message.toLowerCase().contains("already exists");
+ }
+
+ private static String summarize(String sql)
+ {
+ String compact = sql.replaceAll("\\s+", " ");
+ return compact.length() > 120 ? compact.substring(0, 117) + "..." : compact;
+ }
+}
diff --git a/pixels-common/src/main/java/io/pixelsdb/pixels/common/metadata/MetadataDbType.java b/pixels-common/src/main/java/io/pixelsdb/pixels/common/metadata/MetadataDbType.java
new file mode 100644
index 0000000000..55f1d4dea9
--- /dev/null
+++ b/pixels-common/src/main/java/io/pixelsdb/pixels/common/metadata/MetadataDbType.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * This file is part of Pixels.
+ *
+ * Pixels is free software: you can redistribute it and/or modify
+ * it under the terms of the Affero GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * Pixels is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * Affero GNU General Public License for more details.
+ *
+ * You should have received a copy of the Affero GNU General Public
+ * License along with Pixels. If not, see
+ * .
+ */
+package io.pixelsdb.pixels.common.metadata;
+
+import io.pixelsdb.pixels.common.utils.ConfigFactory;
+
+/**
+ * Supported metadata database backends. The type is detected from
+ * {@code metadata.db.driver} and {@code metadata.db.url} in pixels.properties.
+ *
+ * @author hank
+ * @create 2026-09-02
+ */
+public enum MetadataDbType
+{
+ MYSQL("pixels_metadata_mysql.sql"),
+ DERBY("pixels_metadata_derby.sql");
+
+ private final String schemaResource;
+
+ MetadataDbType(String schemaResource)
+ {
+ this.schemaResource = schemaResource;
+ }
+
+ /**
+ * @return the classpath resource that contains CREATE TABLE statements for this backend
+ */
+ public String getSchemaResource()
+ {
+ return this.schemaResource;
+ }
+
+ /**
+ * Detect the metadata database type from {@code pixels.properties}.
+ */
+ public static MetadataDbType fromConfig()
+ {
+ ConfigFactory config = ConfigFactory.Instance();
+ return from(config.getProperty("metadata.db.driver"), config.getProperty("metadata.db.url"));
+ }
+
+ /**
+ * Detect the metadata database type from the JDBC driver class and URL.
+ */
+ public static MetadataDbType from(String driver, String url)
+ {
+ String driverLower = driver == null ? "" : driver.toLowerCase();
+ String urlLower = url == null ? "" : url.toLowerCase();
+ if (driverLower.contains("derby") || urlLower.contains("jdbc:derby"))
+ {
+ return DERBY;
+ }
+ if (driverLower.contains("mysql") || urlLower.contains("jdbc:mysql"))
+ {
+ return MYSQL;
+ }
+ throw new IllegalArgumentException("unsupported metadata database, driver=" +
+ driver + ", url=" + url);
+ }
+}
diff --git a/pixels-daemon/pom.xml b/pixels-daemon/pom.xml
index 4dcac2147e..3d2d0ede2a 100644
--- a/pixels-daemon/pom.xml
+++ b/pixels-daemon/pom.xml
@@ -158,7 +158,6 @@
software.amazon.awssdk
ec2