Skip to content

Commit bcd9fbc

Browse files
authored
feat: domains/table_schema (#13)
1 parent 030580d commit bcd9fbc

File tree

4 files changed

+264
-0
lines changed

4 files changed

+264
-0
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ func main() {
3131
4. running your application
3232
5. access `http://localhost:39393` and get the query list
3333

34+
## Getting Table Schemas
35+
36+
```sh
37+
DATABASE='isupipe'
38+
mysql -u root -ppass -h 127.0.0.1 -N -e "SHOW TABLES FROM $DATABASE" | while read table; do mysql -u root -ppass -h 127.0.0.1 -e "SHOW CREATE TABLE $DATABASE.\`$table\`" | awk 'NR>1 {$1=""; print substr($0,2) ";"}' | sed 's/\\n/\n/g'; done > schema.sql
39+
```
40+
3441
## Cache Plan
3542

3643
### Format

domains/table_schema.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package domains
2+
3+
type TableSchema struct {
4+
TableName string
5+
Columns []TableSchemaColumn
6+
}
7+
8+
type TableSchemaColumn struct {
9+
ColumnName string
10+
DataType TableSchemaDataType
11+
IsNullable bool
12+
IsPrimary bool
13+
IsUnique bool
14+
}
15+
16+
type TableSchemaDataType string
17+
18+
const (
19+
TableSchemaDataType_STRING TableSchemaDataType = "string"
20+
TableSchemaDataType_BYTES TableSchemaDataType = "bytes"
21+
TableSchemaDataType_INT TableSchemaDataType = "int"
22+
TableSchemaDataType_INT64 TableSchemaDataType = "int64"
23+
TableSchemaDataType_DATETIME TableSchemaDataType = "time"
24+
TableSchemaDataType_UNKNOWN TableSchemaDataType = "unknown"
25+
)

domains/table_schema_repository.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package domains
2+
3+
import (
4+
"regexp"
5+
"strings"
6+
)
7+
8+
var commentRegex = regexp.MustCompile("(?m)--.*$")
9+
var tableDefRegex = regexp.MustCompile("(?s)CREATE TABLE `?(?P<TableName>\\w+)`? \\((?P<Columns>.*?)\\)[^\\n]*;")
10+
var columnDefRegex = regexp.MustCompile("^`?(?P<ColumnName>\\w+)`?\\s+(?P<DataType>\\w+(?:\\([^)]*\\))?)(?:(?P<AutoIncrement>\\s+AUTO_INCREMENT)|(?P<Unique>\\s+UNIQUE)|(?P<PrimaryKey>\\s+PRIMARY KEY)|(?P<NonNullable>\\s+NOT NULL)|(?P<Default>\\s+DEFAULT [^\\s]+))*$")
11+
var primaryKeyRegex = regexp.MustCompile("^PRIMARY KEY \\(`?(?P<ColumnName>\\w+)`?\\)$")
12+
var uniqueRegex = regexp.MustCompile("^UNIQUE(?: KEY)?\\s+(?:`?\\w+`?\\s+)?\\(`?(?P<ColumnName>\\w+)`?\\)$")
13+
14+
func LoadTableSchema(sql string) ([]TableSchema, error) {
15+
var tableSchemas []TableSchema
16+
17+
sql = commentRegex.ReplaceAllString(sql, "")
18+
tableMatches := tableDefRegex.FindAllStringSubmatch(sql, -1)
19+
tableNames := tableDefRegex.SubexpNames()
20+
21+
for _, tableMatch := range tableMatches {
22+
var schema TableSchema
23+
for i, name := range tableNames {
24+
if name == "TableName" {
25+
schema.TableName = tableMatch[i]
26+
} else if name == "Columns" {
27+
columnsDef := tableMatch[i]
28+
columnDefs := strings.Split(columnsDef, ",")
29+
for _, columnDef := range columnDefs {
30+
columnDef = strings.TrimSpace(columnDef)
31+
primaryKeyMatches := primaryKeyRegex.FindStringSubmatch(columnDef)
32+
if len(primaryKeyMatches) > 0 {
33+
primaryKeyNames := primaryKeyRegex.SubexpNames()
34+
for j, name := range primaryKeyNames {
35+
if name == "ColumnName" {
36+
for i, column := range schema.Columns {
37+
if column.ColumnName == primaryKeyMatches[j] {
38+
schema.Columns[i].IsPrimary = true
39+
}
40+
}
41+
}
42+
}
43+
}
44+
45+
uniqueMatches := uniqueRegex.FindStringSubmatch(columnDef)
46+
if len(uniqueMatches) > 0 {
47+
uniqueNames := uniqueRegex.SubexpNames()
48+
for j, name := range uniqueNames {
49+
if name == "ColumnName" {
50+
for i, column := range schema.Columns {
51+
if column.ColumnName == uniqueMatches[j] {
52+
schema.Columns[i].IsUnique = true
53+
}
54+
}
55+
}
56+
}
57+
}
58+
59+
columnMatches := columnDefRegex.FindAllStringSubmatch(columnDef, -1)
60+
columnNames := columnDefRegex.SubexpNames()
61+
62+
for _, columnMatch := range columnMatches {
63+
var column TableSchemaColumn
64+
column.IsNullable = true
65+
for j, cname := range columnNames {
66+
switch cname {
67+
case "ColumnName":
68+
column.ColumnName = columnMatch[j]
69+
case "DataType":
70+
column.DataType = parseDataType(columnMatch[j])
71+
case "NonNullable":
72+
column.IsNullable = column.IsNullable && columnMatch[j] == ""
73+
case "PrimaryKey":
74+
column.IsPrimary = columnMatch[j] != ""
75+
case "Unique":
76+
column.IsUnique = columnMatch[j] != ""
77+
}
78+
}
79+
schema.Columns = append(schema.Columns, column)
80+
}
81+
}
82+
}
83+
}
84+
tableSchemas = append(tableSchemas, schema)
85+
}
86+
87+
return tableSchemas, nil
88+
}
89+
90+
func parseDataType(sqlType string) TableSchemaDataType {
91+
sqlType = strings.ToLower(sqlType)
92+
sqlType = strings.Split(sqlType, "(")[0]
93+
switch sqlType {
94+
case "varchar", "text":
95+
return TableSchemaDataType_STRING
96+
case "longblob", "mediumblob", "blob":
97+
return TableSchemaDataType_BYTES
98+
case "int", "tinyint":
99+
return TableSchemaDataType_INT
100+
case "bigint":
101+
return TableSchemaDataType_INT64
102+
case "time", "date", "datetime", "timestamp":
103+
return TableSchemaDataType_DATETIME
104+
default:
105+
return TableSchemaDataType_UNKNOWN
106+
}
107+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package domains
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestLoadTableSchema(t *testing.T) {
11+
tests := []struct {
12+
sql string
13+
expected []TableSchema
14+
}{
15+
{
16+
sql: "CREATE TABLE users (\n" +
17+
" id BIGINT AUTO_INCREMENT PRIMARY KEY,\n" +
18+
" `name` VARCHAR(255) NOT NULL,\n" +
19+
" created_at DATETIME(6) UNIQUE,\n" +
20+
" description TEXT NOT NULL,\n" +
21+
" icon LONGBLOB NOT NULL,\n" +
22+
" UNIQUE KEY uniq_name (name),\n" +
23+
");\n" +
24+
"CREATE TABLE posts (\n" +
25+
" id BIGINT NOT NULL AUTO_INCREMENT,\n" +
26+
" title VARCHAR(255) NOT NULL,\n" +
27+
" content TEXT NOT NULL,\n" +
28+
" PRIMARY KEY (id),\n" +
29+
" UNIQUE (`title`),\n" +
30+
");",
31+
expected: []TableSchema{
32+
{
33+
TableName: "users",
34+
Columns: []TableSchemaColumn{
35+
{ColumnName: "id", DataType: TableSchemaDataType_INT64, IsNullable: true, IsPrimary: true, IsUnique: false},
36+
{ColumnName: "name", DataType: TableSchemaDataType_STRING, IsNullable: false, IsPrimary: false, IsUnique: true},
37+
{ColumnName: "created_at", DataType: TableSchemaDataType_DATETIME, IsNullable: true, IsPrimary: false, IsUnique: true},
38+
{ColumnName: "description", DataType: TableSchemaDataType_STRING, IsNullable: false, IsPrimary: false, IsUnique: false},
39+
{ColumnName: "icon", DataType: TableSchemaDataType_BYTES, IsNullable: false, IsPrimary: false, IsUnique: false},
40+
},
41+
},
42+
{
43+
TableName: "posts",
44+
Columns: []TableSchemaColumn{
45+
{ColumnName: "id", DataType: TableSchemaDataType_INT64, IsNullable: false, IsPrimary: true, IsUnique: false},
46+
{ColumnName: "title", DataType: TableSchemaDataType_STRING, IsNullable: false, IsPrimary: false, IsUnique: true},
47+
{ColumnName: "content", DataType: TableSchemaDataType_STRING, IsNullable: false, IsPrimary: false, IsUnique: false},
48+
},
49+
},
50+
},
51+
},
52+
{
53+
sql: "DROP TABLE IF EXISTS users;\n" +
54+
"CREATE TABLE users (\n" +
55+
" `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY,\n" +
56+
" `account_name` varchar(64) NOT NULL UNIQUE,\n" +
57+
" `passhash` varchar(128) NOT NULL, -- SHA2 512 non-binary (hex)\n" +
58+
" `authority` tinyint(1) NOT NULL DEFAULT 0,\n" +
59+
" `del_flg` tinyint(1) NOT NULL DEFAULT 0,\n" +
60+
" `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP\n" +
61+
") DEFAULT CHARSET=utf8mb4;\n" +
62+
"\n" +
63+
"DROP TABLE IF EXISTS posts;\n" +
64+
"CREATE TABLE posts (\n" +
65+
" `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY,\n" +
66+
" `user_id` int NOT NULL,\n" +
67+
" `mime` varchar(64) NOT NULL,\n" +
68+
" `imgdata` mediumblob NOT NULL,\n" +
69+
" `body` text NOT NULL,\n" +
70+
" `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP\n" +
71+
") DEFAULT CHARSET=utf8mb4;\n" +
72+
"\n" +
73+
"DROP TABLE IF EXISTS comments;\n" +
74+
"CREATE TABLE comments (\n" +
75+
" `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY,\n" +
76+
" `post_id` int NOT NULL,\n" +
77+
" `user_id` int NOT NULL,\n" +
78+
" `comment` text NOT NULL,\n" +
79+
" `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP\n" +
80+
") DEFAULT CHARSET=utf8mb4;\n",
81+
expected: []TableSchema{
82+
{
83+
TableName: "users",
84+
Columns: []TableSchemaColumn{
85+
{ColumnName: "id", DataType: TableSchemaDataType_INT, IsNullable: false, IsPrimary: true, IsUnique: false},
86+
{ColumnName: "account_name", DataType: TableSchemaDataType_STRING, IsNullable: false, IsPrimary: false, IsUnique: true},
87+
{ColumnName: "passhash", DataType: TableSchemaDataType_STRING, IsNullable: false, IsPrimary: false, IsUnique: false},
88+
{ColumnName: "authority", DataType: TableSchemaDataType_INT, IsNullable: false, IsPrimary: false, IsUnique: false},
89+
{ColumnName: "del_flg", DataType: TableSchemaDataType_INT, IsNullable: false, IsPrimary: false, IsUnique: false},
90+
{ColumnName: "created_at", DataType: TableSchemaDataType_DATETIME, IsNullable: false, IsPrimary: false, IsUnique: false},
91+
},
92+
},
93+
{
94+
TableName: "posts",
95+
Columns: []TableSchemaColumn{
96+
{ColumnName: "id", DataType: TableSchemaDataType_INT, IsNullable: false, IsPrimary: true, IsUnique: false},
97+
{ColumnName: "user_id", DataType: TableSchemaDataType_INT, IsNullable: false, IsPrimary: false, IsUnique: false},
98+
{ColumnName: "mime", DataType: TableSchemaDataType_STRING, IsNullable: false, IsPrimary: false, IsUnique: false},
99+
{ColumnName: "imgdata", DataType: TableSchemaDataType_BYTES, IsNullable: false, IsPrimary: false, IsUnique: false},
100+
{ColumnName: "body", DataType: TableSchemaDataType_STRING, IsNullable: false, IsPrimary: false, IsUnique: false},
101+
{ColumnName: "created_at", DataType: TableSchemaDataType_DATETIME, IsNullable: false, IsPrimary: false, IsUnique: false},
102+
},
103+
},
104+
{
105+
TableName: "comments",
106+
Columns: []TableSchemaColumn{
107+
{ColumnName: "id", DataType: TableSchemaDataType_INT, IsNullable: false, IsPrimary: true, IsUnique: false},
108+
{ColumnName: "post_id", DataType: TableSchemaDataType_INT, IsNullable: false, IsPrimary: false, IsUnique: false},
109+
{ColumnName: "user_id", DataType: TableSchemaDataType_INT, IsNullable: false, IsPrimary: false, IsUnique: false},
110+
{ColumnName: "comment", DataType: TableSchemaDataType_STRING, IsNullable: false, IsPrimary: false, IsUnique: false},
111+
{ColumnName: "created_at", DataType: TableSchemaDataType_DATETIME, IsNullable: false, IsPrimary: false, IsUnique: false},
112+
},
113+
},
114+
},
115+
},
116+
}
117+
118+
for i, test := range tests {
119+
t.Run("test"+fmt.Sprint(i), func(t *testing.T) {
120+
schemas, err := LoadTableSchema(test.sql)
121+
assert.NoError(t, err)
122+
assert.Equal(t, test.expected, schemas)
123+
})
124+
}
125+
}

0 commit comments

Comments
 (0)