private TableSchema(
            List<TableColumn> columns,
            List<WatermarkSpec> watermarkSpecs,
            @Nullable UniqueConstraint primaryKey) {
        this.columns = Preconditions.checkNotNull(columns);
        this.watermarkSpecs = Preconditions.checkNotNull(watermarkSpecs);
        this.primaryKey = primaryKey;
    }
@Test
    void testTableSchema() {
        TableSchema schema =
                TableSchema.builder()
                        .add(TableColumn.physical("f0", DataTypes.BIGINT()))
                        .add(
                                TableColumn.physical(
                                        "f1",
                                        DataTypes.ROW(
                                                DataTypes.FIELD("q1", DataTypes.STRING()),
                                                DataTypes.FIELD("q2", DataTypes.TIMESTAMP(3)))))
                        .add(TableColumn.physical("f2", DataTypes.STRING()))
                        .add(TableColumn.computed("f3", DataTypes.BIGINT(), "f0 + 1"))
                        .add(TableColumn.metadata("f4", DataTypes.BIGINT(), "other.key", true))
                        .watermark("f1.q2", WATERMARK_EXPRESSION, WATERMARK_DATATYPE)
                        .build();

        // test toString()
        String expected =
                "root\n"
                        + " |-- f0: BIGINT\n"
                        + " |-- f1: ROW<`q1` STRING, `q2` TIMESTAMP(3)>\n"
                        + " |-- f2: STRING\n"
                        + " |-- f3: BIGINT AS f0 + 1\n"
                        + " |-- f4: BIGINT METADATA FROM 'other.key' VIRTUAL\n"
                        + " |-- WATERMARK FOR f1.q2: TIMESTAMP(3) AS localtimestamp\n";
        assertThat(schema.toString()).isEqualTo(expected);

        // test getFieldNames and getFieldDataType
        assertThat(schema.getFieldName(2)).isEqualTo(Optional.of("f2"));
        assertThat(schema.getFieldDataType(3)).isEqualTo(Optional.of(DataTypes.BIGINT()));
        assertThat(schema.getTableColumn(3))
                .isEqualTo(Optional.of(TableColumn.computed("f3", DataTypes.BIGINT(), "f0 + 1")));
        assertThat(schema.getFieldDataType("f2")).isEqualTo(Optional.of(DataTypes.STRING()));
        assertThat(schema.getFieldDataType("f1").map(r -> r.getChildren().get(0)))
                .isEqualTo(Optional.of(DataTypes.STRING()));
        assertThat(schema.getFieldName(5)).isNotPresent();
        assertThat(schema.getFieldType(-1)).isNotPresent();
        assertThat(schema.getFieldType("c")).isNotPresent();
        assertThat(schema.getFieldDataType("f1.q1")).isNotPresent();
        assertThat(schema.getFieldDataType("f1.q3")).isNotPresent();

        // test copy() and equals()
        assertThat(schema.copy()).isEqualTo(schema);
        assertThat(schema.copy().hashCode()).isEqualTo(schema.hashCode());
    }