Skip to content

Commit 278f88a

Browse files
authored
Generate enum types from Postgres and MySQL (#188)
* add enum in postgres * hrmm * chore: better message * wip * wip: working on enums * postgres enums * wip * mysql handling * works for mysql as wellg * fix * fix clippy * clean up * clippy fixes * fmt * fix * hrm * fix? * fix
1 parent c5d2298 commit 278f88a

File tree

8 files changed

+254
-23
lines changed

8 files changed

+254
-23
lines changed

.github/workflows/rust-test.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ jobs:
5151
env:
5252
MYSQL_VERSION: ${{ matrix.db.mysql }}
5353
PG_VERSION: ${{ matrix.db.postgres }}
54+
MYSQL_MIGRATION_FILE: "${{ matrix.db.mysql == '5.6' && 'mysql_migration_5_6.sql' || 'mysql_migration.sql' }}"
5455

5556
- uses: GuillaumeFalourd/wait-sleep-action@v1
5657
with:

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ services:
1616
image: mysql:${MYSQL_VERSION:-8}
1717
restart: always
1818
volumes:
19-
- ./playpen/db/mysql_migration.sql:/docker-entrypoint-initdb.d/mysql_migration.sql
19+
- ./playpen/db/${MYSQL_MIGRATION_FILE:-mysql_migration.sql}:/docker-entrypoint-initdb.d/mysql_migration.sql
2020
ports:
2121
- 33306:3306
2222
environment:

playpen/db/mysql_migration.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,6 @@ CREATE TABLE random (
6565

6666
-- JSON types
6767
json1 JSON
68-
68+
6969
);
7070

playpen/db/mysql_migration_5_6.sql

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
CREATE TABLE tables (
2+
id INTEGER NOT NULL AUTO_INCREMENT,
3+
number INTEGER NOT NULL,
4+
occupied BOOL NOT NULL DEFAULT FALSE,
5+
PRIMARY KEY (id)
6+
);
7+
8+
CREATE TABLE items (
9+
id INTEGER NOT NULL,
10+
food_type VARCHAR(30) NOT NULL,
11+
time_takes_to_cook INTEGER NOT NULL,
12+
table_id INTEGER NOT NULL,
13+
points SMALLINT NOT NULL,
14+
FOREIGN KEY (table_id) REFERENCES tables (id),
15+
PRIMARY KEY (id)
16+
);
17+
18+
INSERT INTO tables (number)
19+
VALUES
20+
(1), (2), (3), (4), (5), (6), (7), (8), (9), (10);
21+
22+
INSERT INTO items (id, food_type, time_takes_to_cook, table_id, points)
23+
VALUES
24+
(1, 'korean', 10, 1, 2),
25+
(2, 'chinese', 10, 1, 2),
26+
(3, 'japanese', 10, 1, 2),
27+
(4, 'italian', 10, 1, 2),
28+
(5, 'french', 10, 1, 2);
29+
30+
31+
-- We can primarily use this table to check how a column in MySQL can be converted to a TsFieldType
32+
33+
CREATE TABLE random (
34+
-- numeric types
35+
intz INT,
36+
smallint1 SMALLINT,
37+
tinyint1 TINYINT,
38+
medium1 MEDIUMINT,
39+
bigint1 BIGINT,
40+
decimal1 DECIMAL(2, 2),
41+
numeric1 NUMERIC(2, 2),
42+
double_precision1 DOUBLE PRECISION(2, 2),
43+
float1 FLOAT,
44+
double1 DOUBLE,
45+
bit1 BIT(2),
46+
bool1 BOOL,
47+
bool2 BOOLEAN,
48+
49+
-- date and datetime types
50+
date1 DATE,
51+
datetime1 DATETIME,
52+
timestamp1 TIMESTAMP,
53+
year1 YEAR,
54+
55+
-- string types
56+
char1 CHAR,
57+
varchar1 VARCHAR(20),
58+
binary1 BINARY,
59+
varbinary1 VARBINARY(2),
60+
blob1 BLOB,
61+
text1 TEXT,
62+
-- ideally this one should be generated as a legit enum type
63+
enum1 ENUM('x-small', 'small', 'medium', 'large', 'x-large'),
64+
set1 SET('one', 'two')
65+
);
66+

playpen/db/postgres_migration.sql

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ CREATE TABLE postgres.public.items (
2121
PRIMARY KEY (id)
2222
);
2323

24+
CREATE TYPE sizes AS ENUM ('x-small', 'small', 'medium', 'large', 'x-large');
25+
2426
-- A table of randomness, just to test various field types in PostgreSQL
2527
-- There is a pretty comprehensive list of data types available in Postgres
2628
-- found in https://www.geeksforgeeks.org/postgresql-data-types/ -> not the official Postgres doc
@@ -58,6 +60,8 @@ CREATE TABLE postgres.public.random (
5860

5961
-- UUID
6062
uuid1 UUID,
63+
64+
enum1 sizes,
6165

6266
-- Special data types
6367
box1 BOX,

src/ts_generator/information_schema.rs

Lines changed: 76 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::common::errors::{DB_CONN_POOL_RETRIEVE_ERROR, DB_SCHEME_READ_ERROR};
2+
use crate::common::logger::*;
23
use crate::core::connection::DBConn;
34
use crate::core::mysql::pool::MySqlConnectionManager;
45
use crate::core::postgres::pool::PostgresConnectionManager;
@@ -25,7 +26,12 @@ struct ColumnsQueryResultRow {
2526
}
2627

2728
pub struct DBSchema {
29+
// Holds cache details for table / columns of the target database
2830
tables_cache: HashMap<String, Fields>,
31+
// Holds cache details for enums that exists in the target database
32+
enums_cache: HashMap<String, HashMap<String, Vec<String>>>,
33+
// A flag to track if we have already tried to fetch enum and cache it
34+
has_cached_enums: bool,
2935
}
3036

3137
impl Default for DBSchema {
@@ -38,6 +44,8 @@ impl DBSchema {
3844
pub fn new() -> DBSchema {
3945
DBSchema {
4046
tables_cache: HashMap::new(),
47+
enums_cache: HashMap::new(),
48+
has_cached_enums: false,
4149
}
4250
}
4351

@@ -58,7 +66,7 @@ impl DBSchema {
5866

5967
let result = match &conn {
6068
DBConn::MySQLPooledConn(conn) => Self::mysql_fetch_table(self, table_name, conn).await,
61-
DBConn::PostgresConn(conn) => Self::postgres_fetch_table(self, table_name, conn).await,
69+
DBConn::PostgresConn(conn) => Self::postgres_fetch_table(self, &"public".to_string(), table_name, conn).await,
6270
};
6371

6472
if let Some(result) = &result {
@@ -70,6 +78,7 @@ impl DBSchema {
7078

7179
async fn postgres_fetch_table(
7280
&self,
81+
schema: &String,
7382
table_names: &Vec<&str>,
7483
conn: &Mutex<Pool<PostgresConnectionManager>>,
7584
) -> Option<Fields> {
@@ -82,14 +91,24 @@ impl DBSchema {
8291
let query = format!(
8392
r"
8493
SELECT
85-
COLUMN_NAME as column_name,
86-
DATA_TYPE as data_type,
87-
IS_NULLABLE as is_nulalble
88-
FROM information_schema.COLUMNS
89-
WHERE TABLE_SCHEMA = 'public'
90-
AND TABLE_NAME IN ({})
94+
COLUMN_NAME as column_name,
95+
DATA_TYPE as data_type,
96+
IS_NULLABLE as is_nulalble,
97+
TABLE_NAME as table_name,
98+
(
99+
select string_agg(e.enumlabel, ',')
100+
from pg_type t
101+
join pg_enum e on t.oid = e.enumtypid
102+
join pg_catalog.pg_namespace n ON n.oid = t.typnamespace
103+
where n.nspname = '{}'
104+
and t.typname = udt_name
105+
group by n.nspname, t.typname
106+
) as enum_values
107+
FROM information_schema.COLUMNS
108+
WHERE TABLE_SCHEMA = '{}'
109+
AND TABLE_NAME IN ({});
91110
",
92-
table_names,
111+
schema, schema, table_names,
93112
);
94113

95114
let mut fields: HashMap<String, Field> = HashMap::new();
@@ -103,10 +122,27 @@ impl DBSchema {
103122
let field_name: String = row.get(0);
104123
let field_type: String = row.get(1);
105124
let is_nullable: String = row.get(2);
125+
let table_name: String = row.get(3);
126+
let enum_values: Option<Vec<String>> = row
127+
.try_get(4)
128+
.ok()
129+
.map(|val: String| val.split(",").map(|x| x.to_string()).collect());
130+
106131
let field = Field {
107-
field_type: TsFieldType::get_ts_field_type_from_postgres_field_type(field_type.to_owned()),
132+
field_type: TsFieldType::get_ts_field_type_from_postgres_field_type(
133+
field_type.to_owned(),
134+
field_name.to_owned(),
135+
table_name,
136+
enum_values,
137+
),
108138
is_nullable: is_nullable == "YES",
109139
};
140+
if field.field_type == TsFieldType::Any {
141+
let message = format!(
142+
"The column {field_name} of type {field_type} will be translated any as it isn't supported by sqlx-ts"
143+
);
144+
info!(message);
145+
}
110146
fields.insert(field_name.to_owned(), field);
111147
}
112148

@@ -131,8 +167,22 @@ impl DBSchema {
131167
SELECT
132168
COLUMN_NAME as column_name,
133169
DATA_TYPE as data_type,
134-
IS_NULLABLE as is_nulalble
135-
FROM information_schema.COLUMNS
170+
IS_NULLABLE as is_nulalble,
171+
TABLE_NAME,
172+
(
173+
SELECT REPLACE(
174+
TRIM(TRAILING ')' FROM
175+
TRIM(LEADING '(' from
176+
TRIM(LEADING 'enum' FROM COLUMN_TYPE)))
177+
, '\''
178+
, ''
179+
)
180+
FROM information_schema.COLUMNS subcols
181+
WHERE subcols.TABLE_SCHEMA = (SELECT DATABASE())
182+
AND subcols.TABLE_NAME = C.TABLE_NAME
183+
AND subcols.COLUMN_NAME = C.COLUMN_NAME
184+
) AS enums
185+
FROM information_schema.COLUMNS C
136186
WHERE TABLE_SCHEMA = (SELECT DATABASE())
137187
AND TABLE_NAME IN ({})
138188
",
@@ -149,8 +199,22 @@ impl DBSchema {
149199
let field_name: String = row.clone().take(0).expect(DB_SCHEME_READ_ERROR);
150200
let field_type: String = row.clone().take(1).expect(DB_SCHEME_READ_ERROR);
151201
let is_nullable: String = row.clone().take(2).expect(DB_SCHEME_READ_ERROR);
202+
let table_name: String = row.clone().take(3).expect(DB_SCHEME_READ_ERROR);
203+
204+
let enum_values: Option<Vec<String>> = if field_type == "enum" {
205+
let enums: String = row.clone().take(4).expect(DB_SCHEME_READ_ERROR);
206+
let enum_values: Vec<String> = enums.split(",").map(|x| x.to_string()).collect();
207+
Some(enum_values)
208+
} else {
209+
None
210+
};
152211
let field = Field {
153-
field_type: TsFieldType::get_ts_field_type_from_mysql_field_type(field_type.to_owned()),
212+
field_type: TsFieldType::get_ts_field_type_from_mysql_field_type(
213+
field_type.to_owned(),
214+
table_name.to_owned(),
215+
field_name.to_owned(),
216+
enum_values.to_owned(),
217+
),
154218
is_nullable: is_nullable == "YES",
155219
};
156220
fields.insert(field_name.to_owned(), field);

src/ts_generator/types/ts_query.rs

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
1+
use crate::common::logger::*;
12
use color_eyre::eyre::Result;
23
use convert_case::{Case, Casing};
34
use regex::Regex;
45
use std::collections::{BTreeMap, HashMap};
56
use std::fmt::{self};
67

78
use crate::common::lazy::CONFIG;
8-
use crate::common::logger::*;
99
use crate::ts_generator::errors::TsGeneratorError;
1010

1111
type Array2DContent = Vec<Vec<TsFieldType>>;
1212

13-
#[derive(Debug, Clone)]
13+
#[derive(Debug, Clone, PartialEq)]
1414
pub enum TsFieldType {
1515
String,
1616
Number,
1717
Boolean,
1818
Object,
1919
Date,
2020
Null,
21+
Enum(Vec<String>),
2122
Any,
2223
Array2D(Array2DContent),
2324
Array(Box<TsFieldType>),
@@ -57,6 +58,11 @@ impl fmt::Display for TsFieldType {
5758

5859
write!(f, "{result}")
5960
}
61+
TsFieldType::Enum(values) => {
62+
let enums: Vec<String> = values.iter().map(|x| format!("'{x}'")).collect();
63+
let joined_enums = enums.join(" | ");
64+
write!(f, "{joined_enums}")
65+
}
6066
}
6167
}
6268
}
@@ -69,30 +75,53 @@ impl TsFieldType {
6975
/// @examples
7076
/// get_ts_field_type_from_postgres_field_type("integer") -> TsFieldType::Number
7177
/// get_ts_field_type_from_postgres_field_type("smallint") -> TsFieldType::Number
78+
/// get_ts_field_type_from_postgres_field_type("character varying", ",,")
7279
///
73-
pub fn get_ts_field_type_from_postgres_field_type(field_type: String) -> Self {
80+
pub fn get_ts_field_type_from_postgres_field_type(
81+
field_type: String,
82+
field_name: String,
83+
table_name: String,
84+
enum_values: Option<Vec<String>>,
85+
) -> Self {
7486
match field_type.as_str() {
7587
"smallint" | "integer" | "real" | "double precision" | "numeric" => Self::Number,
7688
"character" | "character varying" | "bytea" | "uuid" | "text" => Self::String,
7789
"boolean" => Self::Boolean,
7890
"json" | "jsonb" => Self::Object,
79-
"ARRAY" | "array" => {
80-
info!(
81-
"Currently we cannot figure out the type information for an array, the feature will be added in the future"
82-
);
91+
"ARRAY" | "array" => Self::Any,
92+
"date" => Self::Date,
93+
"USER-DEFINED" => {
94+
if let Some(enum_values) = enum_values {
95+
return Self::Enum(enum_values);
96+
}
97+
let warning_message = format!("Failed to find enum values for field {field_name} of table {table_name}");
98+
warning!(warning_message);
8399
Self::Any
84100
}
85-
"date" => Self::Date,
86101
_ => Self::Any,
87102
}
88103
}
89104

90-
pub fn get_ts_field_type_from_mysql_field_type(mysql_field_type: String) -> Self {
105+
pub fn get_ts_field_type_from_mysql_field_type(
106+
mysql_field_type: String,
107+
table_name: String,
108+
field_name: String,
109+
enum_values: Option<Vec<String>>,
110+
) -> Self {
91111
match mysql_field_type.as_str() {
92112
"bigint" | "decimal" | "double" | "float" | "int" | "mediumint" | "smallint" | "year" => Self::Number,
93113
"binary" | "bit" | "blob" | "char" | "text" | "varbinary" | "varchar" => Self::String,
94114
"tinyint" => Self::Boolean,
95115
"date" | "datetime" | "timestamp" => Self::Date,
116+
"enum" => {
117+
if let Some(enum_values) = enum_values {
118+
return Self::Enum(enum_values);
119+
}
120+
121+
let warning_message = format!("Failed to find enum values for field {field_name} of table {table_name}");
122+
warning!(warning_message);
123+
Self::Any
124+
}
96125
_ => Self::Any,
97126
}
98127
}

0 commit comments

Comments
 (0)