001/*
002 *  Copyright (c) 2022-2025, Mybatis-Flex (fuhai999@gmail.com).
003 *  <p>
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *  <p>
008 *  http://www.apache.org/licenses/LICENSE-2.0
009 *  <p>
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package com.mybatisflex.core.dialect.impl;
017
018import com.mybatisflex.core.dialect.KeywordWrap;
019import com.mybatisflex.core.dialect.LimitOffsetProcessor;
020import com.mybatisflex.core.dialect.OperateType;
021import com.mybatisflex.core.exception.FlexExceptions;
022import com.mybatisflex.core.exception.locale.LocalizedFormats;
023import com.mybatisflex.core.query.CPI;
024import com.mybatisflex.core.query.QueryTable;
025import com.mybatisflex.core.query.QueryWrapper;
026import com.mybatisflex.core.row.Row;
027import com.mybatisflex.core.row.RowCPI;
028import com.mybatisflex.core.table.TableInfo;
029import com.mybatisflex.core.update.RawValue;
030import com.mybatisflex.core.util.ArrayUtil;
031import com.mybatisflex.core.util.CollectionUtil;
032import com.mybatisflex.core.util.StringUtil;
033
034import java.util.List;
035import java.util.Map;
036import java.util.Set;
037import java.util.StringJoiner;
038
039import static com.mybatisflex.core.constant.SqlConsts.*;
040
041/**
042 * @author: 老唐
043 * @date: 2024-07-20 11:36
044 * @version: 1.0
045 */
046public class ClickhouseDialectImpl extends CommonsDialectImpl {
047    public static final String ALTER_TABLE = " ALTER TABLE ";
048    public static final String CK_DELETE = " DELETE ";
049    public static final String CK_UPDATE = " UPDATE ";
050
051    public ClickhouseDialectImpl(KeywordWrap keywordWrap, LimitOffsetProcessor limitOffsetProcessor) {
052        super(keywordWrap, limitOffsetProcessor);
053    }
054
055    /**
056     * 根据主键更新
057     *
058     * @param schema
059     * @param tableName
060     * @param row
061     * @return
062     */
063    @Override
064    public String forUpdateById(String schema, String tableName, Row row) {
065        //eg: ALTER TABLE test  UPDATE USERNAME = ? , AGE = ?  WHERE CUSERID = ?
066        String table = getRealTable(tableName, OperateType.UPDATE);
067        StringBuilder sql = new StringBuilder();
068        Set<String> modifyAttrs = RowCPI.getModifyAttrs(row);
069        Map<String, RawValue> rawValueMap = RowCPI.getRawValueMap(row);
070        String[] primaryKeys = RowCPI.obtainsPrimaryKeyStrings(row);
071
072        sql.append(ALTER_TABLE);
073        if (StringUtil.isNotBlank(schema)) {
074            sql.append(wrap(getRealSchema(schema, table, OperateType.UPDATE))).append(REFERENCE);
075        }
076        sql.append(wrap(table)).append(CK_UPDATE);
077        int index = 0;
078        for (Map.Entry<String, Object> e : row.entrySet()) {
079            String colName = e.getKey();
080            if (modifyAttrs.contains(colName) && !ArrayUtil.contains(primaryKeys, colName)) {
081                if (index > 0) {
082                    sql.append(DELIMITER);
083                }
084                sql.append(wrap(colName));
085
086                if (rawValueMap.containsKey(colName)) {
087                    sql.append(EQUALS).append(rawValueMap.get(colName).toSql(this));
088                } else {
089                    sql.append(EQUALS_PLACEHOLDER);
090                }
091
092                index++;
093            }
094        }
095        sql.append(WHERE);
096        for (int i = 0; i < primaryKeys.length; i++) {
097            if (i > 0) {
098                sql.append(AND);
099            }
100            sql.append(wrap(primaryKeys[i])).append(EQUALS_PLACEHOLDER);
101        }
102        prepareAuth(schema, table, sql, OperateType.UPDATE);
103        return sql.toString();
104    }
105
106    /**
107     * 根据主键删除
108     *
109     * @param schema
110     * @param tableName
111     * @param primaryKeys
112     * @return
113     */
114    @Override
115    public String forDeleteById(String schema, String tableName, String[] primaryKeys) {
116        //eg: ALTER TABLE test  DELETE WHERE CUSERID = ?
117        String table = getRealTable(tableName, OperateType.DELETE);
118        StringBuilder sql = new StringBuilder();
119
120        sql.append(ALTER_TABLE);
121        if (StringUtil.isNotBlank(schema)) {
122            sql.append(wrap(getRealSchema(schema, table, OperateType.DELETE))).append(REFERENCE);
123        }
124        sql.append(wrap(table));
125        sql.append(CK_DELETE);
126        sql.append(WHERE);
127        for (int i = 0; i < primaryKeys.length; i++) {
128            if (i > 0) {
129                sql.append(AND);
130            }
131            sql.append(wrap(primaryKeys[i])).append(EQUALS_PLACEHOLDER);
132        }
133        prepareAuth(schema, table, sql, OperateType.DELETE);
134        return sql.toString();
135    }
136
137    /**
138     * 根据查询更新
139     *
140     * @param queryWrapper
141     * @param row
142     * @return
143     */
144    @Override
145    public String forUpdateByQuery(QueryWrapper queryWrapper, Row row) {
146        //eg: ALTER TABLE test  UPDATE USERNAME = ? , AGE = ?  WHERE CUSERID = ?
147        prepareAuth(queryWrapper, OperateType.UPDATE);
148        StringBuilder sql = new StringBuilder();
149
150        Set<String> modifyAttrs = RowCPI.getModifyAttrs(row);
151        Map<String, RawValue> rawValueMap = RowCPI.getRawValueMap(row);
152
153        List<QueryTable> queryTables = CPI.getQueryTables(queryWrapper);
154        if (queryTables == null || queryTables.size() != 1) {
155            throw FlexExceptions.wrap(LocalizedFormats.UPDATE_ONLY_SUPPORT_1_TABLE);
156        }
157        sql.append(ALTER_TABLE);
158        // fix: support schema
159        QueryTable queryTable = queryTables.get(0);
160        sql.append(queryTable.toSql(this, OperateType.UPDATE)).append(CK_UPDATE);
161        int index = 0;
162        for (String modifyAttr : modifyAttrs) {
163            if (index > 0) {
164                sql.append(DELIMITER);
165            }
166
167            sql.append(wrap(modifyAttr));
168
169            if (rawValueMap.containsKey(modifyAttr)) {
170                sql.append(EQUALS).append(rawValueMap.get(modifyAttr).toSql(this));
171            } else {
172                sql.append(EQUALS_PLACEHOLDER);
173            }
174
175            index++;
176        }
177
178        buildJoinSql(sql, queryWrapper, queryTables, OperateType.UPDATE);
179        buildWhereSql(sql, queryWrapper, queryTables, false);
180        buildGroupBySql(sql, queryWrapper, queryTables);
181        buildHavingSql(sql, queryWrapper, queryTables);
182
183        // ignore orderBy and limit
184        buildOrderBySql(sql, queryWrapper, queryTables);
185
186        Long limitRows = CPI.getLimitRows(queryWrapper);
187        Long limitOffset = CPI.getLimitOffset(queryWrapper);
188        if (limitRows != null || limitOffset != null) {
189            sql = buildLimitOffsetSql(sql, queryWrapper, limitRows, limitOffset);
190        }
191        return sql.toString();
192    }
193
194    /**
195     * 根据主键批量删除
196     *
197     * @param schema
198     * @param tableName
199     * @param primaryKeys
200     * @param ids
201     * @return
202     */
203    @Override
204    public String forDeleteBatchByIds(String schema, String tableName, String[] primaryKeys, Object[] ids) {
205        //eg: ALTER TABLE test  DELETE WHERE CUSERID = ?
206        String table = getRealTable(tableName, OperateType.DELETE);
207        StringBuilder sql = new StringBuilder();
208        sql.append(ALTER_TABLE);
209        if (StringUtil.isNotBlank(schema)) {
210            sql.append(wrap(getRealSchema(schema, table, OperateType.DELETE))).append(REFERENCE);
211        }
212        sql.append(wrap(table));
213        sql.append(CK_DELETE);
214        sql.append(WHERE);
215
216        // 多主键的场景
217        if (primaryKeys.length > 1) {
218            for (int i = 0; i < ids.length / primaryKeys.length; i++) {
219                if (i > 0) {
220                    sql.append(OR);
221                }
222                sql.append(BRACKET_LEFT);
223                for (int j = 0; j < primaryKeys.length; j++) {
224                    if (j > 0) {
225                        sql.append(AND);
226                    }
227                    sql.append(wrap(primaryKeys[j])).append(EQUALS_PLACEHOLDER);
228                }
229                sql.append(BRACKET_RIGHT);
230            }
231        }
232        // 单主键
233        else {
234            for (int i = 0; i < ids.length; i++) {
235                if (i > 0) {
236                    sql.append(OR);
237                }
238                sql.append(wrap(primaryKeys[0])).append(EQUALS_PLACEHOLDER);
239            }
240        }
241        prepareAuth(schema, table, sql, OperateType.DELETE);
242        return sql.toString();
243    }
244
245    /**
246     * 实体 根据主键批量删除及逻辑删除
247     *
248     * @param tableInfo
249     * @param primaryValues
250     * @return
251     */
252    @Override
253    public String forDeleteEntityBatchByIds(TableInfo tableInfo, Object[] primaryValues) {
254        //eg: ALTER TABLE test  UPDATE DR = ?  WHERE CUSERID = ?
255        String logicDeleteColumn = tableInfo.getLogicDeleteColumnOrSkip();
256        Object[] tenantIdArgs = tableInfo.buildTenantIdArgs();
257
258        // 正常删除
259        if (StringUtil.isBlank(logicDeleteColumn)) {
260            String deleteSQL = forDeleteBatchByIds(tableInfo.getSchema(), tableInfo.getTableName(), tableInfo.getPrimaryColumns(), primaryValues);
261
262            // 多租户
263            if (ArrayUtil.isNotEmpty(tenantIdArgs)) {
264                deleteSQL = deleteSQL.replace(WHERE, WHERE + BRACKET_LEFT) + BRACKET_RIGHT;
265                deleteSQL = tableInfo.buildTenantCondition(deleteSQL, tenantIdArgs, this);
266            }
267            return deleteSQL;
268        }
269
270        StringBuilder sql = new StringBuilder();
271        sql.append(ALTER_TABLE);
272        sql.append(tableInfo.getWrapSchemaAndTableName(this, OperateType.UPDATE));
273        sql.append(CK_UPDATE).append(buildLogicDeletedSet(logicDeleteColumn, tableInfo));
274        sql.append(WHERE);
275        sql.append(BRACKET_LEFT);
276
277        String[] primaryKeys = tableInfo.getPrimaryColumns();
278
279        // 多主键的场景
280        if (primaryKeys.length > 1) {
281            for (int i = 0; i < primaryValues.length / primaryKeys.length; i++) {
282                if (i > 0) {
283                    sql.append(OR);
284                }
285                sql.append(BRACKET_LEFT);
286                for (int j = 0; j < primaryKeys.length; j++) {
287                    if (j > 0) {
288                        sql.append(AND);
289                    }
290                    sql.append(wrap(primaryKeys[j])).append(EQUALS_PLACEHOLDER);
291                }
292                sql.append(BRACKET_RIGHT);
293            }
294        }
295        // 单主键
296        else {
297            for (int i = 0; i < primaryValues.length; i++) {
298                if (i > 0) {
299                    sql.append(OR);
300                }
301                sql.append(wrap(primaryKeys[0])).append(EQUALS_PLACEHOLDER);
302            }
303        }
304
305        sql.append(BRACKET_RIGHT).append(AND).append(buildLogicNormalCondition(logicDeleteColumn, tableInfo));
306
307        tableInfo.buildTenantCondition(sql, tenantIdArgs, this);
308        prepareAuth(tableInfo, sql, OperateType.DELETE);
309        return sql.toString();
310    }
311
312    @Override
313    public String buildDeleteSql(QueryWrapper queryWrapper) {
314        //eg: ALTER TABLE test  DELETE WHERE CUSERID = ?
315        List<QueryTable> queryTables = CPI.getQueryTables(queryWrapper);
316        List<QueryTable> joinTables = CPI.getJoinTables(queryWrapper);
317        List<QueryTable> allTables = CollectionUtil.merge(queryTables, joinTables);
318        // delete with join
319        if (joinTables != null && !joinTables.isEmpty()) {
320            throw new IllegalArgumentException("Delete query not support join sql ");
321        }
322        // ignore selectColumns
323        StringBuilder sqlBuilder = new StringBuilder(ALTER_TABLE);
324        String hint = CPI.getHint(queryWrapper);
325        if (StringUtil.isNotBlank(hint)) {
326            sqlBuilder.append(BLANK).append(hint).deleteCharAt(sqlBuilder.length() - 1);
327        }
328
329        sqlBuilder.append(StringUtil.join(DELIMITER, queryTables, queryTable -> queryTable.toSql(this, OperateType.DELETE)));
330        sqlBuilder.append(CK_DELETE);
331
332        buildWhereSql(sqlBuilder, queryWrapper, allTables, false);
333        buildGroupBySql(sqlBuilder, queryWrapper, allTables);
334        buildHavingSql(sqlBuilder, queryWrapper, allTables);
335
336        // ignore orderBy and limit
337        buildOrderBySql(sqlBuilder, queryWrapper, allTables);
338
339        Long limitRows = CPI.getLimitRows(queryWrapper);
340        Long limitOffset = CPI.getLimitOffset(queryWrapper);
341        if (limitRows != null || limitOffset != null) {
342            sqlBuilder = buildLimitOffsetSql(sqlBuilder, queryWrapper, limitRows, limitOffset);
343        }
344
345        List<String> endFragments = CPI.getEndFragments(queryWrapper);
346        if (CollectionUtil.isNotEmpty(endFragments)) {
347            for (String endFragment : endFragments) {
348                sqlBuilder.append(BLANK).append(endFragment);
349            }
350        }
351
352        return sqlBuilder.toString();
353    }
354
355    @Override
356    public String forDeleteEntityBatchByQuery(TableInfo tableInfo, QueryWrapper queryWrapper) {
357        String logicDeleteColumn = tableInfo.getLogicDeleteColumnOrSkip();
358
359        // 正常删除
360        if (StringUtil.isBlank(logicDeleteColumn)) {
361            return forDeleteByQuery(queryWrapper);
362        }
363
364
365        prepareAuth(queryWrapper, OperateType.DELETE);
366        // 逻辑删除
367        List<QueryTable> queryTables = CPI.getQueryTables(queryWrapper);
368        List<QueryTable> joinTables = CPI.getJoinTables(queryWrapper);
369        List<QueryTable> allTables = CollectionUtil.merge(queryTables, joinTables);
370
371        // ignore selectColumns
372        //eg: ALTER TABLE test  UPDATE DR = ?  WHERE CUSERID = ?
373        StringBuilder sqlBuilder = new StringBuilder(ALTER_TABLE).append(forHint(CPI.getHint(queryWrapper)));
374        sqlBuilder.append(tableInfo.getWrapSchemaAndTableName(this, OperateType.DELETE));
375        sqlBuilder.append(CK_UPDATE).append(buildLogicDeletedSet(logicDeleteColumn, tableInfo));
376
377
378        buildJoinSql(sqlBuilder, queryWrapper, allTables, OperateType.DELETE);
379        buildWhereSql(sqlBuilder, queryWrapper, allTables, false);
380        buildGroupBySql(sqlBuilder, queryWrapper, allTables);
381        buildHavingSql(sqlBuilder, queryWrapper, allTables);
382
383        // ignore orderBy and limit
384        // buildOrderBySql(sqlBuilder, queryWrapper)
385        // buildLimitSql(sqlBuilder, queryWrapper)
386
387        return sqlBuilder.toString();
388    }
389
390
391    @Override
392    public String forUpdateEntity(TableInfo tableInfo, Object entity, boolean ignoreNulls) {
393        //eg: ALTER TABLE test  UPDATE AGE = ?  WHERE CUSERID = ?
394        StringBuilder sql = new StringBuilder();
395
396        Set<String> updateColumns = tableInfo.obtainUpdateColumns(entity, ignoreNulls, false);
397        Map<String, RawValue> rawValueMap = tableInfo.obtainUpdateRawValueMap(entity);
398        String[] primaryKeys = tableInfo.getPrimaryColumns();
399
400        sql.append(ALTER_TABLE)
401                .append(tableInfo.getWrapSchemaAndTableName(this, OperateType.UPDATE))
402                .append(CK_UPDATE);
403
404        StringJoiner stringJoiner = new StringJoiner(DELIMITER);
405
406        for (String updateColumn : updateColumns) {
407            if (rawValueMap.containsKey(updateColumn)) {
408                stringJoiner.add(wrap(updateColumn) + EQUALS + rawValueMap.get(updateColumn).toSql(this));
409            } else {
410                stringJoiner.add(wrap(updateColumn) + EQUALS_PLACEHOLDER);
411            }
412        }
413
414        Map<String, String> onUpdateColumns = tableInfo.getOnUpdateColumns();
415        if (onUpdateColumns != null && !onUpdateColumns.isEmpty()) {
416            onUpdateColumns.forEach((column, value) -> stringJoiner.add(wrap(column) + EQUALS + value));
417        }
418
419        // 乐观锁字段
420        String versionColumn = tableInfo.getVersionColumn();
421        if (StringUtil.isNotBlank(versionColumn)) {
422            stringJoiner.add(wrap(versionColumn) + EQUALS + wrap(versionColumn) + " + 1 ");
423        }
424
425        sql.append(stringJoiner);
426
427        sql.append(WHERE);
428        for (int i = 0; i < primaryKeys.length; i++) {
429            if (i > 0) {
430                sql.append(AND);
431            }
432            sql.append(wrap(primaryKeys[i])).append(EQUALS_PLACEHOLDER);
433        }
434
435        // 逻辑删除条件,已删除的数据不能被修改
436        String logicDeleteColumn = tableInfo.getLogicDeleteColumnOrSkip();
437        if (StringUtil.isNotBlank(logicDeleteColumn)) {
438            sql.append(AND).append(buildLogicNormalCondition(logicDeleteColumn, tableInfo));
439        }
440
441
442        // 租户ID字段
443        Object[] tenantIdArgs = tableInfo.buildTenantIdArgs();
444        tableInfo.buildTenantCondition(sql, tenantIdArgs, this);
445
446        // 乐观锁条件
447        if (StringUtil.isNotBlank(versionColumn)) {
448            Object versionValue = tableInfo.buildColumnSqlArg(entity, versionColumn);
449            if (versionValue == null) {
450                throw FlexExceptions.wrap(LocalizedFormats.ENTITY_VERSION_NULL, entity);
451            }
452            sql.append(AND).append(wrap(versionColumn)).append(EQUALS).append(versionValue);
453        }
454
455        prepareAuth(tableInfo, sql, OperateType.UPDATE);
456        return sql.toString();
457    }
458
459    @Override
460    public String forUpdateEntityByQuery(TableInfo tableInfo, Object entity, boolean ignoreNulls, QueryWrapper queryWrapper) {
461        //eg: ALTER TABLE test  UPDATE DR = ?  WHERE CUSERID = ?
462        prepareAuth(queryWrapper, OperateType.UPDATE);
463        StringBuilder sqlBuilder = new StringBuilder();
464
465        Set<String> updateColumns = tableInfo.obtainUpdateColumns(entity, ignoreNulls, true);
466        Map<String, RawValue> rawValueMap = tableInfo.obtainUpdateRawValueMap(entity);
467
468        sqlBuilder.append(ALTER_TABLE).append(forHint(CPI.getHint(queryWrapper)));
469        sqlBuilder.append(tableInfo.getWrapSchemaAndTableName(this, OperateType.UPDATE));
470
471        List<QueryTable> queryTables = CPI.getQueryTables(queryWrapper);
472        buildJoinSql(sqlBuilder, queryWrapper, queryTables, OperateType.UPDATE);
473
474        sqlBuilder.append(CK_UPDATE);
475
476        StringJoiner stringJoiner = new StringJoiner(DELIMITER);
477
478        for (String modifyAttr : updateColumns) {
479            if (rawValueMap.containsKey(modifyAttr)) {
480                stringJoiner.add(wrap(modifyAttr) + EQUALS + rawValueMap.get(modifyAttr).toSql(this));
481            } else {
482                stringJoiner.add(wrap(modifyAttr) + EQUALS_PLACEHOLDER);
483            }
484        }
485
486
487        Map<String, String> onUpdateColumns = tableInfo.getOnUpdateColumns();
488        if (onUpdateColumns != null && !onUpdateColumns.isEmpty()) {
489            onUpdateColumns.forEach((column, value) -> stringJoiner.add(wrap(column) + EQUALS + value));
490        }
491
492        // 乐观锁字段
493        String versionColumn = tableInfo.getVersionColumn();
494        if (StringUtil.isNotBlank(versionColumn)) {
495            stringJoiner.add(wrap(versionColumn) + EQUALS + wrap(versionColumn) + " + 1 ");
496        }
497
498        sqlBuilder.append(stringJoiner);
499
500
501        buildWhereSql(sqlBuilder, queryWrapper, queryTables, false);
502        buildGroupBySql(sqlBuilder, queryWrapper, queryTables);
503        buildHavingSql(sqlBuilder, queryWrapper, queryTables);
504
505        // ignore orderBy and limit
506        buildOrderBySql(sqlBuilder, queryWrapper, queryTables);
507
508        Long limitRows = CPI.getLimitRows(queryWrapper);
509        Long limitOffset = CPI.getLimitOffset(queryWrapper);
510        if (limitRows != null || limitOffset != null) {
511            sqlBuilder = buildLimitOffsetSql(sqlBuilder, queryWrapper, limitRows, limitOffset);
512        }
513
514
515        List<String> endFragments = CPI.getEndFragments(queryWrapper);
516        if (CollectionUtil.isNotEmpty(endFragments)) {
517            for (String endFragment : endFragments) {
518                sqlBuilder.append(BLANK).append(endFragment);
519            }
520        }
521
522        return sqlBuilder.toString();
523    }
524}