|
+ :class="{ 'bg-primary-50': dragOverZone === 'value' }">
VALUES
-
+
Скрипт SQL
@@ -381,7 +381,7 @@
Сброс всех настроек
- Вы уверены? Все выбранные поля, фильтры настройки будут удалены.
+ Вы уверены? Все выбранные поля, фильтры, настройки будут удалены.
@@ -436,9 +436,7 @@
-
- Рестораны не найдены
-
+ Рестораны не найдены
@@ -486,9 +484,7 @@
-
- Подключения не найдены
-
+ Подключения не найдены
@@ -503,12 +499,15 @@
@@ -1182,8 +1273,6 @@ onMounted(() => {
.fade-leave-to {
opacity: 0;
}
-
-/* Стили для скроллбара внутри левой панели */
.overflow-y-auto::-webkit-scrollbar {
width: 6px;
}
diff --git a/frontend/src/views/OlapQueriesPage.vue b/frontend/src/views/OlapQueriesPage.vue
new file mode 100644
index 0000000..e9de66f
--- /dev/null
+++ b/frontend/src/views/OlapQueriesPage.vue
@@ -0,0 +1,131 @@
+
+
+
+ OLAP запросы
+ + Создать запрос
+
+
+
+
+
+
+
+ | ID |
+ Название |
+ Подключение |
+ Рестораны |
+ Создан |
+ Действия |
+
+
+
+
+ | {{ q.id }} |
+ {{ q.name }} |
+ {{ q.dbConnectionName }} |
+ {{ q.restaurants }} |
+ {{ formatDate(q.created) }} |
+
+
+
+
+
+ |
+
+
+ | Нет запросов. Создайте первый! |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/su/xserver/iikocon/MainVerticle.java b/src/main/java/su/xserver/iikocon/MainVerticle.java
index d3cc278..3be79e3 100644
--- a/src/main/java/su/xserver/iikocon/MainVerticle.java
+++ b/src/main/java/su/xserver/iikocon/MainVerticle.java
@@ -8,6 +8,7 @@ import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpServer;
+import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
@@ -26,11 +27,13 @@ import su.xserver.iikocon.config.AppConfig;
import su.xserver.iikocon.handler.*;
import su.xserver.iikocon.iiko.IikoHandler;
import su.xserver.iikocon.iiko.IikoOlapClient;
+import su.xserver.iikocon.iiko.OlapQueryService;
import su.xserver.iikocon.service.*;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
+import java.util.stream.Collectors;
public class MainVerticle extends AbstractVerticle {
@@ -46,6 +49,7 @@ public class MainVerticle extends AbstractVerticle {
private RestaurantService restaurantService;
private ExternalDataBaseService externalDataBaseService;
private SettingsService settingsService;
+ private OlapQueryService olapQueryService;
@Override
public void start(Promise startPromise) throws ClassNotFoundException {
@@ -75,6 +79,7 @@ public class MainVerticle extends AbstractVerticle {
restaurantService = new RestaurantService(db.getPool());
settingsService = new SettingsService(db.getPool());
externalDataBaseService = new ExternalDataBaseService(db.getPool(), vertx);
+ olapQueryService = new OlapQueryService(db.getPool(), externalDataBaseService);
userService.initDatabase().onFailure(err -> {
log.error("Failed to initialize database", err);
@@ -92,6 +97,10 @@ public class MainVerticle extends AbstractVerticle {
log.error("Failed to initialize database", err);
startPromise.fail(err);
});
+ olapQueryService.initDatabase().onFailure(err -> {
+ log.error("Failed to initialize database", err);
+ startPromise.fail(err);
+ });
createRouterAndStartHttp(startPromise);
@@ -517,6 +526,98 @@ public class MainVerticle extends AbstractVerticle {
new IikoHandler(vertx, router, db, restaurantService, authHandler);
+
+ // Роуты для OLAP запросов
+ router.get("/api/olap/queries").handler(authHandler::requireAuth).handler(rc -> {
+ olapQueryService.getAllQueries()
+ .onSuccess(queries -> rc.response().putHeader("Content-Type", "application/json").end(queries.encode()))
+ .onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
+ });
+
+ router.get("/api/olap/queries/:id").handler(authHandler::requireAuth).handler(rc -> {
+ int id = Integer.parseInt(rc.pathParam("id"));
+ olapQueryService.getQueryById(id)
+ .onSuccess(query -> {
+ if (query == null) rc.response().setStatusCode(404).end();
+ else rc.response().putHeader("Content-Type", "application/json").end(query.encode());
+ })
+ .onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
+ });
+
+ router.post("/api/olap/queries").handler(authHandler::requireAuth).handler(rc -> {
+ JsonObject body = rc.body().asJsonObject();
+ String name = body.getString("name");
+ Integer dbConnectionId = body.getInteger("dbConnectionId");
+ JsonObject config = body.getJsonObject("config");
+ JsonArray restaurantIdsArray = body.getJsonArray("restaurantIds", new JsonArray());
+ List restaurantIds = restaurantIdsArray.stream().map(id -> (Integer) id).collect(Collectors.toList());
+
+ if (name == null || dbConnectionId == null || config == null) {
+ rc.response().setStatusCode(400).end("Missing required fields");
+ return;
+ }
+
+ // Сначала генерируем SQL
+ olapQueryService.generateSql(config, dbConnectionId)
+ .compose(sql -> olapQueryService.createQuery(name, dbConnectionId, config, restaurantIds, sql))
+ .onSuccess(id -> rc.response().setStatusCode(201).putHeader("Content-Type", "application/json").end(new JsonObject().put("id", id).encode()))
+ .onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
+ });
+
+ router.put("/api/olap/queries/:id").handler(authHandler::requireAuth).handler(rc -> {
+ int id = Integer.parseInt(rc.pathParam("id"));
+ JsonObject body = rc.body().asJsonObject();
+ String name = body.getString("name");
+ Integer dbConnectionId = body.getInteger("dbConnectionId");
+ JsonObject config = body.getJsonObject("config");
+ JsonArray restaurantIdsArray = body.getJsonArray("restaurantIds", new JsonArray());
+ List restaurantIds = restaurantIdsArray.stream().map(v -> (Integer) v).collect(Collectors.toList());
+
+ if (name == null || dbConnectionId == null || config == null) {
+ rc.response().setStatusCode(400).end("Missing required fields");
+ return;
+ }
+
+ olapQueryService.generateSql(config, dbConnectionId)
+ .compose(sql -> olapQueryService.updateQuery(id, name, dbConnectionId, config, restaurantIds, sql))
+ .onSuccess(v -> rc.response().end())
+ .onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
+ });
+
+ router.delete("/api/olap/queries/:id").handler(authHandler::requireAuth).handler(rc -> {
+ int id = Integer.parseInt(rc.pathParam("id"));
+ olapQueryService.deleteQuery(id)
+ .onSuccess(v -> rc.response().end())
+ .onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
+ });
+
+ router.post("/api/olap/generate-sql").handler(authHandler::requireAuth).handler(rc -> {
+ JsonObject body = rc.body().asJsonObject();
+ JsonObject config = body.getJsonObject("config");
+ Integer dbConnectionId = body.getInteger("dbConnectionId");
+ if (config == null || dbConnectionId == null) {
+ rc.response().setStatusCode(400).end("Missing config or dbConnectionId");
+ return;
+ }
+ olapQueryService.generateSql(config, dbConnectionId)
+ .onSuccess(sql -> rc.response().putHeader("Content-Type", "text/plain").end(sql))
+ .onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
+ });
+
+ router.post("/api/olap/export-json").handler(authHandler::requireAuth).handler(rc -> {
+ JsonObject body = rc.body().asJsonObject();
+ JsonObject config = body.getJsonObject("config");
+ if (config == null) {
+ rc.response().setStatusCode(400).end("Missing config");
+ return;
+ }
+ JsonObject fullJson = olapQueryService.generateFullIikoJson(config);
+ rc.response()
+ .putHeader("Content-Type", "application/json")
+ .putHeader("Content-Disposition", "attachment; filename=olap_export.json")
+ .end(fullJson.encodePrettily());
+ });
+
return router;
}
diff --git a/src/main/java/su/xserver/iikocon/iiko/OlapQueryService.java b/src/main/java/su/xserver/iikocon/iiko/OlapQueryService.java
new file mode 100644
index 0000000..f219d0f
--- /dev/null
+++ b/src/main/java/su/xserver/iikocon/iiko/OlapQueryService.java
@@ -0,0 +1,267 @@
+package su.xserver.iikocon.iiko;
+
+import io.vertx.core.Future;
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
+import io.vertx.sqlclient.Pool;
+import io.vertx.sqlclient.Row;
+import io.vertx.sqlclient.templates.SqlTemplate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import su.xserver.iikocon.service.ExternalDataBaseService;
+
+import java.util.*;
+
+public class OlapQueryService {
+ private static final Logger log = LoggerFactory.getLogger(OlapQueryService.class);
+ private final Pool pool;
+ private final ExternalDataBaseService externalDataBaseService;
+ private final SqlGenerator sqlGenerator;
+
+ public OlapQueryService(Pool pool, ExternalDataBaseService externalDataBaseService) {
+ this.pool = pool;
+ this.externalDataBaseService = externalDataBaseService;
+ this.sqlGenerator = new SqlGenerator();
+ }
+
+ public Future initDatabase() {
+ String createQueriesTable = """
+ CREATE TABLE IF NOT EXISTS olap_queries (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ db_connection_id INT NOT NULL,
+ config_json JSON NOT NULL,
+ full_config_json JSON NOT NULL,
+ sql_text TEXT,
+ created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ FOREIGN KEY (db_connection_id) REFERENCES external_database(id) ON DELETE RESTRICT
+ ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
+ """;
+
+ String createQueryRestaurantsTable = """
+ CREATE TABLE IF NOT EXISTS olap_query_restaurants (
+ query_id INT NOT NULL,
+ restaurant_id INT NOT NULL,
+ PRIMARY KEY (query_id, restaurant_id),
+ FOREIGN KEY (query_id) REFERENCES olap_queries(id) ON DELETE CASCADE,
+ FOREIGN KEY (restaurant_id) REFERENCES restaurants(id) ON DELETE CASCADE
+ ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
+ """;
+
+ return pool.query(createQueriesTable).execute()
+ .compose(v -> pool.query(createQueryRestaurantsTable).execute())
+ .mapEmpty();
+ }
+
+ // Создание запроса
+ public Future createQuery(String name, int dbConnectionId, JsonObject config,
+ List restaurantIds, String generatedSql) {
+ JsonObject fullConfig = generateFullIikoJson(config);
+ Map params = Map.of(
+ "name", name,
+ "db_connection_id", dbConnectionId,
+ "config_json", config.encode(),
+ "full_config_json", fullConfig.encode(),
+ "sql_text", generatedSql != null ? generatedSql : ""
+ );
+ String sql = "INSERT INTO olap_queries (name, db_connection_id, config_json, full_config_json, sql_text) VALUES (#{name}, #{db_connection_id}, #{config_json}, #{full_config_json}, #{sql_text})";
+ return SqlTemplate.forUpdate(pool, sql)
+ .execute(params)
+ .compose(rows -> getLastInsertId())
+ .compose(queryId -> linkRestaurants(queryId, restaurantIds).map(queryId));
+ }
+
+ public Future updateQuery(int id, String name, int dbConnectionId, JsonObject config,
+ List restaurantIds, String generatedSql) {
+ JsonObject fullConfig = generateFullIikoJson(config);
+ Map params = Map.of(
+ "id", id,
+ "name", name,
+ "db_connection_id", dbConnectionId,
+ "config_json", config.encode(),
+ "full_config_json", fullConfig.encode(),
+ "sql_text", generatedSql != null ? generatedSql : ""
+ );
+ String sql = "UPDATE olap_queries SET name = #{name}, db_connection_id = #{db_connection_id}, config_json = #{config_json}, full_config_json = #{full_config_json}, sql_text = #{sql_text} WHERE id = #{id}";
+ return SqlTemplate.forUpdate(pool, sql)
+ .execute(params)
+ .compose(v -> {
+ return pool.query("DELETE FROM olap_query_restaurants WHERE query_id = " + id).execute()
+ .compose(del -> linkRestaurants(id, restaurantIds));
+ }).mapEmpty();
+ }
+
+ public JsonObject generateFullIikoJson(JsonObject clientConfig) {
+ String reportType = clientConfig.getString("reportType", "SALES");
+ boolean buildSummary = clientConfig.getBoolean("buildSummary", false);
+ String dateToStr = clientConfig.getString("dateTo", "");
+ int daysBack = clientConfig.getInteger("daysBack", 7);
+ JsonArray rowFields = clientConfig.getJsonArray("rowFields", new JsonArray());
+ JsonArray columnFields = clientConfig.getJsonArray("columnFields", new JsonArray());
+ JsonArray valueFields = clientConfig.getJsonArray("valueFields", new JsonArray());
+ JsonArray filterFields = clientConfig.getJsonArray("filterFields", new JsonArray());
+
+ // Пользовательские фильтры
+ JsonObject userFilters = new JsonObject();
+ for (Object fObj : filterFields) {
+ JsonObject f = (JsonObject) fObj;
+ String fieldKey = f.getString("fieldKey");
+ String filterType = f.getString("filterType");
+ JsonObject filterDef = new JsonObject().put("filterType", filterType);
+ if ("IncludeValues".equals(filterType) || "ExcludeValues".equals(filterType)) {
+ filterDef.put("values", f.getJsonArray("values", new JsonArray()));
+ } else if ("EnumValue".equals(filterType)) {
+ filterDef.put("enumKey", f.getString("enumKey", ""));
+ filterDef.put("enumValue", f.getString("enumValue", ""));
+ } else if ("StringValue".equals(filterType)) {
+ filterDef.put("value", f.getString("value", ""));
+ }
+ userFilters.put(fieldKey, filterDef);
+ }
+
+ // Фильтр дат
+ JsonObject dateFilter = buildDateFilter(reportType, dateToStr, daysBack);
+ // Системные фильтры
+ JsonObject systemFilters = new JsonObject()
+ .put("DeletedWithWriteoff", new JsonObject()
+ .put("filterType", "ExcludeValues")
+ .put("values", new JsonArray().add("DELETED_WITH_WRITEOFF").add("DELETED_WITHOUT_WRITEOFF")))
+ .put("OrderDeleted", new JsonObject()
+ .put("filterType", "IncludeValues")
+ .put("values", new JsonArray().add("NOT_DELETED")));
+
+ JsonObject allFilters = new JsonObject();
+ // Добавляем пользовательские
+ userFilters.forEach(entry -> allFilters.put(entry.getKey(), entry.getValue()));
+ // Добавляем дату
+ allFilters.mergeIn(dateFilter);
+ // Добавляем системные
+ allFilters.mergeIn(systemFilters);
+
+ JsonObject result = new JsonObject()
+ .put("reportType", reportType)
+ .put("buildSummary", buildSummary)
+ .put("groupByRowFields", rowFields)
+ .put("groupByColFields", columnFields)
+ .put("aggregateFields", valueFields)
+ .put("filters", allFilters);
+ return result;
+ }
+
+ private JsonObject buildDateFilter(String reportType, String dateToStr, int daysBack) {
+ // Определяем корректную дату "до" (конец дня)
+ java.time.ZonedDateTime toDate;
+ if (dateToStr != null && !dateToStr.isEmpty()) {
+ toDate = java.time.LocalDate.parse(dateToStr).atStartOfDay(java.time.ZoneOffset.UTC);
+ } else {
+ toDate = java.time.ZonedDateTime.now(java.time.ZoneOffset.UTC);
+ }
+ toDate = toDate.withHour(23).withMinute(59).withSecond(59).withNano(999_999_999);
+ java.time.ZonedDateTime fromDate = toDate.minusDays(Math.max(1, daysBack))
+ .withHour(0).withMinute(0).withSecond(0).withNano(0);
+
+ String filterKey = "TRANSACTIONS".equals(reportType) ? "DateTime.DateTyped" : "OpenDate.Typed";
+ return new JsonObject().put(filterKey, new JsonObject()
+ .put("filterType", "DateRange")
+ .put("periodType", "CUSTOM")
+ .put("from", fromDate.toString())
+ .put("to", toDate.toString()));
+ }
+
+ private Future getLastInsertId() {
+ return pool.query("SELECT LAST_INSERT_ID() AS id").execute()
+ .map(rows -> rows.iterator().next().getInteger("id"));
+ }
+
+ private Future linkRestaurants(int queryId, List restaurantIds) {
+ if (restaurantIds == null || restaurantIds.isEmpty()) return Future.succeededFuture();
+ List> futures = new ArrayList<>();
+ for (Integer restId : restaurantIds) {
+ Map params = Map.of("query_id", queryId, "restaurant_id", restId);
+ futures.add(SqlTemplate.forUpdate(pool,
+ "INSERT INTO olap_query_restaurants (query_id, restaurant_id) VALUES (#{query_id}, #{restaurant_id})")
+ .execute(params).mapEmpty());
+ }
+ return Future.all(futures).mapEmpty();
+ }
+
+ // Удаление
+ public Future deleteQuery(int id) {
+ return SqlTemplate.forUpdate(pool, "DELETE FROM olap_queries WHERE id = #{id}")
+ .execute(Map.of("id", id)).mapEmpty();
+ }
+
+ // Получить все запросы (без config_json, для списка)
+ public Future getAllQueries() {
+ String sql = """
+ SELECT q.id, q.name, q.db_connection_id, q.created, q.updated,
+ GROUP_CONCAT(r.name SEPARATOR ', ') AS restaurants,
+ dc.name AS db_connection_name
+ FROM olap_queries q
+ LEFT JOIN olap_query_restaurants qr ON q.id = qr.query_id
+ LEFT JOIN restaurants r ON qr.restaurant_id = r.id
+ LEFT JOIN external_database dc ON q.db_connection_id = dc.id
+ GROUP BY q.id
+ ORDER BY q.id DESC
+ """;
+ return pool.query(sql).execute()
+ .map(rows -> {
+ JsonArray arr = new JsonArray();
+ rows.forEach(row -> {
+ arr.add(new JsonObject()
+ .put("id", row.getInteger("id"))
+ .put("name", row.getString("name"))
+ .put("dbConnectionId", row.getInteger("db_connection_id"))
+ .put("dbConnectionName", row.getString("db_connection_name"))
+ .put("restaurants", row.getString("restaurants") != null ? row.getString("restaurants") : "")
+ .put("created", row.getLocalDateTime("created") != null ? row.getLocalDateTime("created").toString() : null)
+ .put("updated", row.getLocalDateTime("updated") != null ? row.getLocalDateTime("updated").toString() : null));
+ });
+ return arr;
+ });
+ }
+
+ // Получить один запрос с полной конфигурацией
+ public Future getQueryById(int id) {
+ String querySql = "SELECT id, name, db_connection_id, config_json, sql_text, created, updated FROM olap_queries WHERE id = ?";
+ String restaurantsSql = "SELECT restaurant_id FROM olap_query_restaurants WHERE query_id = ?";
+
+ return pool.preparedQuery(querySql).execute(io.vertx.sqlclient.Tuple.of(id))
+ .compose(rows -> {
+ if (rows.size() == 0) return Future.succeededFuture(null);
+ Row row = rows.iterator().next();
+ JsonObject result = new JsonObject()
+ .put("id", row.getInteger("id"))
+ .put("name", row.getString("name"))
+ .put("dbConnectionId", row.getInteger("db_connection_id"))
+ .put("config", new JsonObject(row.getString("config_json")))
+ .put("sql", row.getString("sql_text"))
+ .put("created", row.getLocalDateTime("created") != null ? row.getLocalDateTime("created").toString() : null)
+ .put("updated", row.getLocalDateTime("updated") != null ? row.getLocalDateTime("updated").toString() : null);
+
+ return pool.preparedQuery(restaurantsSql).execute(io.vertx.sqlclient.Tuple.of(id))
+ .map(restRows -> {
+ JsonArray restIds = new JsonArray();
+ restRows.forEach(restRow -> restIds.add(restRow.getInteger("restaurant_id")));
+ result.put("restaurantIds", restIds);
+ return result;
+ });
+ });
+ }
+
+ // Генерация SQL на основе конфигурации и ID подключения
+ public Future generateSql(JsonObject config, int dbConnectionId) {
+ return externalDataBaseService.findById(dbConnectionId)
+ .compose(conn -> {
+ if (conn == null) return Future.failedFuture("Database connection not found");
+ String dbType = conn.getString("type");
+ try {
+ String sql = sqlGenerator.generate(config, dbType);
+ return Future.succeededFuture(sql);
+ } catch (Exception e) {
+ return Future.failedFuture("SQL generation failed: " + e.getMessage());
+ }
+ });
+ }
+}
diff --git a/src/main/java/su/xserver/iikocon/iiko/SqlGenerator.java b/src/main/java/su/xserver/iikocon/iiko/SqlGenerator.java
new file mode 100644
index 0000000..97417f7
--- /dev/null
+++ b/src/main/java/su/xserver/iikocon/iiko/SqlGenerator.java
@@ -0,0 +1,108 @@
+package su.xserver.iikocon.iiko;
+
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class SqlGenerator {
+
+ public String generate(JsonObject config, String dbType) {
+ String reportType = config.getString("reportType", "SALES");
+ String dateCol = reportType.equals("TRANSACTIONS") ? "DateTime_DateTyped" : "OpenDate_Typed";
+ String tableName = sanitizeTableName(config.getString("tableName", "olap_table"));
+ boolean buildSummary = config.getBoolean("buildSummary", false);
+ String dateTo = config.getString("dateTo", "");
+ int daysBack = config.getInteger("daysBack", 7);
+ JsonArray rowFields = config.getJsonArray("rowFields", new JsonArray());
+ JsonArray columnFields = config.getJsonArray("columnFields", new JsonArray());
+ JsonArray valueFields = config.getJsonArray("valueFields", new JsonArray());
+ JsonArray filterFields = config.getJsonArray("filterFields", new JsonArray());
+
+ // Собираем список всех столбцов
+ List allColumns = new ArrayList<>();
+ allColumns.add(dateCol);
+ rowFields.forEach(f -> allColumns.add(f.toString()));
+ columnFields.forEach(f -> allColumns.add(f.toString()));
+ if (valueFields.isEmpty()) {
+ allColumns.add("dummy");
+ } else {
+ valueFields.forEach(vf -> allColumns.add(vf.toString()));
+ }
+
+ // Формируем CREATE TABLE
+ String createTable = buildCreateTable(tableName, allColumns, dateCol, dbType);
+
+ // Формируем INSERT
+ String insert = buildInsert(tableName, allColumns, dateCol, dateTo, daysBack, dbType);
+
+ // Если нужно SUMMARY, добавим позже (пока пропустим, можно расширить)
+ return createTable + "\n\n" + insert;
+ }
+
+ private String buildCreateTable(String tableName, List columns, String dateCol, String dbType) {
+ StringBuilder sb = new StringBuilder("CREATE TABLE IF NOT EXISTS ");
+ if (dbType.equals("mysql")) {
+ sb.append("`").append(tableName).append("` (\n");
+ for (String col : columns) {
+ sb.append(" `").append(col).append("` ").append(getColumnType(col, dbType)).append(",\n");
+ }
+ sb.append(" PRIMARY KEY (`").append(dateCol).append("`)\n");
+ sb.append(") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
+ } else if (dbType.equals("postgres")) {
+ sb.append("\"").append(tableName).append("\" (\n");
+ for (String col : columns) {
+ sb.append(" \"").append(col).append("\" ").append(getColumnType(col, dbType)).append(",\n");
+ }
+ sb.append(" PRIMARY KEY (\"").append(dateCol).append("\")\n");
+ sb.append(");");
+ } else { // ClickHouse
+ sb.append("`default`.`").append(tableName).append("` (\n");
+ for (String col : columns) {
+ sb.append(" `").append(col).append("` ").append(getColumnType(col, dbType)).append(",\n");
+ }
+ sb.append(") ENGINE = ReplacingMergeTree()\n");
+ sb.append("ORDER BY (`").append(dateCol).append("`)\n");
+ sb.append("SETTINGS index_granularity = 8192;");
+ }
+ return sb.toString();
+ }
+
+ private String buildInsert(String tableName, List columns, String dateCol,
+ String dateTo, int daysBack, String dbType) {
+ StringBuilder sb = new StringBuilder("INSERT INTO ");
+ if (dbType.equals("mysql")) {
+ sb.append("`").append(tableName).append("` (");
+ } else if (dbType.equals("postgres")) {
+ sb.append("\"").append(tableName).append("\" (");
+ } else {
+ sb.append("`default`.`").append(tableName).append("` (");
+ }
+ String columnNames = columns.stream()
+ .map(c -> dbType.equals("postgres") ? "\"" + c + "\"" : "`" + c + "`")
+ .collect(Collectors.joining(", "));
+ sb.append(columnNames).append(") VALUES\n");
+ // Здесь должна быть реальная выборка из iiko, но для демонстрации – заглушка
+ sb.append("-- Здесь будет реальный SELECT из источника данных\n");
+ sb.append("-- (например, из временной таблицы или прямого запроса к iiko API)");
+ return sb.toString();
+ }
+
+ private String getColumnType(String column, String dbType) {
+ if (column.equalsIgnoreCase("dummy")) return "String";
+ if (column.contains("Date")) {
+ if (dbType.equals("mysql")) return "DATE";
+ if (dbType.equals("postgres")) return "DATE";
+ return "Date";
+ }
+ if (dbType.equals("clickhouse")) return "String";
+ return "VARCHAR(255)";
+ }
+
+ private String sanitizeTableName(String name) {
+ if (name == null || name.trim().isEmpty()) return "olap_table";
+ return name.replaceAll("[^a-zA-Z0-9_]", "_");
+ }
+}
|