Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion lib/core/erd/erd_catalog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,16 @@ SELECT ${_pgQualified('c', 'ns')} AS table_name, a.attname AS column_name,
OR pg_get_expr(df.adbin, df.adrelid) LIKE 'nextval(%'
THEN 1 ELSE 0 END AS is_identity,
col_description(c.oid, a.attnum) AS column_comment,
obj_description(c.oid, 'pg_class') AS table_comment
obj_description(c.oid, 'pg_class') AS table_comment,
(SELECT string_agg(e.enumlabel, chr(31) ORDER BY e.enumsortorder)
FROM pg_catalog.pg_enum e
WHERE e.enumtypid = CASE WHEN ty.typtype = 'd'
THEN ty.typbasetype ELSE ty.oid END) AS enum_values,
CASE WHEN ty.typtype = 'd'
THEN format_type(ty.typbasetype, ty.typtypmod) END AS domain_base
FROM pg_catalog.pg_attribute a
JOIN pg_catalog.pg_class c ON c.oid = a.attrelid
JOIN pg_catalog.pg_type ty ON ty.oid = a.atttypid
JOIN pg_catalog.pg_namespace ns ON ns.oid = c.relnamespace
LEFT JOIN pg_catalog.pg_constraint pk
ON pk.conrelid = c.oid AND pk.contype = 'p' AND a.attnum = ANY (pk.conkey)
Expand Down
47 changes: 46 additions & 1 deletion lib/core/erd/erd_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ class ErdColumn {
this.defaultValue,
this.isIdentity = false,
this.comment,
this.enumValues = const [],
this.domainBase,
});

