@Override
    public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency,
            int resultSetHoldability) throws SQLException {
        ensureOpen();

        ClickHouseConfig config = clientRequest.getConfig();
        // TODO remove the extra parsing
        ClickHouseSqlStatement[] stmts = parse(sql, config, clientRequest.getSettings());
        if (stmts.length != 1) {
            throw SqlExceptionUtils
                    .clientError("Prepared statement only supports one query but we got: " + stmts.length);
        }
        ClickHouseSqlStatement parsedStmt = stmts[0];

        ClickHouseParameterizedQuery preparedQuery;
        try {
            preparedQuery = jdbcConf.useNamedParameter()
                    ? ClickHouseParameterizedQuery.of(clientRequest.getConfig(), parsedStmt.getSQL())
                    : JdbcParameterizedQuery.of(config, parsedStmt.getSQL());
        } catch (RuntimeException e) {
            throw SqlExceptionUtils.clientError(e);
        }

        PreparedStatement ps = null;
        if (preparedQuery.hasParameter()) {
            if (parsedStmt.hasTempTable() || parsedStmt.hasInput()) {
                throw SqlExceptionUtils
                        .clientError(
                                "External table, input function, and query parameter cannot be used together in PreparedStatement.");
            } else if (parsedStmt.getStatementType() == StatementType.INSERT &&
                    !parsedStmt.containsKeyword("SELECT") && parsedStmt.hasValues() &&
                    (!parsedStmt.hasFormat() || clientRequest.getFormat().name().equals(parsedStmt.getFormat()))) {
                String query = parsedStmt.getSQL();
                boolean useStream = false;
                Integer startIndex = parsedStmt.getPositions().get(ClickHouseSqlStatement.KEYWORD_VALUES_START);
                if (startIndex != null) {
                    useStream = true;
                    int endIndex = parsedStmt.getPositions().get(ClickHouseSqlStatement.KEYWORD_VALUES_END);
                    for (int i = startIndex + 1; i < endIndex; i++) {
                        char ch = query.charAt(i);
                        if (ch != '?' && ch != ',' && !Character.isWhitespace(ch)) {
                            useStream = false;
                            break;
                        }
                    }
                }

                if (useStream) {
                    ps = new InputBasedPreparedStatement(this,
                            clientRequest.write().query(query.substring(0, parsedStmt.getStartPosition("VALUES")),
                                    newQueryId()),
                            getTableColumns(parsedStmt.getDatabase(), parsedStmt.getTable(),
                                    parsedStmt.getContentBetweenKeywords(
                                            ClickHouseSqlStatement.KEYWORD_TABLE_COLUMNS_START,
                                            ClickHouseSqlStatement.KEYWORD_TABLE_COLUMNS_END)),
                            resultSetType, resultSetConcurrency, resultSetHoldability);
                }
            }
        } else {
            if (parsedStmt.hasTempTable()) {
                // queries using external/temporary table
                ps = new TableBasedPreparedStatement(this,
                        clientRequest.copy().query(parsedStmt.getSQL(), newQueryId()), parsedStmt,
                        resultSetType, resultSetConcurrency, resultSetHoldability);
            } else if (parsedStmt.getStatementType() == StatementType.INSERT) {
                if (!ClickHouseChecker.isNullOrBlank(parsedStmt.getInput())) {
                    // an ugly workaround of https://github.com/ClickHouse/ClickHouse/issues/39866
                    // would be replace JSON and Object('json') types in the query to String

                    Mutation m = clientRequest.write();
                    if (parsedStmt.hasFormat()) {
                        m.format(ClickHouseFormat.valueOf(parsedStmt.getFormat()));
                    }
                    // insert query using input function
                    ps = new InputBasedPreparedStatement(this, m.query(parsedStmt.getSQL(), newQueryId()),
                            ClickHouseColumn.parse(parsedStmt.getInput()), resultSetType, resultSetConcurrency,
                            resultSetHoldability);
                } else if (!parsedStmt.containsKeyword("SELECT") && !parsedStmt.hasValues()) {
                    ps = parsedStmt.hasFormat()
                            ? new StreamBasedPreparedStatement(this,
                                    clientRequest.write().query(parsedStmt.getSQL(), newQueryId()), parsedStmt,
                                    resultSetType, resultSetConcurrency, resultSetHoldability)
                            : new InputBasedPreparedStatement(this,
                                    clientRequest.write().query(parsedStmt.getSQL(), newQueryId()),
                                    getTableColumns(parsedStmt.getDatabase(), parsedStmt.getTable(),
                                            parsedStmt.getContentBetweenKeywords(
                                                    ClickHouseSqlStatement.KEYWORD_TABLE_COLUMNS_START,
                                                    ClickHouseSqlStatement.KEYWORD_TABLE_COLUMNS_END)),
                                    resultSetType, resultSetConcurrency, resultSetHoldability);
                }
            }
        }

        return ps != null ? ps
                : new SqlBasedPreparedStatement(this, clientRequest.copy().query(preparedQuery, newQueryId()),
                        stmts[0], resultSetType, resultSetConcurrency, resultSetHoldability);
    }
Checks if a feature can be supported or not.

@param feature non-empty feature name
@param silent  whether to show warning in log or throw unsupported exception
@throws SQLException when the feature is not supported and silent is
                     {@code false}
clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/internal/ClickHouseConnectionImpl.java