From 10f2713c2f1f90205dfa6451ac9e60ed29ae9ec6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 23:11:00 +0000 Subject: [PATCH 1/2] Support the MySQL spatial types, SRID attribute, and SPATIAL table constraint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the spatial pieces of the MySQL type system, which postdate the goyacc grammar (TiDB never supported spatial data): - The spatial column data types (MySQL 26.7 §13.4.1): GEOMETRY, POINT, LINESTRING, POLYGON, MULTIPOINT, MULTILINESTRING, MULTIPOLYGON, and GEOMETRYCOLLECTION, with GEOMCOLLECTION lexing as its GEOMETRYCOLLECTION synonym via the aliases table (the DEC → DECIMAL precedent). MySQL reports every spatial column as type byte 0xff on the wire, so the subtypes get parser-only type bytes 0xe2–0xe8 following the TypeTiDBVectorFloat32 precedent; spatial FieldTypes carry the binary charset, like JSON. - The SRID column attribute (§13.1.20.10): new ColumnOptionSrid with the value in a new ColumnOption.UintValue field. - The SPATIAL [INDEX | KEY] table constraint (§15.1.20) in CREATE TABLE and ALTER TABLE ... ADD, mirroring the FULLTEXT alternative with a new ConstraintSpatial that restores canonically as SPATIAL; CREATE SPATIAL INDEX already parsed. - CAST/CONVERT to the spatial types (§14.10, castable since MySQL 8.0.24), with RestoreAsCastType coverage. The ast and types changes are backwards-compatible: appended enum values, a new ColumnOption field, and new type-byte map entries. Keyword tables: GEOMETRY, GEOMETRYCOLLECTION, LINESTRING, MULTILINESTRING, MULTIPOINT, MULTIPOLYGON, POLYGON, and SRID become unreserved keywords, matching their MySQL 26.7 classification (POINT already was); TestKeywordsLength counts updated accordingly. testdata/errors.json is unaffected. The new file-driven test group spatial_types covers the types, SRID, the constraint forms, casts, and the new keywords in identifier position. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01C3dMfdpMyaWffPoh8RBia4 --- ast/ddl.go | 10 ++ mysql/type.go | 11 ++ parser/keyword_classes.go | 8 ++ parser/keywords.go | 8 ++ parser/keywords_test.go | 4 +- parser/misc.go | 18 ++- parser/parse_alter.go | 2 +- parser/parse_column.go | 37 ++++- parser/parse_create_table.go | 2 +- parser/parse_func.go | 11 ++ parser/parse_types.go | 37 +++++ .../testdata/parser/spatial_types/input.sql | 51 +++++++ .../testdata/parser/spatial_types/output.sql | 51 +++++++ parser/token_kinds.go | 8 ++ types/etc.go | 126 ++++++++++-------- types/field_type.go | 4 + 16 files changed, 323 insertions(+), 65 deletions(-) create mode 100644 parser/testdata/parser/spatial_types/input.sql create mode 100644 parser/testdata/parser/spatial_types/output.sql diff --git a/ast/ddl.go b/ast/ddl.go index ab75ccb..12f668b 100644 --- a/ast/ddl.go +++ b/ast/ddl.go @@ -527,6 +527,7 @@ const ( ColumnOptionStorage ColumnOptionAutoRandom ColumnOptionSecondaryEngineAttribute + ColumnOptionSrid ) var ( @@ -564,6 +565,8 @@ type ColumnOption struct { ConstraintName string PrimaryKeyTp PrimaryKeyType SecondaryEngineAttr string + // UintValue is only for ColumnOptionSrid. + UintValue uint64 } // Restore implements Node interface. @@ -686,6 +689,9 @@ func (n *ColumnOption) Restore(ctx *format.RestoreCtx) error { ctx.WriteKeyWord("SECONDARY_ENGINE_ATTRIBUTE") ctx.WritePlain(" = ") ctx.WriteString(n.StrValue) + case ColumnOptionSrid: + ctx.WriteKeyWord("SRID ") + ctx.WritePlainf("%d", n.UintValue) default: return errors.New("An error occurred while splicing ColumnOption") } @@ -933,6 +939,8 @@ const ( // It will be rewritten into ConstraintColumnar after preprocessor phase. ConstraintVector ConstraintColumnar + // ConstraintSpatial is only used in AST. + ConstraintSpatial ) // Constraint is constraint for table definition. @@ -987,6 +995,8 @@ func (n *Constraint) Restore(ctx *format.RestoreCtx) error { ctx.WriteKeyWord("UNIQUE INDEX") case ConstraintFulltext: ctx.WriteKeyWord("FULLTEXT") + case ConstraintSpatial: + ctx.WriteKeyWord("SPATIAL") case ConstraintCheck: if n.Name != "" { ctx.WriteKeyWord("CONSTRAINT ") diff --git a/mysql/type.go b/mysql/type.go index 6f1ace0..2158626 100644 --- a/mysql/type.go +++ b/mysql/type.go @@ -47,6 +47,17 @@ const ( TypeGeometry byte = 0xff TypeTiDBVectorFloat32 byte = 0xe1 + + // MySQL reports every spatial column as TypeGeometry on the wire; these + // bytes exist only so the parser can keep the declared spatial subtype + // (following the TypeTiDBVectorFloat32 precedent for non-wire types). + TypePoint byte = 0xe2 + TypeLineString byte = 0xe3 + TypePolygon byte = 0xe4 + TypeMultiPoint byte = 0xe5 + TypeMultiLineString byte = 0xe6 + TypeMultiPolygon byte = 0xe7 + TypeGeometryCollection byte = 0xe8 ) // Flag information. diff --git a/parser/keyword_classes.go b/parser/keyword_classes.go index 9e3e08f..1a993dc 100644 --- a/parser/keyword_classes.go +++ b/parser/keyword_classes.go @@ -122,6 +122,14 @@ var unReservedKeywordNames = []string{ "STATUS", "OPEN", "POINT", + "GEOMETRY", + "GEOMETRYCOLLECTION", + "LINESTRING", + "MULTILINESTRING", + "MULTIPOINT", + "MULTIPOLYGON", + "POLYGON", + "SRID", "SUBPARTITIONS", "SUBPARTITION", "TABLES", diff --git a/parser/keywords.go b/parser/keywords.go index 25f3b14..b0f38c5 100644 --- a/parser/keywords.go +++ b/parser/keywords.go @@ -429,6 +429,8 @@ var Keywords = []KeywordsType{ {"FULL", false, "unreserved"}, {"FUNCTION", false, "unreserved"}, {"GENERAL", false, "unreserved"}, + {"GEOMETRY", false, "unreserved"}, + {"GEOMETRYCOLLECTION", false, "unreserved"}, {"GLOBAL", false, "unreserved"}, {"GRANTS", false, "unreserved"}, {"GROUP_REPLICATION", false, "unreserved"}, @@ -471,6 +473,7 @@ var Keywords = []KeywordsType{ {"LESS", false, "unreserved"}, {"LEVEL", false, "unreserved"}, {"LIBRARY", false, "unreserved"}, + {"LINESTRING", false, "unreserved"}, {"LIST", false, "unreserved"}, {"LOAD_STATS", false, "unreserved"}, {"LOCAL", false, "unreserved"}, @@ -500,6 +503,9 @@ var Keywords = []KeywordsType{ {"MODE", false, "unreserved"}, {"MODIFY", false, "unreserved"}, {"MONTH", false, "unreserved"}, + {"MULTILINESTRING", false, "unreserved"}, + {"MULTIPOINT", false, "unreserved"}, + {"MULTIPOLYGON", false, "unreserved"}, {"MUTEX", false, "unreserved"}, {"NAMES", false, "unreserved"}, {"NATIONAL", false, "unreserved"}, @@ -553,6 +559,7 @@ var Keywords = []KeywordsType{ {"PLUGINS", false, "unreserved"}, {"POINT", false, "unreserved"}, {"POLICY", false, "unreserved"}, + {"POLYGON", false, "unreserved"}, {"PRECEDES", false, "unreserved"}, {"PRECEDING", false, "unreserved"}, {"PREPARE", false, "unreserved"}, @@ -647,6 +654,7 @@ var Keywords = []KeywordsType{ {"SQL_TSI_SECOND", false, "unreserved"}, {"SQL_TSI_WEEK", false, "unreserved"}, {"SQL_TSI_YEAR", false, "unreserved"}, + {"SRID", false, "unreserved"}, {"STACKED", false, "unreserved"}, {"START", false, "unreserved"}, {"STARTS", false, "unreserved"}, diff --git a/parser/keywords_test.go b/parser/keywords_test.go index a5f4b4f..81fbaf6 100644 --- a/parser/keywords_test.go +++ b/parser/keywords_test.go @@ -43,8 +43,8 @@ func TestKeywords(t *testing.T) { } func TestKeywordsLength(t *testing.T) { - if !reflect.DeepEqual(751, len(parser.Keywords)) { - t.Fatalf("got %v, want %v", len(parser.Keywords), 751) + if !reflect.DeepEqual(759, len(parser.Keywords)) { + t.Fatalf("got %v, want %v", len(parser.Keywords), 759) } reservedNr := 0 diff --git a/parser/misc.go b/parser/misc.go index d01329a..646af7d 100644 --- a/parser/misc.go +++ b/parser/misc.go @@ -450,6 +450,9 @@ var tokenMap = map[string]int{ "GC_TTL": gcTTL, "GENERAL": general, "GENERATED": generated, + "GEOMCOLLECTION": geometryCollectionType, + "GEOMETRY": geometryType, + "GEOMETRYCOLLECTION": geometryCollectionType, "GET": get, "GET_FORMAT": getFormat, "GLOBAL": global, @@ -557,6 +560,7 @@ var tokenMap = map[string]int{ "LITE": lite, "LINEAR": linear, "LINES": lines, + "LINESTRING": linestringType, "LIST": list, "LOAD": load, "LOCAL": local, @@ -608,6 +612,9 @@ var tokenMap = map[string]int{ "MODIFY": modify, "MONITOR": monitor, "MONTH": month, + "MULTILINESTRING": multilinestringType, + "MULTIPOINT": multipointType, + "MULTIPOLYGON": multipolygonType, "MUTEX": mutex, "NAMES": names, "NATIONAL": national, @@ -688,6 +695,7 @@ var tokenMap = map[string]int{ "POINT": point, "POLICIES": policies, "POLICY": policy, + "POLYGON": polygonType, "POSITION": position, "PRE_SPLIT_REGIONS": preSplitRegions, "PRECEDES": precedes, @@ -847,6 +855,7 @@ var tokenMap = map[string]int{ "SQLEXCEPTION": sqlexception, "SQLSTATE": sqlstate, "SQLWARNING": sqlwarning, + "SRID": srid, "SSL": ssl, "STACKED": stacked, "STALENESS": staleness, @@ -1087,10 +1096,11 @@ var windowFuncTokenMap = map[string]int{ // aliases are strings directly map to another string and use the same token. var aliases = map[string]string{ - "SCHEMA": "DATABASE", - "SCHEMAS": "DATABASES", - "DEC": "DECIMAL", - "SUBSTR": "SUBSTRING", + "SCHEMA": "DATABASE", + "SCHEMAS": "DATABASES", + "DEC": "DECIMAL", + "GEOMCOLLECTION": "GEOMETRYCOLLECTION", + "SUBSTR": "SUBSTRING", } // hintedTokens is a set of tokens which recognizes a hint. diff --git a/parser/parse_alter.go b/parser/parse_alter.go index 70f175a..256d63d 100644 --- a/parser/parse_alter.go +++ b/parser/parse_alter.go @@ -732,7 +732,7 @@ func (r *rdParser) parseAlterTableSpec() *ast.AlterTableSpec { func (r *rdParser) parseAlterTableSpecAdd() *ast.AlterTableSpec { r.expect(add) switch r.tok() { - case constraint, primary, unique, fulltext, foreign, check, key, index: + case constraint, primary, unique, fulltext, spatial, foreign, check, key, index: // "ADD" ConstraintWithColumnarIndex (the Constraint alternative) return &ast.AlterTableSpec{ Tp: ast.AlterTableAddConstraint, diff --git a/parser/parse_column.go b/parser/parse_column.go index cf22b24..9565502 100644 --- a/parser/parse_column.go +++ b/parser/parse_column.go @@ -58,7 +58,8 @@ func (r *rdParser) isColumnOptionStart() bool { switch r.tok() { case not, not2, null, autoIncrement, primary, key, unique, defaultKwd, serial, on, comment, check, constraint, generated, as, references, - collate, columnFormat, storage, autoRandom, secondaryEngineAttribute: + collate, columnFormat, storage, autoRandom, secondaryEngineAttribute, + srid: return true } return false @@ -262,6 +263,14 @@ func (r *rdParser) parseColumnOption() interface{} { Tp: ast.ColumnOptionSecondaryEngineAttribute, StrValue: r.expect(stringLit).lit, } + case srid: + // ColumnOption: "SRID" LengthNum — the spatial column attribute + // (MySQL 26.7 §13.1.20.10); postdates the goyacc grammar. + r.advance() + return &ast.ColumnOption{ + Tp: ast.ColumnOptionSrid, + UintValue: r.parseLengthNum(), + } } r.syntaxError() return nil @@ -748,6 +757,32 @@ func (r *rdParser) parseConstraintElem() *ast.Constraint { c.Option = &ast.IndexOption{} } return c + case spatial: + // "SPATIAL" KeyOrIndexOpt IndexName '(' IndexPartSpecificationList ')' IndexOptionList + // — the MySQL 26.7 §15.1.20 table-constraint form, which postdates + // the goyacc grammar (it only had CREATE SPATIAL INDEX). Mirrors + // the FULLTEXT alternative. + r.advance() + if r.tok() == key || r.tok() == index { + r.advance() + } + name := r.parseIndexName() + r.expect(int('(')) + keys := r.parseIndexPartSpecificationList() + r.expect(int(')')) + option := r.parseIndexOptionList() + c := &ast.Constraint{ + Tp: ast.ConstraintSpatial, + Keys: keys, + Name: name.String, + IsEmptyIndex: name.Empty, + } + if option != nil { + c.Option = option + } else { + c.Option = &ast.IndexOption{} + } + return c case key, index: // KeyOrIndex IfNotExists IndexNameAndTypeOpt '(' IndexPartSpecificationList ')' IndexOptionList r.advance() diff --git a/parser/parse_create_table.go b/parser/parse_create_table.go index c01122a..017a861 100644 --- a/parser/parse_create_table.go +++ b/parser/parse_create_table.go @@ -331,7 +331,7 @@ func (r *rdParser) parseTableElementList() []interface{} { // ConstraintWithColumnarIndex. func (r *rdParser) parseTableElement() interface{} { switch r.tok() { - case constraint, primary, unique, fulltext, foreign, check, key, index: + case constraint, primary, unique, fulltext, spatial, foreign, check, key, index: // Constraint: ConstraintKeywordOpt ConstraintElem return r.parseConstraint() case vectorType, columnar: diff --git a/parser/parse_func.go b/parser/parse_func.go index 8051151..97b4a8e 100644 --- a/parser/parse_func.go +++ b/parser/parse_func.go @@ -1428,6 +1428,17 @@ func (r *rdParser) parseCastType() *types.FieldType { tp.SetCharset(charset.CharsetBin) tp.SetCollate(charset.CollationBin) return tp + case geometryType, point, linestringType, polygonType, multipointType, + multilinestringType, multipolygonType, geometryCollectionType: + // CastType: the spatial types — castable since MySQL 8.0.24 + // (MySQL 26.7 §14.10); postdates the goyacc grammar. + b := spatialTypeByte(r.tok()) + r.advance() + tp := types.NewFieldType(b) + tp.SetCharset(charset.CharsetBin) + tp.SetCollate(charset.CollationBin) + tp.AddFlag(mysql.BinaryFlag) + return tp case vectorType: // "VECTOR" OptVectorElementType OptFieldLen r.advance() diff --git a/parser/parse_types.go b/parser/parse_types.go index 30f2ca5..fb31baa 100644 --- a/parser/parse_types.go +++ b/parser/parse_types.go @@ -320,6 +320,20 @@ func (r *rdParser) parseStringOrDateAndTimeType() *types.FieldType { tp.SetCharset(charset.CharsetBin) tp.SetCollate(charset.CollationBin) return tp + case geometryType, point, linestringType, polygonType, multipointType, + multilinestringType, multipolygonType, geometryCollectionType: + // SpatialType: "GEOMETRY" | "POINT" | "LINESTRING" | "POLYGON" + // | "MULTIPOINT" | "MULTILINESTRING" | "MULTIPOLYGON" + // | "GEOMETRYCOLLECTION" — MySQL 26.7 §13.4.1. The spatial types + // postdate the goyacc grammar; GEOMCOLLECTION lexes as its + // GEOMETRYCOLLECTION synonym. Spatial values are stored in a + // binary format, like JSON. + b := spatialTypeByte(r.tok()) + r.advance() + tp := types.NewFieldType(b) + tp.SetCharset(charset.CharsetBin) + tp.SetCollate(charset.CollationBin) + return tp case long: return r.parseLongType() case vectorType: @@ -390,6 +404,29 @@ func (r *rdParser) parseStringOrDateAndTimeType() *types.FieldType { return nil } +// spatialTypeByte maps a spatial type keyword token to its type byte. +func spatialTypeByte(tok int) byte { + switch tok { + case geometryType: + return mysql.TypeGeometry + case point: + return mysql.TypePoint + case linestringType: + return mysql.TypeLineString + case polygonType: + return mysql.TypePolygon + case multipointType: + return mysql.TypeMultiPoint + case multilinestringType: + return mysql.TypeMultiLineString + case multipolygonType: + return mysql.TypeMultiPolygon + case geometryCollectionType: + return mysql.TypeGeometryCollection + } + return 0 +} + // parseCharTail finishes `Char/NChar FieldLen OptBinary` and // `Char/NChar OptBinary` after the type keyword(s) have been consumed. func (r *rdParser) parseCharTail() *types.FieldType { diff --git a/parser/testdata/parser/spatial_types/input.sql b/parser/testdata/parser/spatial_types/input.sql new file mode 100644 index 0000000..af755df --- /dev/null +++ b/parser/testdata/parser/spatial_types/input.sql @@ -0,0 +1,51 @@ +CREATE TABLE t (g GEOMETRY) +-- case +CREATE TABLE t (pt POINT) +-- case +CREATE TABLE t (ls LINESTRING) +-- case +CREATE TABLE t (pg POLYGON) +-- case +CREATE TABLE t (mpt MULTIPOINT) +-- case +CREATE TABLE t (mls MULTILINESTRING) +-- case +CREATE TABLE t (mpg MULTIPOLYGON) +-- case +CREATE TABLE t (gc GEOMETRYCOLLECTION) +-- case +CREATE TABLE t (gc GEOMCOLLECTION) +-- case +CREATE TABLE t (g GEOMETRY NOT NULL) +-- case +CREATE TABLE t (g GEOMETRY NOT NULL SRID 4326) +-- case +CREATE TABLE t (pt POINT SRID 0) +-- case +CREATE TABLE t (g GEOMETRY NOT NULL SRID 4326, SPATIAL INDEX idx_g (g)) +-- case +CREATE TABLE t (g GEOMETRY NOT NULL, SPATIAL KEY (g)) +-- case +CREATE TABLE t (g GEOMETRY NOT NULL, SPATIAL (g)) +-- case +ALTER TABLE t ADD SPATIAL INDEX idx_g (g) +-- case +CREATE SPATIAL INDEX idx_g ON t (g) +-- case +CREATE TABLE geom (g GEOMETRY, p POINT NOT NULL SRID 4326, ll LINESTRING COMMENT 'path') +-- case +ALTER TABLE t ADD COLUMN g GEOMETRY NOT NULL SRID 4326 +-- case +ALTER TABLE t MODIFY COLUMN pt POINT SRID 4326 +-- case +CREATE TABLE t (geometry INT, point INT, linestring INT, polygon INT, multipoint INT, multilinestring INT, multipolygon INT, geometrycollection INT, srid INT) +-- case +SELECT geometry, point, srid FROM polygon +-- case +CREATE TABLE t (g GEOMETRY SRID 4326 COMMENT 'geo column' NOT NULL) +-- case +SELECT CAST(g AS POINT) FROM t +-- case +SELECT CAST(x AS GEOMETRY), CAST(x AS GEOMETRYCOLLECTION), CAST(x AS GEOMCOLLECTION) FROM t +-- case +SELECT CONVERT(x, MULTIPOLYGON), CONVERT(x, LINESTRING) FROM t diff --git a/parser/testdata/parser/spatial_types/output.sql b/parser/testdata/parser/spatial_types/output.sql new file mode 100644 index 0000000..8ccb9f4 --- /dev/null +++ b/parser/testdata/parser/spatial_types/output.sql @@ -0,0 +1,51 @@ +CREATE TABLE `t` (`g` GEOMETRY) +-- case +CREATE TABLE `t` (`pt` POINT) +-- case +CREATE TABLE `t` (`ls` LINESTRING) +-- case +CREATE TABLE `t` (`pg` POLYGON) +-- case +CREATE TABLE `t` (`mpt` MULTIPOINT) +-- case +CREATE TABLE `t` (`mls` MULTILINESTRING) +-- case +CREATE TABLE `t` (`mpg` MULTIPOLYGON) +-- case +CREATE TABLE `t` (`gc` GEOMETRYCOLLECTION) +-- case +CREATE TABLE `t` (`gc` GEOMETRYCOLLECTION) +-- case +CREATE TABLE `t` (`g` GEOMETRY NOT NULL) +-- case +CREATE TABLE `t` (`g` GEOMETRY NOT NULL SRID 4326) +-- case +CREATE TABLE `t` (`pt` POINT SRID 0) +-- case +CREATE TABLE `t` (`g` GEOMETRY NOT NULL SRID 4326,SPATIAL `idx_g`(`g`)) +-- case +CREATE TABLE `t` (`g` GEOMETRY NOT NULL,SPATIAL(`g`)) +-- case +CREATE TABLE `t` (`g` GEOMETRY NOT NULL,SPATIAL(`g`)) +-- case +ALTER TABLE `t` ADD SPATIAL `idx_g`(`g`) +-- case +CREATE SPATIAL INDEX `idx_g` ON `t` (`g`) +-- case +CREATE TABLE `geom` (`g` GEOMETRY,`p` POINT NOT NULL SRID 4326,`ll` LINESTRING COMMENT 'path') +-- case +ALTER TABLE `t` ADD COLUMN `g` GEOMETRY NOT NULL SRID 4326 +-- case +ALTER TABLE `t` MODIFY COLUMN `pt` POINT SRID 4326 +-- case +CREATE TABLE `t` (`geometry` INT,`point` INT,`linestring` INT,`polygon` INT,`multipoint` INT,`multilinestring` INT,`multipolygon` INT,`geometrycollection` INT,`srid` INT) +-- case +SELECT `geometry`,`point`,`srid` FROM `polygon` +-- case +CREATE TABLE `t` (`g` GEOMETRY SRID 4326 COMMENT 'geo column' NOT NULL) +-- case +SELECT CAST(`g` AS POINT) FROM `t` +-- case +SELECT CAST(`x` AS GEOMETRY),CAST(`x` AS GEOMETRYCOLLECTION),CAST(`x` AS GEOMETRYCOLLECTION) FROM `t` +-- case +SELECT CONVERT(`x`, MULTIPOLYGON),CONVERT(`x`, LINESTRING) FROM `t` diff --git a/parser/token_kinds.go b/parser/token_kinds.go index df0ff3a..a6b7bfe 100644 --- a/parser/token_kinds.go +++ b/parser/token_kinds.go @@ -379,6 +379,8 @@ const ( ge = 58209 general = 57733 generated = 57436 + geometryCollectionType = 58320 + geometryType = 58321 get = 58254 getFormat = 58030 global = 57734 @@ -500,6 +502,7 @@ const ( limit = 57478 linear = 57479 lines = 57480 + linestringType = 58322 list = 57769 lite = 58171 load = 57481 @@ -577,6 +580,9 @@ const ( modify = 57793 monitor = 57794 month = 57795 + multilinestringType = 58323 + multipointType = 58324 + multipolygonType = 58325 mutex = 58277 names = 57796 national = 57797 @@ -674,6 +680,7 @@ const ( point = 57840 policies = 58177 policy = 57841 + polygonType = 58326 position = 58060 preSplitRegions = 57845 precedes = 58311 @@ -837,6 +844,7 @@ const ( sqlexception = 57547 sqlstate = 57548 sqlwarning = 57549 + srid = 58327 ssl = 57553 stacked = 58257 staleness = 58078 diff --git a/types/etc.go b/types/etc.go index 109cd9a..e3f6247 100644 --- a/types/etc.go +++ b/types/etc.go @@ -46,65 +46,79 @@ func IsTypeVector(tp byte) bool { } var type2Str = map[byte]string{ - mysql.TypeBit: "bit", - mysql.TypeBlob: "text", - mysql.TypeDate: "date", - mysql.TypeDatetime: "datetime", - mysql.TypeUnspecified: "unspecified", - mysql.TypeNewDecimal: "decimal", - mysql.TypeDouble: "double", - mysql.TypeEnum: "enum", - mysql.TypeFloat: "float", - mysql.TypeGeometry: "geometry", - mysql.TypeTiDBVectorFloat32: "vector", - mysql.TypeInt24: "mediumint", - mysql.TypeJSON: "json", - mysql.TypeLong: "int", - mysql.TypeLonglong: "bigint", - mysql.TypeLongBlob: "longtext", - mysql.TypeMediumBlob: "mediumtext", - mysql.TypeNull: "null", - mysql.TypeSet: "set", - mysql.TypeShort: "smallint", - mysql.TypeString: "char", - mysql.TypeDuration: "time", - mysql.TypeTimestamp: "timestamp", - mysql.TypeTiny: "tinyint", - mysql.TypeTinyBlob: "tinytext", - mysql.TypeVarchar: "varchar", - mysql.TypeVarString: "var_string", - mysql.TypeYear: "year", + mysql.TypeBit: "bit", + mysql.TypeBlob: "text", + mysql.TypeDate: "date", + mysql.TypeDatetime: "datetime", + mysql.TypeUnspecified: "unspecified", + mysql.TypeNewDecimal: "decimal", + mysql.TypeDouble: "double", + mysql.TypeEnum: "enum", + mysql.TypeFloat: "float", + mysql.TypeGeometry: "geometry", + mysql.TypePoint: "point", + mysql.TypeLineString: "linestring", + mysql.TypePolygon: "polygon", + mysql.TypeMultiPoint: "multipoint", + mysql.TypeMultiLineString: "multilinestring", + mysql.TypeMultiPolygon: "multipolygon", + mysql.TypeGeometryCollection: "geometrycollection", + mysql.TypeTiDBVectorFloat32: "vector", + mysql.TypeInt24: "mediumint", + mysql.TypeJSON: "json", + mysql.TypeLong: "int", + mysql.TypeLonglong: "bigint", + mysql.TypeLongBlob: "longtext", + mysql.TypeMediumBlob: "mediumtext", + mysql.TypeNull: "null", + mysql.TypeSet: "set", + mysql.TypeShort: "smallint", + mysql.TypeString: "char", + mysql.TypeDuration: "time", + mysql.TypeTimestamp: "timestamp", + mysql.TypeTiny: "tinyint", + mysql.TypeTinyBlob: "tinytext", + mysql.TypeVarchar: "varchar", + mysql.TypeVarString: "var_string", + mysql.TypeYear: "year", } var str2Type = map[string]byte{ - "bit": mysql.TypeBit, - "text": mysql.TypeBlob, - "date": mysql.TypeDate, - "datetime": mysql.TypeDatetime, - "unspecified": mysql.TypeUnspecified, - "decimal": mysql.TypeNewDecimal, - "double": mysql.TypeDouble, - "enum": mysql.TypeEnum, - "float": mysql.TypeFloat, - "geometry": mysql.TypeGeometry, - "vector": mysql.TypeTiDBVectorFloat32, - "mediumint": mysql.TypeInt24, - "json": mysql.TypeJSON, - "int": mysql.TypeLong, - "bigint": mysql.TypeLonglong, - "longtext": mysql.TypeLongBlob, - "mediumtext": mysql.TypeMediumBlob, - "null": mysql.TypeNull, - "set": mysql.TypeSet, - "smallint": mysql.TypeShort, - "char": mysql.TypeString, - "time": mysql.TypeDuration, - "timestamp": mysql.TypeTimestamp, - "tinyint": mysql.TypeTiny, - "tinytext": mysql.TypeTinyBlob, - "varchar": mysql.TypeVarchar, - "var_string": mysql.TypeVarString, - "year": mysql.TypeYear, + "bit": mysql.TypeBit, + "text": mysql.TypeBlob, + "date": mysql.TypeDate, + "datetime": mysql.TypeDatetime, + "unspecified": mysql.TypeUnspecified, + "decimal": mysql.TypeNewDecimal, + "double": mysql.TypeDouble, + "enum": mysql.TypeEnum, + "float": mysql.TypeFloat, + "geometry": mysql.TypeGeometry, + "point": mysql.TypePoint, + "linestring": mysql.TypeLineString, + "polygon": mysql.TypePolygon, + "multipoint": mysql.TypeMultiPoint, + "multilinestring": mysql.TypeMultiLineString, + "multipolygon": mysql.TypeMultiPolygon, + "geometrycollection": mysql.TypeGeometryCollection, + "vector": mysql.TypeTiDBVectorFloat32, + "mediumint": mysql.TypeInt24, + "json": mysql.TypeJSON, + "int": mysql.TypeLong, + "bigint": mysql.TypeLonglong, + "longtext": mysql.TypeLongBlob, + "mediumtext": mysql.TypeMediumBlob, + "null": mysql.TypeNull, + "set": mysql.TypeSet, + "smallint": mysql.TypeShort, + "char": mysql.TypeString, + "time": mysql.TypeDuration, + "timestamp": mysql.TypeTimestamp, + "tinyint": mysql.TypeTiny, + "tinytext": mysql.TypeTinyBlob, + "varchar": mysql.TypeVarchar, + "var_string": mysql.TypeVarString, + "year": mysql.TypeYear, } // TypeStr converts tp to a string. diff --git a/types/field_type.go b/types/field_type.go index 055f521..8b26235 100644 --- a/types/field_type.go +++ b/types/field_type.go @@ -678,6 +678,10 @@ func (ft *FieldType) RestoreAsCastType(ctx *format.RestoreCtx, explicitCharset b ctx.WriteKeyWord("YEAR") case mysql.TypeTiDBVectorFloat32: ctx.WriteKeyWord("VECTOR") + case mysql.TypeGeometry, mysql.TypePoint, mysql.TypeLineString, + mysql.TypePolygon, mysql.TypeMultiPoint, mysql.TypeMultiLineString, + mysql.TypeMultiPolygon, mysql.TypeGeometryCollection: + ctx.WriteKeyWord(TypeStr(ft.tp)) } if ft.array { ctx.WritePlain(" ") From ab888561e550f98552e79e093746cba66a2c093f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 23:17:42 +0000 Subject: [PATCH 2/2] Support the ASCII, UNICODE, and BYTE attributes on CHAR and VARCHAR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auditing the MySQL 26.7 data-type chapter (§13) against parseType found no missing type names after the spatial commit, but three missing attribute spellings from §13.3.1: CHAR(5) BYTE (the documented alias for BINARY(5)), VARCHAR(10) ASCII, and VARCHAR(10) UNICODE. The goyacc grammar allowed the ASCII/UNICODE/BYTE attributes only on TEXT, ENUM, SET, and LONG (OptCharsetWithOptBinary); MySQL's grammar gives CHAR and VARCHAR the same attribute set. parseCharTail and parseVarcharTail now parse OptCharsetWithOptBinary instead of OptBinary — a superset whose default alternative is OptBinary, so every previously accepted spelling parses identically — and normalize the binary charset (spelled BYTE or CHARACTER SET binary) to BinaryFlag plus the binary collation, mirroring the TextType action, so CHAR(5) BYTE produces the same FieldType as parsing its BINARY(5) restore output. UNICODE keeps the historical goyacc semantics: the attribute maps to the ucs2 charset, which is not supported, so it fails with the same "Unknown character set: 'ucs2'" action error TEXT UNICODE has always produced; the new compat_types goldens record that error contract. testdata/errors.json is unaffected. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01C3dMfdpMyaWffPoh8RBia4 --- parser/parse_types.go | 24 ++++++++++++++++--- parser/testdata/parser/compat_types/input.sql | 12 ++++++++++ .../testdata/parser/compat_types/output.sql | 12 ++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/parser/parse_types.go b/parser/parse_types.go index fb31baa..6620a89 100644 --- a/parser/parse_types.go +++ b/parser/parse_types.go @@ -429,13 +429,25 @@ func spatialTypeByte(tok int) byte { // parseCharTail finishes `Char/NChar FieldLen OptBinary` and // `Char/NChar OptBinary` after the type keyword(s) have been consumed. +// Deviation from the goyacc grammar: the attribute is parsed as +// OptCharsetWithOptBinary, whose extra leading tokens admit the MySQL +// 26.7 §13.3.1 ASCII/UNICODE/BYTE attributes (`CHAR BYTE` is the +// documented alias for BINARY); goyacc allowed them only on TEXT, +// ENUM, SET, and LONG. func (r *rdParser) parseCharTail() *types.FieldType { tp := types.NewFieldType(mysql.TypeString) if r.tok() == int('(') { tp.SetFlen(r.parseFieldLen()) } - opt := r.parseOptBinary() + opt := r.parseOptCharsetWithOptBinary() tp.SetCharset(opt.Charset) + if opt.Charset == charset.CharsetBin { + // The binary charset (spelled BYTE or CHARACTER SET binary) makes + // the column BINARY(n); normalize like the TextType action so the + // result matches what parsing BINARY(n) produces. + tp.AddFlag(mysql.BinaryFlag) + tp.SetCollate(charset.CollationBin) + } if opt.IsBinary { tp.AddFlag(mysql.BinaryFlag) } @@ -443,13 +455,19 @@ func (r *rdParser) parseCharTail() *types.FieldType { } // parseVarcharTail finishes `Varchar/NVarchar FieldLen OptBinary` after -// the type keyword(s) have been consumed. +// the type keyword(s) have been consumed, with the same +// OptCharsetWithOptBinary deviation and binary-charset normalization as +// parseCharTail. func (r *rdParser) parseVarcharTail() *types.FieldType { flen := r.parseFieldLen() - opt := r.parseOptBinary() + opt := r.parseOptCharsetWithOptBinary() tp := types.NewFieldType(mysql.TypeVarchar) tp.SetFlen(flen) tp.SetCharset(opt.Charset) + if opt.Charset == charset.CharsetBin { + tp.AddFlag(mysql.BinaryFlag) + tp.SetCollate(charset.CollationBin) + } if opt.IsBinary { tp.AddFlag(mysql.BinaryFlag) } diff --git a/parser/testdata/parser/compat_types/input.sql b/parser/testdata/parser/compat_types/input.sql index 1810b90..a532170 100644 --- a/parser/testdata/parser/compat_types/input.sql +++ b/parser/testdata/parser/compat_types/input.sql @@ -29,3 +29,15 @@ CREATE TABLE t(id INT PRIMARY KEY, c1 LONG) CREATE TABLE t(id INT PRIMARY KEY, c1 MIDDLEINT) -- case CREATE TABLE t(id INT PRIMARY KEY, c1 NUMERIC) +-- case +CREATE TABLE t(id INT PRIMARY KEY, c1 CHAR(5) BYTE) +-- case +CREATE TABLE t(id INT PRIMARY KEY, c1 CHAR(5) ASCII) +-- case +CREATE TABLE t(id INT PRIMARY KEY, c1 CHAR(5) UNICODE) +-- case +CREATE TABLE t(id INT PRIMARY KEY, c1 VARCHAR(10) BYTE) +-- case +CREATE TABLE t(id INT PRIMARY KEY, c1 VARCHAR(10) ASCII) +-- case +CREATE TABLE t(id INT PRIMARY KEY, c1 VARCHAR(10) UNICODE) diff --git a/parser/testdata/parser/compat_types/output.sql b/parser/testdata/parser/compat_types/output.sql index 959aeb1..ffe609a 100644 --- a/parser/testdata/parser/compat_types/output.sql +++ b/parser/testdata/parser/compat_types/output.sql @@ -29,3 +29,15 @@ CREATE TABLE `t` (`id` INT PRIMARY KEY,`c1` MEDIUMTEXT) CREATE TABLE `t` (`id` INT PRIMARY KEY,`c1` MEDIUMINT) -- case CREATE TABLE `t` (`id` INT PRIMARY KEY,`c1` DECIMAL) +-- case +CREATE TABLE `t` (`id` INT PRIMARY KEY,`c1` BINARY(5)) +-- case +CREATE TABLE `t` (`id` INT PRIMARY KEY,`c1` CHAR(5) CHARACTER SET LATIN1) +-- case +-- error: [parser:1115]Unknown character set: 'ucs2' +-- case +CREATE TABLE `t` (`id` INT PRIMARY KEY,`c1` VARBINARY(10)) +-- case +CREATE TABLE `t` (`id` INT PRIMARY KEY,`c1` VARCHAR(10) CHARACTER SET LATIN1) +-- case +-- error: [parser:1115]Unknown character set: 'ucs2'