final String name;
Expand All @@ -35,8 +37,16 @@ class ErdColumn {
/// none (#1279).
final String? comment;

/// The labels of an enum type, in their order (#1280): PostgreSQL enums
/// (also behind a domain) and MySQL `enum(...)` columns.
final List<String> enumValues;

/// The base type when the column's type is a PostgreSQL domain.
final String? domainBase;

/// Short markers after the type on a card and in exports (#1277).
List<String> get badges => [
if (enumValues.isNotEmpty) 'EN',
if (isUnique && !isPrimaryKey) 'UQ',
if (isIdentity) 'AI',
if (defaultValue != null && !isIdentity) 'DF',
Expand All @@ -52,6 +62,8 @@ class ErdColumn {
defaultValue: defaultValue,
isIdentity: isIdentity,
comment: comment,
enumValues: enumValues,
domainBase: domainBase,
);
}

Expand Down Expand Up @@ -105,7 +117,8 @@ class ErdSchema {
///
/// [columnRows]: `table, column, type, isPk`, then optionally `isNullable`,
/// `isUnique`, `default` and `isIdentity` (#1277), then the column comment
/// and the table comment (#1279). [fkRows]: `table, column, refTable, refColumn`. Relations to
/// and the table comment (#1279), then the enum labels (separated by
/// U+001F) and a domain's base type (#1280). [fkRows]: `table, column, refTable, refColumn`. Relations to
/// unknown tables are dropped.
factory ErdSchema.fromCatalog({
required List<List<String>> columnRows,
Expand All @@ -130,6 +143,8 @@ class ErdSchema {
defaultValue: r.length > 6 ? _default(r[6]) : null,
isIdentity: r.length > 7 && _truthy(r[7]),
comment: r.length > 8 ? _text(r[8]) : null,
enumValues: _enumValues(r[2], r.length > 10 ? r[10] : ''),
domainBase: r.length > 11 ? _text(r[11]) : null,
));
if (r.length > 9) {
final tc = _text(r[9]);
Expand Down Expand Up @@ -193,3 +208,33 @@ String? _text(String raw) {
if (v.isEmpty || v == 'NULL') return null;
return v;
}

/// Enum labels from the catalog cell, or from a MySQL `enum('a','b')` type.
List<String> _enumValues(String type, String cell) {
final listed = _text(cell);
if (listed != null) return listed.split('\u001f');
final t = type.trim();
if (!t.toLowerCase().startsWith('enum(') || !t.endsWith(')')) return const [];
final body = t.substring(5, t.length - 1);
final out = <String>[];
final cur = StringBuffer();
var inQuote = false;
for (var i = 0; i < body.length; i++) {
final ch = body[i];
if (inQuote) {
if (ch == "'" && i + 1 < body.length && body[i + 1] == "'") {
cur.write("'");
i++;
} else if (ch == "'") {
inQuote = false;
out.add(cur.toString());
cur.clear();
} else {
cur.write(ch);
}
} else if (ch == "'") {
inQuote = true;
}
}
return out;
}
4 changes: 4 additions & 0 deletions lib/core/mcp/mcp_query_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ class McpQueryService {
identity: c.isIdentity,
defaultValue: c.defaultValue,
comment: c.comment,
enumValues: c.enumValues,
references: [
for (final r in schema.relations)
if (r.fromTable == t.name && r.fromColumn == c.name)
Expand Down Expand Up @@ -433,6 +434,7 @@ class McpColumnInfo {
this.defaultValue,
this.identity = false,
this.comment,
this.enumValues = const [],
});

final String name;
Expand All @@ -442,6 +444,7 @@ class McpColumnInfo {
final String? defaultValue;
final bool identity;
final String? comment;
final List<String> enumValues;

/// `table.column` this column points to, when it is a foreign key.
final String? references;
Expand All @@ -455,6 +458,7 @@ class McpColumnInfo {
if (identity) 'identity': true,
if (defaultValue != null) 'default': defaultValue,
if (comment != null) 'comment': comment,
if (enumValues.isNotEmpty) 'enum_values': enumValues,
};
}

Expand Down
9 changes: 9 additions & 0 deletions lib/features/erd/erd_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1390,9 +1390,17 @@ class _TableCard extends material.StatelessWidget {
if (c.isUnique && !c.isPrimaryKey) 'Unique',
if (c.isIdentity) 'Generated (identity / auto increment)',
if (c.defaultValue case final d?) 'Default: $d',
if (c.domainBase case final b?) 'Domain over $b',
if (c.enumValues.isNotEmpty) 'Values: ${_enumList(c.enumValues)}',
if (c.comment case final n?) 'Note: $n',
].join('\n');

/// At most [cap] labels, then how many more.
static String _enumList(List<String> values, {int cap = 12}) =>
values.length <= cap
? values.join(', ')
: '${values.take(cap).join(', ')} (+${values.length - cap} more)';

/// Shadows read only on light surfaces, so a dark canvas gets twice the alpha.
double _shadowAlpha(material.Color canvas) {
final base = dragging ? 0.28 : 0.12;
Expand Down Expand Up @@ -1559,6 +1567,7 @@ class _TableCard extends material.StatelessWidget {
fontSize: 9,
fontWeight: material.FontWeight.w700,
color: switch (b) {
'EN' => palette.type5,
'UQ' => palette.type3,
'AI' => palette.type4,
_ => wb.mutedForeground,
Expand Down
45 changes: 45 additions & 0 deletions test/core/erd/erd_enum_values_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:querya_desktop/core/database/table_mutation_engine.dart';
import 'package:querya_desktop/core/erd/erd_catalog.dart';
import 'package:querya_desktop/core/erd/erd_model.dart';

/// #1280: enum labels and domain base types.
List<String> _row(String type, {String values = '', String base = ''}) =>
['t', 'c', type, '0', '1', '0', '', '0', '', '', values, base];

ErdColumn _col(List<String> row) =>
ErdSchema.fromCatalog(columnRows: [row], fkRows: const [])
.tables
.single
.columns
.single;

void main() {
test('PostgreSQL labels come in order, separated by U+001F', () {
final c = _col(_row('user_role_enum',
values: 'admin\u001feditor\u001fviewer'));
expect(c.enumValues, ['admin', 'editor', 'viewer']);
expect(c.badges, contains('EN'));
});

test("MySQL enum('a','b') is read from the type, quotes unescaped", () {
final c = _col(_row("enum('new','it''s paid','a,b')"));
expect(c.enumValues, ['new', "it's paid", 'a,b']);
});

test('a domain reports its base type; plain columns have neither', () {
final d = _col(_row('positive_int', base: 'integer'));
expect(d.domainBase, 'integer');
expect(d.enumValues, isEmpty);
final plain = _col(['t', 'c', 'text', '0']);
expect(plain.enumValues, isEmpty);
expect(plain.domainBase, isNull);
expect(plain.badges, isEmpty);
});

test('the PostgreSQL catalog asks pg_enum and the domain base', () {
final sql = ErdCatalog.columnsSql(SqlDialect.postgres);
expect(sql, contains('pg_enum'));
expect(sql, contains('typbasetype'));
});
}
Loading