From 4755378d95e774dc2722d6c2e420601b38e2cb7a Mon Sep 17 00:00:00 2001 From: ppvan Date: Sun, 31 Mar 2024 18:46:22 +0700 Subject: [PATCH 01/10] table graph instead of boring listview --- res/gtk/schema-view.blp | 17 +- res/gtk/style.css | 4 + res/gtk/table-data-view.blp | 4 +- res/gtk/table-graph.blp | 18 ++ res/meson.build | 1 + res/psequel.gresource.xml | 1 + src/application.vala | 1 + src/meson.build | 4 + src/models/Schema.vala | 8 + src/ui/views/SchemaView.vala | 2 +- src/ui/widgets/TableGraph.vala | 141 +++++++++++ src/utils/helpers.vala | 49 ++++ src/utils/types.vala | 103 ++++++++ src/vapi/csv.vapi | 181 ++++++++------ src/vapi/libgvc.vapi | 249 ++++++++++++++++++++ src/viewmodels/TableStructureViewModel.vala | 93 +++++++- 16 files changed, 777 insertions(+), 99 deletions(-) create mode 100644 res/gtk/table-graph.blp create mode 100644 src/ui/widgets/TableGraph.vala create mode 100644 src/vapi/libgvc.vapi diff --git a/res/gtk/schema-view.blp b/res/gtk/schema-view.blp index ea263e3..bfb6c0f 100644 --- a/res/gtk/schema-view.blp +++ b/res/gtk/schema-view.blp @@ -382,22 +382,7 @@ template $PsequelSchemaView: Adw.Bin { icon-name: "library-symbolic"; title: "Structure"; - child: Stack { - visible-child-name: bind template.view-mode; - transition-type: slide_left_right; - - StackPage { - name: "table"; - - child: $PsequelTableStructureView {}; - } - - StackPage { - name: "view"; - - child: $PsequelViewStructureView {}; - } - }; + child: $PsequelTableGraph {}; } Adw.ViewStackPage { diff --git a/res/gtk/style.css b/res/gtk/style.css index 3833011..27ebe16 100644 --- a/res/gtk/style.css +++ b/res/gtk/style.css @@ -29,6 +29,10 @@ -gtk-icon-size: 24px; } +border { + border: #1e1e1e 1px solid; +} + .icon-xl image { -gtk-icon-size: 50%; } diff --git a/res/gtk/table-data-view.blp b/res/gtk/table-data-view.blp index 7e42b77..d349982 100644 --- a/res/gtk/table-data-view.blp +++ b/res/gtk/table-data-view.blp @@ -6,9 +6,9 @@ template $PsequelTableDataView: Gtk.Box { height-request: 600; orientation: vertical; spacing: 4; - margin-start: 8; +// margin-start: 8; margin-top: 8; - margin-end: 8; +// margin-end: 8; margin-bottom: 8; Box { diff --git a/res/gtk/table-graph.blp b/res/gtk/table-graph.blp new file mode 100644 index 0000000..d00531d --- /dev/null +++ b/res/gtk/table-graph.blp @@ -0,0 +1,18 @@ +using Gtk 4.0; + +template $PsequelTableGraph: Box { + spacing: 12; + vexpand: true; + hexpand: true; + halign: center; + margin-top: 4; + margin-bottom: 4; + margin-start: 4; + margin-end: 4; + + styles ["view"] + + Picture pic { + content-fit: contain; + } +} diff --git a/res/meson.build b/res/meson.build index 8540279..df92f7d 100644 --- a/res/meson.build +++ b/res/meson.build @@ -26,6 +26,7 @@ blueprints = custom_target('blueprints', 'gtk/table-cols.blp', 'gtk/table-listitem.blp', 'gtk/table-row.blp', + 'gtk/table-graph.blp', 'gtk/query-listitem.blp', 'gtk/view-listitem.blp', 'gtk/table-fk.blp', diff --git a/res/psequel.gresource.xml b/res/psequel.gresource.xml index d7e9e0a..5859360 100644 --- a/res/psequel.gresource.xml +++ b/res/psequel.gresource.xml @@ -19,6 +19,7 @@ gtk/table-structure-view.ui gtk/view-structure-view.ui gtk/table-row.ui + gtk/table-graph.ui gtk/table-listitem.ui gtk/view-listitem.ui gtk/query-listitem.ui diff --git a/src/application.vala b/src/application.vala index 173501c..3e309bd 100644 --- a/src/application.vala +++ b/src/application.vala @@ -175,6 +175,7 @@ public class Application : Adw.Application { private static void ensure_types() { typeof(Psequel.StyleSwitcher).ensure(); typeof(Psequel.TableRow).ensure(); + typeof(Psequel.TableGraph).ensure(); typeof(Psequel.DataCell).ensure(); typeof(Psequel.ConnectionViewModel).ensure(); typeof(Psequel.SchemaView).ensure(); diff --git a/src/meson.build b/src/meson.build index 39aec0c..2658972 100644 --- a/src/meson.build +++ b/src/meson.build @@ -54,6 +54,7 @@ ui_sources = files([ 'ui/widgets/DataCell.vala', 'ui/widgets/TableRow.vala', + 'ui/widgets/TableGraph.vala', 'ui/widgets/StyleSwitcher.vala', 'ui/editor/QueryEditor.vala', @@ -95,6 +96,8 @@ psequel_deps = [ dependency('gtksourceview-5', version: '>= 5.0'), dependency('libpq', version: '>= 15.3'), dependency('sqlite3'), + dependency('libgvc'), + dependency('librsvg-2.0'), csv_dep, dependency('pgquery-vala'), valac.find_library('config', dirs: vapi_dir), @@ -107,6 +110,7 @@ add_project_arguments( '-DGETTEXT_PACKAGE="@0@"'.format(meson.project_name()), language: 'c' ) +add_project_arguments(['-D', 'WITH_CGRAPH'], language: 'vala') psequel_sources_main = psequel_sources + ui_sources + files(['application.vala']) diff --git a/src/models/Schema.vala b/src/models/Schema.vala index 0daa201..fef4293 100644 --- a/src/models/Schema.vala +++ b/src/models/Schema.vala @@ -125,11 +125,19 @@ namespace Psequel { } + public class PrimaryKey : BaseType { + public string[] columns { get; set; } + + public PrimaryKey () { + } + } /** Table foreign key info */ public class ForeignKey : BaseType { public string columns { get; set; default = ""; } + public string[] columns_v2 { get; set; } public string fk_table { get; set; default = ""; } public string fk_columns { get; set; default = ""; } + public string[] fk_columns_v2 { get; set; } public FKType on_update { get; set; default = NO_ACTION; } public FKType on_delete { get; set; default = NO_ACTION; } diff --git a/src/ui/views/SchemaView.vala b/src/ui/views/SchemaView.vala index 93ed97d..4cdf876 100644 --- a/src/ui/views/SchemaView.vala +++ b/src/ui/views/SchemaView.vala @@ -53,7 +53,7 @@ public class SchemaView : Adw.Bin { table_sort_model.sorter = table_name_sorter; view_sort_model.sorter = view_name_sorter; view_filter.expression = new Gtk.PropertyExpression(typeof(View), null, "name"); - stack.visible_child_name = "data-view"; + stack.visible_child_name = "structure-view"; } diff --git a/src/ui/widgets/TableGraph.vala b/src/ui/widgets/TableGraph.vala new file mode 100644 index 0000000..f863ebf --- /dev/null +++ b/src/ui/widgets/TableGraph.vala @@ -0,0 +1,141 @@ +using Rsvg; + +namespace Psequel { +[GtkTemplate(ui = "/me/ppvan/psequel/gtk/table-graph.ui")] +public class TableGraph : Gtk.Box { + private uint8[] buff; + + private TableStructureViewModel viewmodel; + + + public TableGraph() { + Object(); + } + + construct { + this.viewmodel = autowire (); + + this.viewmodel.notify["selected-table"].connect(() => { + debug ("Test: %s", this.viewmodel.selected_table.name); + var table = this.viewmodel.selected_table; + this.render_graph.begin(table); + }); + } + + public async void render_graph(Table table) { + var fks = this.viewmodel.foreign_keys; + uint8[] buff = generate_graph(table, fks.to_list()); + var svgPaintable = new SvgPaintable(buff); + + pic.set_paintable(svgPaintable); + debug ("Test svgPaintable: %d %d %d", fks.size, this.viewmodel.columns.size, this.viewmodel.indexes.size); + } + + public uint8[] generate_graph(Table table, List fks) { + var gvc = new Gvc.Context(); + var g = new Gvc.Graph("g", Gvc.Agdirected, 0); + g.safe_set("rankdir", "LR", ""); + g.safe_set("fontname", "Roboto", ""); + g.safe_set("bgcolor", "transparent", ""); + + foreach (var item in fks) + { + if (item.table != table.name && item.fk_table != table.name) + { + continue; + } + + var begin = g.create_node(item.table); + var end = g.create_node(item.fk_table); + + var begin_label = generate_table_details(g, item.table); + var end_label = generate_table_details(g, item.fk_table); + + begin.safe_set("fontname", "Roboto", ""); + begin.safe_set("shape", "plaintext", ""); + begin.safe_set("label", begin_label, ""); + begin.safe_set("fontcolor", "#D1CDC7", ""); + begin.safe_set("color", "#858786", ""); + + + end.safe_set("fontname", "Roboto", ""); + end.safe_set("shape", "plaintext", ""); + end.safe_set("label", end_label, ""); + end.safe_set("fontcolor", "#D1CDC7", ""); + end.safe_set("color", "#858786", ""); + var edge = g.create_edge(begin, end); + edge.safe_set("color", "#858786", ""); + edge.safe_set("tailport", item.fk_columns_v2[0], ""); + edge.safe_set("headport", item.columns_v2[0], ""); + } + gvc.layout(g, "dot"); + gvc.render_data(g, "svg", out this.buff); + gvc.free_layout(g); + + return(this.buff); + } + + private Gvc.HtmlString generate_table_details(Gvc.Graph g, string table) { + var stringBuilder = new StringBuilder(""""""); + stringBuilder.append(@""); + string[] current_pks = new string[0]; + string[] current_fks = new string[0]; + + foreach (var pk in this.viewmodel.primary_keys) + { + if (pk.table == table) + { + current_pks = pk.columns; + break; + } + } + + foreach (var fk in this.viewmodel.foreign_keys) + { + if (fk.table == table) + { + current_fks = fk.columns_v2; + break; + } + } + + debug (table); + + for (int i = 0; i < current_pks.length; i++) { + debug("PK: %s", current_pks[i]); + } + + for (int i = 0; i < current_fks.length; i++) { + debug("FK: %s", current_fks[i]); + } + + foreach (var col in this.viewmodel.columns) + { + if (col.table != table) + { + continue; + } + + if (col.name in current_fks) + { + stringBuilder.append_printf("""""", col.name, col.name, col.column_type); + } + else if (col.name in current_pks) + { + stringBuilder.append_printf("""""", col.name, col.name, col.column_type); + } + else + { + stringBuilder.append_printf("""""", col.name, col.name, col.column_type); + } + } + + stringBuilder.append("
$(table)
%s%s
%s%s
%s%s
"); + var markup = stringBuilder.free_and_steal(); + return(Gvc.HtmlString.make_html(g, markup)); + } + + [GtkChild] + private unowned Gtk.Picture pic; +} +} diff --git a/src/utils/helpers.vala b/src/utils/helpers.vala index 994c072..341e178 100644 --- a/src/utils/helpers.vala +++ b/src/utils/helpers.vala @@ -78,4 +78,53 @@ public class MonospaceFilter : Gtk.Filter { } } } + + +public class SvgPaintable : Gdk.Paintable, Object { + public uint8[] data { get; set; } + public Rsvg.Handle handle { get; private set; } + + public SvgPaintable(uint8[] data) { + this.data = data; + this.handle = new Rsvg.Handle.from_data(this.data); + } + + + public void snapshot(Gdk.Snapshot snapshot, double width, double height) { + var gtkSnapshot = snapshot as Gtk.Snapshot; + Graphene.Rect rect = Graphene.Rect() { + }; + rect = rect.init(0, 0, (float)width, (float)height); + + var svgRect = Rsvg.Rectangle() { + x = 0, + y = 0, + width = width, + height = height + }; + + + var cr = gtkSnapshot.append_cairo(rect); + + try { + this.handle.render_document(cr, svgRect); + } catch (GLib.Error err) { + debug(err.message); + } + } + + public int get_intrinsic_width() { + double width; + handle.get_intrinsic_size_in_pixels(out width, null); + + return((int)Math.ceil(width)); + } + + public int get_intrinsic_height() { + double height; + handle.get_intrinsic_size_in_pixels(null, out height); + + return((int)Math.ceil(height)); + } +} } diff --git a/src/utils/types.vala b/src/utils/types.vala index a5d17b5..30acca6 100644 --- a/src/utils/types.vala +++ b/src/utils/types.vala @@ -46,4 +46,107 @@ public T autowire () { var container = Container.instance(); return((T)container.find_type(typeof(T))); } + +public class Vec : Object { + static int DEFAULT_CAPACITY = 16; + + private T[] data; + private int size; + private int capacity; + + + + public Vec() { + this.with_capacity(DEFAULT_CAPACITY); + } + + public Vec.with_data(owned T[] data) { + this.data = data; + this.size = data.length; + this.capacity = data.length; + } + + public Vec.with_capacity(int capacity) { + this.data = new T[capacity]; + this.capacity = capacity; + this.size = 0; + } + + public void append(owned T item) { + if (size >= capacity) + { + capacity *= 2; + data.resize(capacity); + } + + + this.data[size++] = item; + } + + public T pop() { + if (size <= capacity / 4) + { + capacity /= 2; + data.resize(capacity); + } + + return(this.data[--size]); + } + + public new T get(int index) { + bound_check(index); + + return(this.data[index]); + } + + public new void set(int index, owned T item) { + bound_check(index); + + this.data[index] = item; + } + + public new Vec slice(int begin, int end) { + bound_check(begin); + bound_check(end - 1); + + return new Vec.with_data(this.data[begin:end]); + } + + public bool contains(T item) { + bool flag = false; + + for (int i = 0; i < size; i++) { + if (data[i] == item) { + flag = true; + break; + } + } + + return flag; + } + + public class Iterator { + private int index; + private Vec vec; + + public Iterator(Vec vec) { + this.vec = vec; + this.index = 0; + } + + public bool next() { + return index < vec.size -1 ; + } + + public T get() { + return this.vec[this.index++]; + } + } + + private inline void bound_check(int index) { + if (index < 0 || index >= size) { + error("Array index out of bound (index = %d, size = %d)", index, size); + } + } +} } diff --git a/src/vapi/csv.vapi b/src/vapi/csv.vapi index 37a4f61..72bc29b 100644 --- a/src/vapi/csv.vapi +++ b/src/vapi/csv.vapi @@ -1,78 +1,117 @@ /* libcsv.vapi */ -[CCode(cprefix = "csv_", cheader_filename = "csv.h")] -namespace Csv { - - public const uchar STRICT; - public const uchar REPALL_NL; - public const uchar STRICT_FINI; - public const uchar APPEND_NULL; - public const uchar EMPTY_IS_NULL; - - public const int SUCCESS; - public const int ENOMEN; - public const int ETOOBIG; - public const int EINVALID; - - public const uchar TAB; - public const uchar SPACE; - public const uchar CR; - public const uchar COMMA; - public const uchar QUOTE; - - - - - [Compact] - [CCode(cname = "struct csv_parser", free_function = "csv_free")] - public struct Parser { - - [CCode(cname = "csv_init")] - public Parser(uchar options); - - [CCode(cname = "csv_error")] - public int error(); - - [CCode(cname = "csv_strerror")] - public string strerror(); - - [CCode(cname = "csv_get_opts")] - public int get_opts(); - - [CCode(cname = "csv_set_opts")] - public int set_opts(); - - [CCode(cname = "csv_set_delim")] - public void set_delim(uchar ch); - - [CCode(cname = "csv_set_quote")] - public void set_quote(uchar ch); - - [CCode(cname = "csv_get_delim")] - public uchar get_delim(); - - [CCode(cname = "csv_get_quote")] - public uchar get_quote(); - - [CCode(cname = "csv_get_buffer_size")] - public int get_buffer_size(); - - [CCode(cname = "csv_write", simple_generics = true)] - public static uint32 write_internal < T > (T[] dest, T[] src); - } - - public static string quote(string src) { - - if (src.length >= int.MAX / 2) { - return ""; + [CCode(cprefix = "csv_", cheader_filename = "csv.h")] + namespace Csv { + + public const uchar STRICT; + public const uchar REPALL_NL; + public const uchar STRICT_FINI; + public const uchar APPEND_NULL; + public const uchar EMPTY_IS_NULL; + + public const int SUCCESS; + public const int ENOMEN; + public const int ETOOBIG; + public const int EINVALID; + + public const uchar TAB; + public const uchar SPACE; + public const uchar CR; + public const uchar COMMA; + public const uchar QUOTE; + + [CCode (has_target = false)] + public delegate void FieldCallback(uint8[] field, string[] user_data); + [CCode (has_target = false)] + public delegate void RecordCallback(int end_ch, void * user_data); + + [Compact] + [CCode(cname = "struct csv_parser", free_function = "csv_free")] + public struct Parser { + + [CCode(cname = "csv_init")] + public Parser(uchar options); + + [CCode(cname = "csv_error")] + public int error(); + + [CCode(cname = "csv_strerror")] + public string strerror(); + + [CCode(cname = "csv_get_opts")] + public int get_opts(); + + [CCode(cname = "csv_set_opts")] + public int set_opts(); + + [CCode(cname = "csv_set_delim")] + public void set_delim(uchar ch); + + [CCode(cname = "csv_set_quote")] + public void set_quote(uchar ch); + + [CCode(cname = "csv_get_delim")] + public uchar get_delim(); + + [CCode(cname = "csv_get_quote")] + public uchar get_quote(); + + [CCode(cname = "csv_get_buffer_size")] + public int get_buffer_size(); + + + [CCode(cname = "csv_parse", simple_generics = true)] + public uint32 parse < T > (T[] src, FieldCallback? field_cb, RecordCallback? record_cb, [CCode (array_length = false)] string[] collector); + + [CCode(cname = "csv_fini", simple_generics = true)] + public uint32 fini < T > (FieldCallback? field_cb, RecordCallback? record_cb, [CCode (array_length = false)] string[] collector); + + [CCode(cname = "csv_write", simple_generics = true)] + public static uint32 write_internal < T > (T[] dest, T[] src); + } + + public static string quote(string src) { + + if (src.length >= int.MAX / 2) { + return ""; + } + + int buf_size = src.length > 1024 ? src.length * 2 : 2048; + + uint8[] buf = new uint8[buf_size]; + Parser.write_internal < uint8 > (buf, src.data); + + return (string) buf; + } + + public static string[] parse_row(string csv_row) { + Csv.Parser p = Csv.Parser(Csv.APPEND_NULL); + string[] results = new string[1024]; + + uint32 parsed_bytes = p.parse (csv_row.data, (raw_field, collector) => { + int i = 0; + while (collector[i] != null)i++; + string field = (string) raw_field; + collector[i] = field; + }, null, results); + + p.fini ((raw_field, collector) => { + int i = 0; + while (collector[i] != null)i++; + string field = (string) raw_field; + collector[i] = field; + }, null, results); + + if (parsed_bytes != csv_row.length) { + return new string[0]; } - int buf_size = src.length > 1024 ? src.length * 2 : 2048; - - uint8[] buf = new uint8[buf_size]; - Parser.write_internal < uint8 > (buf, src.data); + int i = 0; + while (results[i] != null)i++; + results.resize(i); - return (string) buf; + return results; } -} \ No newline at end of file + + } \ No newline at end of file diff --git a/src/vapi/libgvc.vapi b/src/vapi/libgvc.vapi new file mode 100644 index 0000000..dff35c4 --- /dev/null +++ b/src/vapi/libgvc.vapi @@ -0,0 +1,249 @@ +/* + * libgvc.vapi + * + * Copyright (C) 2009 Martin Olsson + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Author: + * Martin Olsson + */ + +[CCode (cprefix = "", lower_case_cprefix = "", cheader_filename = "gvc.h")] +namespace Gvc { + + [CCode (cname = "aginitlib")] + public void initlib ( size_t graphinfo, size_t nodeinfo, size_t edgeinfo); + + [CCode (cname = "aginit")] +#if WITH_CGRAPH + public void init (Graph g, int kind, char[] rec_name, bool move_to_front); +#else + public void init (); +#endif + +#if WITH_CGRAPH + [SimpleType] + [CCode (cname = "Agdesc_t")] + public struct Desc { + } + + public Desc Agdirected; // Desc.DIRECTED | Desc.MAINGRAPH; + public Desc Agstrictdirected; // Desc.DIRECTED | Desc.STRICT | Desc.MAINGRAPH; + public Desc Agundirected; // Desc.MAINGRAPH; + public Desc Agstrictundirected; // Desc.STRICT | Desc.MAINGRAPH; +#else + [CCode (cprefix = "", has_type_id = false)] + public enum GraphKind { + AGRAPH, + AGRAPHSTRICT, + AGDIGRAPH, + AGDIGRAPHSTRICT, + AGMETAGRAPH, + } +#endif + + [CCode (cname = "agerrlevel_t", cprefix = "", has_type_id = false)] + public enum ErrorLevel { + AGWARN, + AGERR, + AGMAX, + AGPREV + } + + [Compact] + [CCode (cname = "GVC_t", free_function = "gvFreeContext")] + public class Context { + [CCode (cname = "gvContext")] + public Context (); + + [CCode (cname = "gvParseArgs")] + public int parse_args ( [CCode (array_length_pos = 0.9)] string[] argv ); + + [CCode (cname = "gvLayout")] + public int layout (Graph graph, [CCode (type = "char*")] string layout_engine); + + [CCode (cname = "gvFreeLayout")] + public int free_layout (Graph graph); + + [CCode (cname = "gvRender")] + public int render (Graph graph, [CCode (type = "char*")] string file_type, GLib.FileStream? file); + + [CCode (cname = "gvRenderFilename")] + public int render_filename (Graph graph, [CCode (type = "char*")] string file_type, [CCode (type = "char*")] string filename); + + [CCode (cname = "gvLayoutJobs")] + public int layout_jobs (Graph graph); + + [CCode (cname = "gvRenderJobs")] + public int render_jobs (Graph graph); + + [CCode (cname = "gvRenderData")] + public int render_data (Graph graph, [CCode (type = "char*")] string file_type, [CCode (array_length_type = "unsigned int", type = "char**")] out uint8[] output_data); + } + + [Compact] + [CCode (cname = "Agnode_t", ref_function = "", unref_function = "", free_function = "")] + public class Node { + [CCode (cname = "agnameof")] + public unowned string name (); + + [CCode (cname = "agget")] + public unowned string? get ([CCode (type = "char*")] string attribute_name); + + [CCode (cname = "agset")] + public int set ([CCode (type = "char*")] string attribute_name, [CCode (type = "char*")] string attribute_value); + + [CCode (cname = "agsafeset")] + public void safe_set ([CCode (type = "char*")] string attribute_name, [CCode (type = "char*")] string attribute_value, [CCode (type = "char*")] string? default_value); + } + + [Compact] + [CCode (cname = "Agedge_t", ref_function = "", unref_function = "", free_function = "")] + public class Edge { + [CCode (cname = "agget")] + public unowned string? get ([CCode (type = "char*")] string attribute_name); + + [CCode (cname = "agset")] + public int set ([CCode (type = "char*")] string attribute_name, [CCode (type = "char*")] string attribute_value); + + [CCode (cname = "agsafeset")] + public int safe_set ([CCode (type = "char*")] string attribute_name, [CCode (type = "char*")] string attribute_value, [CCode (type = "char*")] string? default_value); + } + + [Compact] + [CCode (cname = "Agraph_t", free_function = "agclose")] + public class Graph { + [CCode (cname = "agopen")] +#if WITH_CGRAPH + public Graph ([CCode (type = "char*")] string graph_name, Desc desc, int disc = 0); +#else + public Graph ([CCode (type = "char*")] string graph_name, GraphKind kind); +#endif + + [CCode (cname = "agread")] + public static Graph read (GLib.FileStream file); + + [CCode (cname = "agmemread")] + public static Graph read_string (string str); + + [CCode (cname = "agnode")] +#if WITH_CGRAPH + public unowned Node create_node ([CCode (type = "char*")] string node_name, int createflag = 1); +#else + public unowned Node create_node ([CCode (type = "char*")] string node_name); +#endif + + [CCode (cname = "agedge")] +#if WITH_CGRAPH + public unowned Edge create_edge (Node from, Node to, string? name = null, int createflag = 1); +#else + public unowned Edge create_edge (Node from, Node to); +#endif + + [CCode (cname = "agfindedge")] + public unowned Edge? find_edge (Node from, Node tO); + + [CCode (cname = "agsubg")] +#if WITH_CGRAPH + public unowned Graph create_subgraph ([CCode (type = "char*")] string? name, int createflag = 1); +#else + public unowned Graph create_subgraph ([CCode (type = "char*")] string? name); +#endif + + [CCode (cname = "agfindsubg")] + public unowned Graph? find_subgraph ([CCode (type = "char*")] string name); + + [CCode (cname = "agidsubg")] +#if WITH_CGRAPH + public unowned Graph create_subgraph_id (ulong id, int createflag = 1); +#else + public unowned Graph create_subgraph_id (ulong id); +#endif + + [CCode (cname = "agfstsubg")] + public unowned Graph? get_first_subgraph (); + + [CCode (cname = "agnxtsubg")] + public unowned Graph? get_next_subgraph (); + + [CCode (cname = "agparent")] + public unowned Graph? get_parent_graph (); + + [CCode (cname = "agdelsubg")] + public int delete_subgraph (Graph subgraph); + + [CCode (cname = "agfindnode")] + public unowned Node? find_node ([CCode (type = "char*")] string node_name); + + [CCode (cname = "agfstnode")] + public unowned Node? get_first_node (); + + [CCode (cname = "agnxtnode")] + public unowned Node? get_next_node (Node node); + + [CCode (cname = "agget")] + public unowned string? get ([CCode (type = "char*")] string attribute_name); + + [CCode (cname = "agset")] + public int set ([CCode (type = "char*")] string attribute_name, [CCode (type = "char*")] string attribute_value); + + [CCode (cname = "agsafeset")] + public int safe_set ([CCode (type = "char*")] string attribute_name, [CCode (type = "char*")] string attribute_value, [CCode (type = "char*")] string? default_value); + + [CCode (cname = "AG_IS_DIRECTED")] + public bool is_directed (); + + [CCode (cname = "AG_IS_STRICT")] + public bool is_strict (); + } + + [CCode (cname = "char", copy_function = "agdupstr_html", free_function = "")] + public class HtmlString : string { + [CCode (cname = "agstrdup_html")] + public static HtmlString make_html (Graph graph, string markup); + + [CCode (cname = "agstrfree")] + public static HtmlString free_html (Graph graph, string markup); + } + + [CCode(cprefix = "ag")] + namespace Error { + [CCode (cname = "agerrno")] + public static ErrorLevel errno; + + [CCode (cname = "agerr")] + [PrintfFormat] + public static int error (ErrorLevel level, string fmt, ...); + + [CCode (cname = "agerrors")] + public static int errors (); + + [CCode (cname = "agseterr")] + public static void set_error (ErrorLevel err); + + [CCode (cname = "aglasterr")] + public static string? last_error (); + + [CCode (cname = "agerrorf")] + [PrintfFormat] + public static void errorf (string format, ...); + + [CCode (cname = "agwarningf")] + [PrintfFormat] + void warningf (string fmt, ...); + } + +} diff --git a/src/viewmodels/TableStructureViewModel.vala b/src/viewmodels/TableStructureViewModel.vala index e234c71..c5595e0 100644 --- a/src/viewmodels/TableStructureViewModel.vala +++ b/src/viewmodels/TableStructureViewModel.vala @@ -2,10 +2,12 @@ namespace Psequel { public class TableStructureViewModel : Observer, Object { public SQLService sql_service { get; set; } public Table selected_table { get; set; } + public Schema current_schema { get; set; } public ObservableList columns { get; set; default = new ObservableList (); } public ObservableList indexes { get; set; default = new ObservableList (); } public ObservableList foreign_keys { get; set; default = new ObservableList (); } + public ObservableList primary_keys { get; set; default = new ObservableList (); } public TableStructureViewModel(SQLService sql_service) { @@ -21,12 +23,16 @@ public class TableStructureViewModel : Observer, Object { { case Event.SCHEMA_CHANGED: var schema = event.data as Schema; - load_data.begin(schema); + this.current_schema = schema; + // load_data.begin(); break; case Event.SELECTED_TABLE_CHANGED: var table = event.data as Table; - selected_table = table; + load_data.begin((obj, res) => { + load_data.end(res); + selected_table = table; + }); break; default: @@ -34,14 +40,16 @@ public class TableStructureViewModel : Observer, Object { } } - private async void load_data(Schema schema) { + private async void load_data() { columns.clear(); indexes.clear(); foreign_keys.clear(); + primary_keys.clear(); - columns.append_all(yield _get_columns(schema)); - indexes.append_all(yield _get_indexes(schema)); - foreign_keys.append_all(yield _get_fks(schema)); + columns.append_all(yield _get_columns(this.current_schema)); + indexes.append_all(yield _get_indexes(this.current_schema)); + foreign_keys.append_all(yield _get_fks(this.current_schema)); + primary_keys.append_all(yield _get_pks(this.current_schema)); debug("cols: %d indx: %d fks: %d", columns.size, indexes.size, foreign_keys.size); } @@ -101,8 +109,8 @@ public class TableStructureViewModel : Observer, Object { var list = new List (); try { - var query = new Query.with_params(FK_SQL, { schema.name }); - var relation = yield sql_service.exec_query_params(query); + var query = new Query(FK_SQL2); + var relation = yield sql_service.exec_query(query); foreach (var row in relation) { @@ -110,7 +118,10 @@ public class TableStructureViewModel : Observer, Object { fk.schemaname = schema.name; fk.name = row[0]; fk.table = row[1]; - fk.fk_def = row[2]; + fk.fk_table = row[3]; + + fk.columns_v2 = parse_array_result(row[2]); + fk.fk_columns_v2 = parse_array_result(row[4]); list.append(fk); } @@ -121,6 +132,36 @@ public class TableStructureViewModel : Observer, Object { return(list); } + private async List _get_pks(Schema schema) { + var list = new List (); + + try { + var query = new Query.with_params(PK_SQL, { schema.name }); + var relation = yield sql_service.exec_query_params(query); + + foreach (var row in relation) + { + var pk = new PrimaryKey(); + pk.schemaname = schema.name; + pk.name = row[0]; + pk.table = row[1]; + pk.columns = parse_array_result(row[2]); + + list.append(pk); + } + } catch (PsequelError err) { + debug(err.message); + } + + return(list); + } + + private string[] parse_array_result(string array_str) { + int len = array_str.length - 2; + string content = array_str.substring(1, len); + return(Csv.parse_row(content)); + } + public const string COLUMN_SQL = """ SELECT column_name, table_name, case @@ -147,5 +188,39 @@ public class TableStructureViewModel : Observer, Object { ON nsp.oid = connamespace WHERE con.contype = 'f' AND nsp.nspname = $1; """; + + public const string PK_SQL = """ + SELECT + con.conname, + cls1.relname AS table, + ARRAY_AGG(attr1.attname) AS columns + FROM pg_catalog.pg_constraint con + JOIN pg_catalog.pg_class cls1 ON con.conrelid = cls1.oid + JOIN pg_catalog.pg_attribute attr1 ON attr1.attrelid = cls1.oid + JOIN pg_catalog.pg_namespace nsp ON nsp.oid = connamespace + WHERE con.contype = 'p' + AND attr1.attnum = ANY(con.conkey) + AND nsp.nspname = $1 + GROUP BY con.oid, cls1.relname; + """; + + public const string FK_SQL2 = """ + SELECT + con.conname, + cls1.relname AS src_table, + ARRAY_AGG(attr1.attname) AS src_columns, + cls2.relname AS dest_table, + ARRAY_AGG(attr2.attname) AS dest_columns + FROM pg_catalog.pg_constraint con + JOIN pg_catalog.pg_class cls1 ON con.conrelid = cls1.oid + JOIN pg_catalog.pg_class cls2 ON con.confrelid = cls2.oid + JOIN pg_catalog.pg_attribute attr1 ON attr1.attrelid = cls1.oid + JOIN pg_catalog.pg_attribute attr2 ON attr2.attrelid = cls2.oid + WHERE con.contype = 'f' + AND con.confrelid > 0 + AND attr1.attnum = ANY(con.conkey) + AND attr2.attnum = ANY(con.confkey) + GROUP BY con.oid, cls1.relname, cls2.relname + """; } } From 17679f41d720930ec78b94325066217fcd70b529 Mon Sep 17 00:00:00 2001 From: ppvan Date: Sun, 31 Mar 2024 20:33:22 +0700 Subject: [PATCH 02/10] refactor: rely on gobject signal instead of handmade observer --- src/application.vala | 15 ------ src/services/NavigationService.vala | 7 +++ src/services/SQLCompletionService.vala | 16 +++--- src/ui/views/SchemaView.vala | 32 +++++------- src/utils/Event.vala | 58 +++++---------------- src/viewmodels/BaseViewModel.vala | 12 +---- src/viewmodels/ConnectionViewModel.vala | 4 +- src/viewmodels/SchemaViewModel.vala | 28 +++++----- src/viewmodels/TableDataViewModel.vala | 13 ++--- src/viewmodels/TableStructureViewModel.vala | 24 +++------ src/viewmodels/TableViewModel.vala | 12 ++--- src/viewmodels/ViewDataViewModel.vala | 13 ++--- src/viewmodels/ViewStructureViewModel.vala | 22 +++----- src/viewmodels/ViewViewModel.vala | 16 +++--- 14 files changed, 91 insertions(+), 181 deletions(-) diff --git a/src/application.vala b/src/application.vala index 3e309bd..1c493e1 100644 --- a/src/application.vala +++ b/src/application.vala @@ -293,21 +293,6 @@ public class Application : Adw.Application { container.register(query_history_vm); container.register(query_vm); - // events - conn_vm.subcribe(Event.ACTIVE_CONNECTION, sche_vm); - - sche_vm.subcribe(Event.SCHEMA_CHANGED, completer); - sche_vm.subcribe(Event.SCHEMA_CHANGED, table_vm); - sche_vm.subcribe(Event.SCHEMA_CHANGED, view_vm); - sche_vm.subcribe(Event.SCHEMA_CHANGED, table_structure_vm); - sche_vm.subcribe(Event.SCHEMA_CHANGED, view_structure_vm); - - table_vm.subcribe(Event.SELECTED_TABLE_CHANGED, table_structure_vm); - table_vm.subcribe(Event.SELECTED_TABLE_CHANGED, table_data_vm); - - view_vm.subcribe(Event.SELECTED_VIEW_CHANGED, view_structure_vm); - view_vm.subcribe(Event.SELECTED_VIEW_CHANGED, view_data_vm); - return(container); } diff --git a/src/services/NavigationService.vala b/src/services/NavigationService.vala index 82512b1..d120df9 100644 --- a/src/services/NavigationService.vala +++ b/src/services/NavigationService.vala @@ -7,6 +7,13 @@ public class NavigationService : Object { public string current_view { get; set; default = CONNECTION_VIEW; } public NavigationService() { + EventBus.instance().connection_disabled.connect_after(() => { + this.navigate(CONNECTION_VIEW); + }); + + EventBus.instance().connection_active.connect_after((_conn) => { + this.navigate(QUERY_VIEW); + }); } public void navigate(string view_name) { diff --git a/src/services/SQLCompletionService.vala b/src/services/SQLCompletionService.vala index 6330cd7..922fd3b 100644 --- a/src/services/SQLCompletionService.vala +++ b/src/services/SQLCompletionService.vala @@ -122,7 +122,7 @@ public class SQLCompletionService : Object, GtkSource.CompletionProvider { } } -public class CompleterService : Object, Observer { +public class CompleterService : Object { public SQLService sql_service { get; construct; } public List schemas { get; owned set; } @@ -135,6 +135,12 @@ public class CompleterService : Object, Observer { Object(sql_service: sql_service); } + construct { + EventBus.instance().schema_changed.connect((schema) => { + refresh_data(schema); + }); + } + public List get_suggestions(SchemaContext context, string last_word) { // context maybe use in the future for smart completion or something. debug("Lastword: %s", last_word); @@ -151,14 +157,6 @@ public class CompleterService : Object, Observer { return(keywords); } - public void update(Event event) { - if (event.type == Event.SCHEMA_CHANGED) - { - var schema = (Schema)event.data; - refresh_data(schema); - } - } - private List suggest_keywords(string prefix = "") { var candidates = new List (); diff --git a/src/ui/views/SchemaView.vala b/src/ui/views/SchemaView.vala index 4cdf876..ba24626 100644 --- a/src/ui/views/SchemaView.vala +++ b/src/ui/views/SchemaView.vala @@ -42,6 +42,15 @@ public class SchemaView : Adw.Bin { view_viewmodel.select_view((View)view); }); + EventBus.instance().schema_reload.connect_after(() => { + var window = get_parrent_window(this); + Adw.Toast toast; + toast = new Adw.Toast("Schema Reloaded") { + timeout = 1, + }; + window.add_toast(toast); + }); + var table_name_expression = new Gtk.PropertyExpression(typeof(Table), null, "name"); var view_name_expression = new Gtk.PropertyExpression(typeof(View), null, "name"); @@ -83,22 +92,8 @@ public class SchemaView : Adw.Bin { [GtkCallback] private void reload_btn_clicked(Gtk.Button btn) { btn.sensitive = false; - schema_viewmodel.reload.begin((obj, res) => { - var window = get_parrent_window(this); - Adw.Toast toast; - try { - schema_viewmodel.reload.end(res); - toast = new Adw.Toast("Schema Reloaded") { - timeout = 1, - }; - } catch (Psequel.PsequelError err) { - toast = new Adw.Toast(err.message) { - timeout = 1, - }; - } - window.add_toast(toast); - btn.sensitive = true; - }); + EventBus.instance().schema_reload(); + btn.sensitive = true; } [GtkCallback] @@ -109,8 +104,9 @@ public class SchemaView : Adw.Bin { [GtkCallback] private void logout_btn_clicked(Gtk.Button btn) { - schema_viewmodel.logout.begin(); - navigation_service.navigate(NavigationService.CONNECTION_VIEW); + btn.sensitive = false; + EventBus.instance().connection_disabled(); + btn.sensitive = true; } [GtkChild] diff --git a/src/utils/Event.vala b/src/utils/Event.vala index f37c3b2..a84be19 100644 --- a/src/utils/Event.vala +++ b/src/utils/Event.vala @@ -1,53 +1,23 @@ namespace Psequel { -public class EventManager : Object { - private List targets; - - private class EventTarget { - public string event_type; - public Observer observer; - } - - public new void notify(string event_type, Object data) { - foreach (EventTarget target in targets) +public class EventBus : Object { + public signal void schema_changed(Schema schema); + public signal void connection_active(Connection connection); + public signal void connection_disabled(); + public signal void selected_table_changed(Table table); + public signal void selected_view_changed(View view); + public signal void schema_reload(); + + private static EventBus _instance; + public static EventBus instance() { + if (EventBus._instance == null) { - if (target.event_type == event_type) - { - Event event = new Event(event_type, data); - target.observer.update(event); - } + _instance = new EventBus(); } - } - public EventManager() { - Object(); - targets = new List (); + return(_instance); } - public void subcribe(string event_type, Observer observer) { - EventTarget target = new EventTarget(); - target.event_type = event_type; - target.observer = observer; - - targets.append(target); + private EventBus() { } } - -public class Event : Object { - public const string SCHEMA_CHANGED = "schema-changed"; - public const string SELECTED_TABLE_CHANGED = "selected-table-changed"; - public const string SELECTED_VIEW_CHANGED = "selected-view-changed"; - public const string ACTIVE_CONNECTION = "active-connection"; - public string type; - public Object data; - - public Event(string type, Object data) { - base(); - this.type = type; - this.data = data; - } -} - -public interface Observer : Object { - public abstract void update(Event event); -} } diff --git a/src/viewmodels/BaseViewModel.vala b/src/viewmodels/BaseViewModel.vala index b0e9dd0..75901e7 100644 --- a/src/viewmodels/BaseViewModel.vala +++ b/src/viewmodels/BaseViewModel.vala @@ -5,20 +5,12 @@ public abstract class BaseViewModel : Object { public signal void navigate_to(string view); - protected EventManager event_manager; + // protected EventManager event_manager; protected BaseViewModel() { - event_manager = new EventManager(); + // event_manager = new EventManager(); // debug ("BaseViewModel created"); } - public void subcribe(string event_type, Observer observer) { - event_manager.subcribe(event_type, observer); - } - - protected void emit_event(string event_type, Object data) { - debug("Emit: %s", event_type); - event_manager.notify(event_type, data); - } } } diff --git a/src/viewmodels/ConnectionViewModel.vala b/src/viewmodels/ConnectionViewModel.vala index 2a7d234..51ac126 100644 --- a/src/viewmodels/ConnectionViewModel.vala +++ b/src/viewmodels/ConnectionViewModel.vala @@ -81,9 +81,7 @@ public class ConnectionViewModel : BaseViewModel { this.current_state = State.CONNECTING; try { yield sql_service.connect_db(connection); - - this.navigation_service.navigate(NavigationService.QUERY_VIEW); - this.emit_event(Event.ACTIVE_CONNECTION, connection); + EventBus.instance().connection_active(connection); } catch (PsequelError err) { this.err_msg = err.message.dup(); debug("Error: %s", err.message); diff --git a/src/viewmodels/SchemaViewModel.vala b/src/viewmodels/SchemaViewModel.vala index 4597bba..4973531 100644 --- a/src/viewmodels/SchemaViewModel.vala +++ b/src/viewmodels/SchemaViewModel.vala @@ -1,5 +1,5 @@ namespace Psequel { -public class SchemaViewModel : BaseViewModel, Observer { +public class SchemaViewModel : BaseViewModel { const string DEFAULT = "public"; public ObservableList schemas { get; set; default = new ObservableList (); } @@ -19,8 +19,22 @@ public class SchemaViewModel : BaseViewModel, Observer { this.schema_service = service; this.notify["current-schema"].connect(() => { - this.emit_event(Event.SCHEMA_CHANGED, current_schema); + EventBus.instance().schema_changed(current_schema); }); + + EventBus.instance().connection_active.connect(() => { + Timeout.add_once(300, () => { + database_connected.begin(); + }); + }); + + EventBus.instance().schema_reload.connect(() => { + this.reload.begin(); + }); + + EventBus.instance().connection_disabled.connect(() => { + this.logout.begin(); + }); } public void select_index(int index) { @@ -32,16 +46,6 @@ public class SchemaViewModel : BaseViewModel, Observer { select_schema.begin(schemas[index]); } - public void update(Event event) { - if (event.type == Event.ACTIVE_CONNECTION) - { - // delay for ui to play animation - Timeout.add_once(300, () => { - database_connected.begin(); - }); - } - } - public async void reload() throws PsequelError { if (current_schema == null) { diff --git a/src/viewmodels/TableDataViewModel.vala b/src/viewmodels/TableDataViewModel.vala index 4f06eb2..8d364bc 100644 --- a/src/viewmodels/TableDataViewModel.vala +++ b/src/viewmodels/TableDataViewModel.vala @@ -1,5 +1,5 @@ namespace Psequel { -public class TableDataViewModel : BaseViewModel, Observer { +public class TableDataViewModel : BaseViewModel { public const int MAX_FETCHED_ROW = 50; public Table ?selected_table { get; set; } @@ -54,15 +54,10 @@ public class TableDataViewModel : BaseViewModel, Observer { current_page = 0; reload_data.begin(); }); - } - public void update(Event event) { - switch (event.type) - { - case Event.SELECTED_TABLE_CHANGED: - selected_table = event.data as Table; - break; - } + EventBus.instance().selected_table_changed.connect((table) => { + selected_table = table; + }); } public async void reload_data() { diff --git a/src/viewmodels/TableStructureViewModel.vala b/src/viewmodels/TableStructureViewModel.vala index c5595e0..ee5b7cc 100644 --- a/src/viewmodels/TableStructureViewModel.vala +++ b/src/viewmodels/TableStructureViewModel.vala @@ -1,5 +1,5 @@ namespace Psequel { -public class TableStructureViewModel : Observer, Object { +public class TableStructureViewModel : Object { public SQLService sql_service { get; set; } public Table selected_table { get; set; } public Schema current_schema { get; set; } @@ -14,30 +14,18 @@ public class TableStructureViewModel : Observer, Object { base(); // debug ("TableStructureViewModel created "); this.sql_service = sql_service; - - // selected_table = table; - } - - public void update(Event event) { - switch (event.type) - { - case Event.SCHEMA_CHANGED: - var schema = event.data as Schema; + EventBus.instance().schema_changed.connect((schema) => { this.current_schema = schema; - // load_data.begin(); - break; + }); - case Event.SELECTED_TABLE_CHANGED: - var table = event.data as Table; + EventBus.instance().selected_table_changed.connect((table) => { load_data.begin((obj, res) => { load_data.end(res); selected_table = table; }); - break; + }); - default: - break; - } + // selected_table = table; } private async void load_data() { diff --git a/src/viewmodels/TableViewModel.vala b/src/viewmodels/TableViewModel.vala index a67dddb..bfe591e 100644 --- a/src/viewmodels/TableViewModel.vala +++ b/src/viewmodels/TableViewModel.vala @@ -1,5 +1,5 @@ namespace Psequel { -public class TableViewModel : BaseViewModel, Observer { +public class TableViewModel : BaseViewModel { public Schema schema { get; set; } public ObservableList tables { get; set; default = new ObservableList
(); } public Table ?selected_table { get; set; } @@ -13,17 +13,13 @@ public class TableViewModel : BaseViewModel, Observer { base(); this.sql_service = sql_service; this.notify["selected-table"].connect(() => { - this.emit_event(Event.SELECTED_TABLE_CHANGED, selected_table); + EventBus.instance().selected_table_changed(selected_table); }); - } - public void update(Event event) { - if (event.type == Event.SCHEMA_CHANGED) - { - schema = (Schema)event.data; + EventBus.instance().schema_changed.connect((schema) => { tables.clear(); load_tables.begin(schema); - } + }); } public void select_table(Table ?table) { diff --git a/src/viewmodels/ViewDataViewModel.vala b/src/viewmodels/ViewDataViewModel.vala index 744dd0c..f6ff1cd 100644 --- a/src/viewmodels/ViewDataViewModel.vala +++ b/src/viewmodels/ViewDataViewModel.vala @@ -1,5 +1,5 @@ namespace Psequel { -public class ViewDataViewModel : Object, Observer { +public class ViewDataViewModel : Object { public View ?selected_view { get; set; } // public View? current_view {get; set;} @@ -52,15 +52,10 @@ public class ViewDataViewModel : Object, Observer { has_next_page = true; } }); - } - public void update(Event event) { - switch (event.type) - { - case Event.SELECTED_VIEW_CHANGED: - selected_view = event.data as View; - break; - } + EventBus.instance().selected_view_changed.connect((view) => { + selected_view = view; + }); } public async void reload_data() { diff --git a/src/viewmodels/ViewStructureViewModel.vala b/src/viewmodels/ViewStructureViewModel.vala index b7089ea..c87ad8b 100644 --- a/src/viewmodels/ViewStructureViewModel.vala +++ b/src/viewmodels/ViewStructureViewModel.vala @@ -1,5 +1,5 @@ namespace Psequel { -public class ViewStructureViewModel : Object, Observer { +public class ViewStructureViewModel : Object { public SQLService sql_service { get; private set; } public View selected_view { get; set; } @@ -10,24 +10,14 @@ public class ViewStructureViewModel : Object, Observer { public ViewStructureViewModel(SQLService sql_service) { base(); this.sql_service = sql_service; - } - - public void update(Event event) { - switch (event.type) - { - case Event.SCHEMA_CHANGED: - var schema = event.data as Schema; - load_data.begin(schema); - break; - case Event.SELECTED_VIEW_CHANGED: - var view = event.data as View; + EventBus.instance().selected_view_changed.connect((view) => { selected_view = view; - break; + }); - default: - break; - } + EventBus.instance().schema_changed.connect((schema) => { + load_data.begin(schema); + }); } private async void load_data(Schema schema) { diff --git a/src/viewmodels/ViewViewModel.vala b/src/viewmodels/ViewViewModel.vala index 9783e38..59b9214 100644 --- a/src/viewmodels/ViewViewModel.vala +++ b/src/viewmodels/ViewViewModel.vala @@ -1,6 +1,6 @@ namespace Psequel { /* View here is database view (virtual tables), not UI */ -public class ViewViewModel : BaseViewModel, Observer { +public class ViewViewModel : BaseViewModel { public ObservableList views { get; set; default = new ObservableList (); } public View ?selected_view { get; set; } @@ -12,17 +12,13 @@ public class ViewViewModel : BaseViewModel, Observer { base(); this.sql_service = service; this.notify["selected-view"].connect(() => { - this.emit_event(Event.SELECTED_VIEW_CHANGED, selected_view); + EventBus.instance().selected_view_changed(selected_view); }); - } - public void update(Event event) { - if (event.type == Event.SCHEMA_CHANGED) - { - schema = (Schema)event.data; - views.clear(); - load_views.begin(schema); - } + EventBus.instance().schema_changed.connect((schema) => { + views.clear(); + load_views.begin(schema); + }); } public void select_view(View ?view) { From ef633793cf990637e4708cdc6db7ae2fcfed4930 Mon Sep 17 00:00:00 2001 From: ppvan Date: Mon, 1 Apr 2024 22:52:07 +0700 Subject: [PATCH 03/10] feat: add SSL connection support --- res/gtk/connection-view.blp | 752 +++++++++--------- .../application-certificate-symbolic.svg | 1 + res/gtk/icons/filemanager-app-symbolic.svg | 6 + res/psequel.gresource.xml | 2 + src/models/Connection.vala | 11 +- src/ui/views/ConnectionView.vala | 52 ++ 6 files changed, 458 insertions(+), 366 deletions(-) create mode 100644 res/gtk/icons/application-certificate-symbolic.svg create mode 100644 res/gtk/icons/filemanager-app-symbolic.svg diff --git a/res/gtk/connection-view.blp b/res/gtk/connection-view.blp index b948c30..03d6541 100644 --- a/res/gtk/connection-view.blp +++ b/res/gtk/connection-view.blp @@ -3,414 +3,438 @@ using Gio 2.0; using Adw 1; menu primary_menu { - section { - item { - custom: "style-switcher"; - } - } - - section { - item { - label: _("_New Window"); - action: "app.new-window"; - } - } - - section { - item { - label: _("_Import"); - action: "win.import"; - } - - item { - label: _("_Export"); - action: "win.export"; + section { + item { + custom: "style-switcher"; + } } - } - section { - item { - label: _("_Preferences"); - action: "app.preferences"; + section { + item { + label: _("_New Window"); + action: "app.new-window"; + } } - item { - label: _("_Keyboard Shortcuts"); - action: "win.show-help-overlay"; - } + section { + item { + label: _("_Import"); + action: "win.import"; + } - item { - label: _("_About"); - action: "app.about"; + item { + label: _("_Export"); + action: "win.export"; + } } - } -} -template $PsequelConnectionView: Adw.Bin { - hexpand: true; - vexpand: true; - - Gtk.Paned paned { - // can-shrink: false; - shrink-start-child: false; - shrink-end-child: false; - - [start] - Box { - width-request: 300; - orientation: vertical; - spacing: 4; - margin-top: 4; - - Box { - Label { - styles [ - "text-bold" - ] - - margin-top: 6; - margin-start: 8; - halign: center; - margin-bottom: 6; - use-markup: true; - label: "Connections"; + section { + item { + label: _("_Preferences"); + action: "app.preferences"; } - Box { - spacing: 4; - hexpand: true; - halign: end; - - Button { - tooltip-text: "Add new connection"; - - styles [ - "flat" - ] - - icon-name: "plus-large-symbolic"; - clicked => $add_new_connection(); - } + item { + label: _("_Keyboard Shortcuts"); + action: "win.show-help-overlay"; } - } - - ScrolledWindow { - vexpand: true; - ListView listview { - styles [ - "navigation-sidebar" - ] - - model: SingleSelection selection_model { - model: bind template.viewmodel as <$PsequelConnectionViewModel>.connections; - autoselect: true; - }; - - activate => $active_connection(); - - factory: BuilderListItemFactory { - resource: "/me/ppvan/psequel/gtk/connection-listitem.ui"; - }; + item { + label: _("_About"); + action: "app.about"; } - } } +} - // $PsequelConnectionSidebar sidebar { - // width-request: 300; - // connections: bind template.viewmodel as <$PsequelConnectionViewModel>.connections; - // selected-connection: bind template.viewmodel as <$PsequelConnectionViewModel>.selected-connection; - // request-new-connection => $add_new_connection(); - // request_dup_connection => $dup_connection(); - // request_remove_connection => $remove_connection(); - // request_connect_database => $active_connection(); - // } - - [end] - Adw.Bin { - child: WindowHandle { - Box { - orientation: vertical; - hexpand: true; - vexpand: true; - - Adw.HeaderBar header { - styles [ - "flat" - ] - - [title] - Label { - label: ""; - } - - // Dupplicate primary_menu in query-view.blp. - // Update both if you change something - - [end] - MenuButton { - icon-name: "open-menu-symbolic"; - menu-model: primary_menu; - - popover: Gtk.PopoverMenu { - menu-model: primary_menu; - - [style-switcher] - $PsequelStyleSwitcher {} - }; - } - } - - Adw.Clamp { - valign: start; - hexpand: true; - vexpand: true; - maximum-size: 700; - - child: Box { - orientation: vertical; - - Box { - orientation: vertical; +template $PsequelConnectionView: Adw.Bin { + hexpand: true; + vexpand: true; - styles [ - "connection-form" - ] + Gtk.Paned paned { + // can-shrink: false; + shrink-start-child: false; + shrink-end-child: false; - // vexpand: true; - spacing: 4; + [start] + Box { + width-request: 300; + orientation: vertical; + spacing: 4; + margin-top: 4; + Box { Label { - margin-top: 20; - margin-bottom: 60; - - styles [ - "title-0" - ] + styles [ + "text-bold" + ] - label: "Connect with Psequel"; + margin-top: 6; + margin-start: 8; + halign: center; + margin-bottom: 6; + use-markup: true; + label: "Connections"; } - Grid { - hexpand: true; - vexpand: true; - row-homogeneous: true; - row-spacing: 8; - column-spacing: 8; - margin-start: 4; - margin-end: 4; - - Label { + Box { + spacing: 4; + hexpand: true; halign: end; - label: "Name:"; - layout { - row: 0; - column: 0; - column-span: 3; - } - } + Button { + tooltip-text: "Add new connection"; - Label { - halign: end; - label: "Host:"; + styles [ + "flat" + ] - layout { - row: 1; - column: 0; - column-span: 3; + icon-name: "plus-large-symbolic"; + clicked => $add_new_connection(); } - } - - Label { - halign: end; - label: "User:"; + } + } - layout { - row: 2; - column: 0; - column-span: 3; - } - } + ScrolledWindow { + vexpand: true; - Label { - halign: end; - label: "Password:"; + ListView listview { + styles [ + "navigation-sidebar" + ] - layout { - row: 3; - column: 0; - column-span: 3; - } - } + model: SingleSelection selection_model { + model: bind template.viewmodel as <$PsequelConnectionViewModel>.connections; + autoselect: true; + }; - Label { - halign: end; - label: "Database:"; + activate => $active_connection(); - layout { - row: 4; - column: 0; - column-span: 3; - } - } + factory: BuilderListItemFactory { + resource: "/me/ppvan/psequel/gtk/connection-listitem.ui"; + }; + } + } + } - Entry name_entry { - placeholder-text: "Connection name"; + // $PsequelConnectionSidebar sidebar { + // width-request: 300; + // connections: bind template.viewmodel as <$PsequelConnectionViewModel>.connections; + // selected-connection: bind template.viewmodel as <$PsequelConnectionViewModel>.selected-connection; + // request-new-connection => $add_new_connection(); + // request_dup_connection => $dup_connection(); + // request_remove_connection => $remove_connection(); + // request_connect_database => $active_connection(); + // } + + [end] + Adw.Bin { + child: WindowHandle { + Box { + orientation: vertical; hexpand: true; - activate => $on_entry_activated(); - changed => $on_text_changed(); + vexpand: true; - layout { - row: 0; - column: 3; - column-span: 7; - } - } - - Entry host_entry { - placeholder-text: "localhost"; - activate => $on_entry_activated(); - changed => $on_text_changed(); + Adw.HeaderBar header { + styles [ + "flat" + ] - layout { - row: 1; - column: 3; - column-span: 5; - } - } + [title] + Label { + label: ""; + } - Label { - label: "Port"; - halign: end; + // Dupplicate primary_menu in query-view.blp. + // Update both if you change something - layout { - row: 1; - column: 8; - } - } + [end] + MenuButton { + icon-name: "open-menu-symbolic"; + menu-model: primary_menu; - Entry port_entry { - placeholder-text: "5432"; - activate => $on_entry_activated(); - changed => $on_text_changed(); + popover: Gtk.PopoverMenu { + menu-model: primary_menu; - layout { - row: 1; - column: 9; - column-span: 1; + [style-switcher] + $PsequelStyleSwitcher {} + }; + } } - } - - Entry user_entry { - placeholder-text: "postgres"; - activate => $on_entry_activated(); - changed => $on_text_changed(); - layout { - row: 2; - column: 3; - column-span: 7; + Adw.Clamp { + valign: start; + hexpand: true; + vexpand: true; + maximum-size: 700; + + child: Box { + orientation: vertical; + + Box { + orientation: vertical; + + styles [ + "connection-form" + ] + + // vexpand: true; + spacing: 4; + + Label { + margin-top: 20; + margin-bottom: 60; + + styles [ + "title-0" + ] + + label: "Connect with Psequel"; + } + + Grid { + hexpand: true; + vexpand: true; + row-homogeneous: true; + row-spacing: 8; + column-spacing: 8; + margin-start: 4; + margin-end: 4; + + Label { + halign: end; + label: "Name:"; + + layout { + row: 0; + column: 0; + column-span: 3; + } + } + + Label { + halign: end; + label: "Host:"; + + layout { + row: 1; + column: 0; + column-span: 3; + } + } + + Label { + halign: end; + label: "User:"; + + layout { + row: 2; + column: 0; + column-span: 3; + } + } + + Label { + halign: end; + label: "Password:"; + + layout { + row: 3; + column: 0; + column-span: 3; + } + } + + Label { + halign: end; + label: "Database:"; + + layout { + row: 4; + column: 0; + column-span: 3; + } + } + + Entry name_entry { + placeholder-text: "Connection name"; + hexpand: true; + activate => $on_entry_activated(); + changed => $on_text_changed(); + + layout { + row: 0; + column: 3; + column-span: 7; + } + } + + Entry host_entry { + placeholder-text: "localhost"; + activate => $on_entry_activated(); + changed => $on_text_changed(); + + layout { + row: 1; + column: 3; + column-span: 5; + } + } + + Label { + label: "Port"; + halign: end; + + layout { + row: 1; + column: 8; + } + } + + Entry port_entry { + placeholder-text: "5432"; + activate => $on_entry_activated(); + changed => $on_text_changed(); + + layout { + row: 1; + column: 9; + column-span: 1; + } + } + + Entry user_entry { + placeholder-text: "postgres"; + activate => $on_entry_activated(); + changed => $on_text_changed(); + + layout { + row: 2; + column: 3; + column-span: 7; + } + } + + PasswordEntry password_entry { + placeholder-text: ""; + show-peek-icon: true; + activate => $on_entry_activated(); + changed => $on_text_changed(); + + layout { + row: 3; + column: 3; + column-span: 7; + } + } + + Entry database_entry { + placeholder-text: "postgres"; + activate => $on_entry_activated(); + changed => $on_text_changed(); + + layout { + row: 4; + column: 3; + column-span: 7; + } + } + + Label { + label: "SSL:"; + halign: end; + + layout { + row: 5; + column: 0; + column-span: 3; + } + } + + Box { + orientation: horizontal; + + Box { + valign: center; + orientation: vertical; + + Switch ssl_switch { + activate => $on_switch_changed(); + } + } + layout { + row: 5; + column: 3; + } + } + + Box { + + visible: bind ssl_switch.active; + orientation: vertical; + Entry cert_path { + editable: false; + can-focus: true; + placeholder-text: "SSL certificate-path"; + primary-icon-name: "application-certificate-symbolic"; + secondary-icon-name: "filemanager-app-symbolic"; + + icon-release => $on_cert_file_chooser(); + secondary-icon-activatable: true; + changed => $on_text_changed(); + activate => $on_cert_entry_activate(); + } + + + layout { + row: 5; + column: 4; + column-span: 6; + } + } + } + } + + Box { + margin-top: 20; + margin-bottom: 10; + + Label status_label { + label: ""; + halign: start; + hexpand: true; + } + + Box { + spacing: 8; + + Spinner spinner { + spinning: bind template.viewmodel as <$PsequelConnectionViewModel>.is-connectting; + } + + Button connect_btn { + styles [ + "suggested-action" + ] + + label: "Connect"; + clicked => $on_connect_clicked(); + } + } + } + }; } - } - - PasswordEntry password_entry { - placeholder-text: ""; - show-peek-icon: true; - activate => $on_entry_activated(); - changed => $on_text_changed(); - - layout { - row: 3; - column: 3; - column-span: 7; - } - } - - Entry database_entry { - placeholder-text: "postgres"; - activate => $on_entry_activated(); - changed => $on_text_changed(); - - layout { - row: 4; - column: 3; - column-span: 7; - } - } - - Label { - label: "SSL:"; - halign: end; - - layout { - row: 5; - column: 0; - column-span: 3; - } - } - - Box { - orientation: horizontal; - - Box { - valign: center; - orientation: vertical; - - Switch ssl_switch { - activate => $on_switch_changed(); - } - } - - layout { - row: 5; - column: 3; - } - } - } - } - - Box { - margin-top: 20; - margin-bottom: 10; - - Label status_label { - label: ""; - halign: start; - hexpand: true; } - - Box { - spacing: 8; - - Spinner spinner { - spinning: bind template.viewmodel as <$PsequelConnectionViewModel>.is-connectting; - } - - Button connect_btn { - styles [ - "suggested-action" - ] - - label: "Connect"; - clicked => $on_connect_clicked(); - } - } - } }; - } } - }; - } - // $PsequelConnectionForm form { - // width-request: 800; - // selected-connection: bind sidebar.selected-connection; - // is-connectting: bind template.viewmodel as <$PsequelConnectionViewModel>.is-connectting; - // current-state: bind template.viewmodel as <$PsequelConnectionViewModel>.current-state; - // menu-model: primary_menu; - // request-database => $active_connection (); - // connections-changed => $save_connections (); - // } - } + // $PsequelConnectionForm form { + // width-request: 800; + // selected-connection: bind sidebar.selected-connection; + // is-connectting: bind template.viewmodel as <$PsequelConnectionViewModel>.is-connectting; + // current-state: bind template.viewmodel as <$PsequelConnectionViewModel>.current-state; + // menu-model: primary_menu; + // request-database => $active_connection (); + // connections-changed => $save_connections (); + // } + } } diff --git a/res/gtk/icons/application-certificate-symbolic.svg b/res/gtk/icons/application-certificate-symbolic.svg new file mode 100644 index 0000000..4add9eb --- /dev/null +++ b/res/gtk/icons/application-certificate-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/gtk/icons/filemanager-app-symbolic.svg b/res/gtk/icons/filemanager-app-symbolic.svg new file mode 100644 index 0000000..ed91279 --- /dev/null +++ b/res/gtk/icons/filemanager-app-symbolic.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/res/psequel.gresource.xml b/res/psequel.gresource.xml index 5859360..fa174ac 100644 --- a/res/psequel.gresource.xml +++ b/res/psequel.gresource.xml @@ -52,5 +52,7 @@ gtk/icons/history-undo-symbolic.svg gtk/icons/check-plain-symbolic.svg gtk/icons/export-symbolic.svg + gtk/icons/filemanager-app-symbolic.svg + gtk/icons/application-certificate-symbolic.svg diff --git a/src/models/Connection.vala b/src/models/Connection.vala index 656f332..9808c7a 100644 --- a/src/models/Connection.vala +++ b/src/models/Connection.vala @@ -37,6 +37,7 @@ public class Connection : Object, Json.Serializable { public string options { get; set; default = DEFAULT; } + public string cert_path {get; set; default = "";} public Connection(string name = "New Connection") { this._name = name; @@ -76,12 +77,18 @@ public class Connection : Object, Json.Serializable { } public string connection_string(int connection_timeout, int query_timeout) { - var ssl_mode = use_ssl ? "required" : "disable"; + var ssl_mode = use_ssl ? "verify-full" : "disable"; var options = @"\'-c statement_timeout=$(query_timeout * 1000)\'"; + var base_str = @"user=$user password=$password port=$port host=$host dbname=$database application_name=$(Config.APP_NAME) sslmode=$ssl_mode connect_timeout=$connection_timeout options=$options"; + var builder = new StringBuilder(base_str); + + if (use_ssl) { + builder.append(@" sslrootcert=$(cert_path) "); + } - return(base_str); + return(builder.free_and_steal()); } /** diff --git a/src/ui/views/ConnectionView.vala b/src/ui/views/ConnectionView.vala index e50a3bd..5f745bf 100644 --- a/src/ui/views/ConnectionView.vala +++ b/src/ui/views/ConnectionView.vala @@ -59,6 +59,21 @@ public class ConnectionView : Adw.Bin { viewmodel.save_connections(); } + [GtkCallback] + private void on_cert_file_chooser(Gtk.EntryIconPosition pos) { + if (pos != Gtk.EntryIconPosition.SECONDARY) + { + return; + } + + open_file_dialog.begin(); + } + + [GtkCallback] + private void on_cert_entry_activate(Gtk.Entry entry) { + open_file_dialog.begin(); + } + [GtkCallback] private void on_switch_changed() { viewmodel.save_connections(); @@ -106,6 +121,39 @@ public class ConnectionView : Adw.Bin { return(true); } + private async void open_file_dialog(string title = "Choose certificate file") { + var filter = new Gtk.FileFilter(); + filter.add_mime_type("application/x-x509-ca-cert"); + + var filters = new ListStore(typeof(Gtk.FileFilter)); + filters.append(filter); + + var window = (Window)get_parrent_window(this); + + var file_dialog = new Gtk.FileDialog() { + modal = true, + initial_folder = File.new_for_path(Environment.get_home_dir()), + title = title, + initial_name = "server.cert", + default_filter = filter, + filters = filters + }; + + try { + var file = yield file_dialog.open(window, null); + var path = file.get_path(); + + this.viewmodel.selected_connection.cert_path = path; + } catch (Error err) { + debug(err.message); + + var toast = new Adw.Toast(err.message) { + timeout = 3, + }; + window.add_toast(toast); + } + } + private void set_up_bindings() { // Save ref so it does not be cleaned this.bindings = create_form_bind_group(); @@ -144,6 +192,7 @@ public class ConnectionView : Adw.Bin { binddings.bind("password", password_entry, "text", SYNC_CREATE | BIDIRECTIONAL); binddings.bind("database", database_entry, "text", SYNC_CREATE | BIDIRECTIONAL); binddings.bind("use_ssl", ssl_switch, "active", SYNC_CREATE | BIDIRECTIONAL); + binddings.bind("cert_path", cert_path, "text", SYNC_CREATE | BIDIRECTIONAL); // debug ("set_up binddings done"); @@ -173,6 +222,9 @@ public class ConnectionView : Adw.Bin { [GtkChild] private unowned Gtk.Entry database_entry; + [GtkChild] + private unowned Gtk.Entry cert_path; + [GtkChild] private unowned Gtk.Switch ssl_switch; } From 436b096bfa267439ec6f2bd267ee6fa7e1ccdb9c Mon Sep 17 00:00:00 2001 From: ppvan Date: Tue, 2 Apr 2024 22:48:16 +0700 Subject: [PATCH 04/10] feat: add database migrations and connection.cert_path --- res/migrations/version-0.sql | 20 ++++++ res/migrations/version-1.sql | 8 +++ res/psequel.gresource.xml | 5 ++ src/application.vala | 8 ++- src/meson.build | 1 + src/repositories/ConnectionRepository.vala | 45 +++--------- src/repositories/QueryRepository.vala | 23 ------ src/services/MigrationService.vala | 83 ++++++++++++++++++++++ 8 files changed, 135 insertions(+), 58 deletions(-) create mode 100644 res/migrations/version-0.sql create mode 100644 res/migrations/version-1.sql create mode 100644 src/services/MigrationService.vala diff --git a/res/migrations/version-0.sql b/res/migrations/version-0.sql new file mode 100644 index 0000000..2e20a7b --- /dev/null +++ b/res/migrations/version-0.sql @@ -0,0 +1,20 @@ +CREATE TABLE IF NOT EXISTS "connections" ( + "id" INTEGER, + "name" TEXT NOT NULL, + "host" TEXT NOT NULL, + "port" TEXT NOT NULL, + "user" TEXT NOT NULL, + "password" TEXT NOT NULL, + "database" TEXT NOT NULL, + "use_ssl" INT NOT NULL, + "options" TEXT NOT NULL, + "create_at" DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY("id" AUTOINCREMENT) +); + +CREATE TABLE IF NOT EXISTS "queries" ( + "id" INTEGER, + "sql" TEXT NOT NULL, + "create_at" DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY("id" AUTOINCREMENT) +); diff --git a/res/migrations/version-1.sql b/res/migrations/version-1.sql new file mode 100644 index 0000000..89f52d8 --- /dev/null +++ b/res/migrations/version-1.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS "migrations" ( + "id" INTEGER, + "version_num" INTEGER, + PRIMARY KEY("id" AUTOINCREMENT) +); +INSERT INTO "migrations" (version_num) VALUES (1); + +ALTER TABLE "connections" ADD COLUMN "cert_path" TEXT NOT NULL DEFAULT ""; \ No newline at end of file diff --git a/res/psequel.gresource.xml b/res/psequel.gresource.xml index fa174ac..aa798d9 100644 --- a/res/psequel.gresource.xml +++ b/res/psequel.gresource.xml @@ -2,6 +2,11 @@ gtk/style.css + + migrations/version-0.sql + migrations/version-1.sql + + gtk/style-switcher.ui gtk/connection-view.ui gtk/connection-row.ui diff --git a/src/application.vala b/src/application.vala index 1c493e1..ef1f7d7 100644 --- a/src/application.vala +++ b/src/application.vala @@ -38,6 +38,7 @@ public class Application : Adw.Application { public const int MAX_COLUMNS = 128; public const int PRE_ALLOCATED_CELL = 1024; public const int BATCH_SIZE = 16; + public const int MIGRATION_VERSION = 1; public static List tasks; public static bool is_running = false; @@ -110,7 +111,7 @@ public class Application : Adw.Application { container.register(this); Application.tasks = new List (); - this.is_running = true; + Application.is_running = true; debug("Begin to load resources"); try { @@ -255,6 +256,11 @@ public class Application : Adw.Application { var storage_service = new StorageService(db_file.get_path()); container.register(storage_service); + var migration_service = new MigrationService(); + migration_service.set_up_baseline(); + migration_service.apply_migrations(Application.MIGRATION_VERSION); + container.register(migration_service); + var sql_service = new SQLService(Application.background); var schema_service = new SchemaService(sql_service); diff --git a/src/meson.build b/src/meson.build index 2658972..0fcdf1b 100644 --- a/src/meson.build +++ b/src/meson.build @@ -15,6 +15,7 @@ psequel_sources = files([ 'services/Container.vala', 'services/ResourceManager.vala', 'services/SchemaService.vala', + 'services/MigrationService.vala', 'repositories/ConnectionRepository.vala', 'repositories/QueryRepository.vala', diff --git a/src/repositories/ConnectionRepository.vala b/src/repositories/ConnectionRepository.vala index ac67b33..1efc3a6 100644 --- a/src/repositories/ConnectionRepository.vala +++ b/src/repositories/ConnectionRepository.vala @@ -1,33 +1,16 @@ namespace Psequel { public class ConnectionRepository : Object { - const string KEY = "connections"; const string table_name = "connections"; - const string DDL = """ - CREATE TABLE IF NOT EXISTS "connections" ( - "id" INTEGER, - "name" TEXT NOT NULL, - "host" TEXT NOT NULL, - "port" TEXT NOT NULL, - "user" TEXT NOT NULL, - "password" TEXT NOT NULL, - "database" TEXT NOT NULL, - "use_ssl" INT NOT NULL, - "options" TEXT NOT NULL, - "create_at" DATETIME DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY("id" AUTOINCREMENT) - ) - """; - const string insert_sql = """ - INSERT INTO connections(name, host, port, user, password, database, use_ssl, options) VALUES (?, ?, ?, ?, ?, ?, ?, ?); + INSERT INTO connections(name, host, port, user, password, database, use_ssl, options, cert_path) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?); """; const string select_sql = """ - SELECT id, name, host, port, user, password, database, use_ssl, options FROM connections; + SELECT id, name, host, port, user, password, database, use_ssl, options, cert_path FROM connections; """; const string update_sql = """ UPDATE connections - SET name = ?, host = ?, port = ?, user = ?, password = ?, database = ?, use_ssl = ?, options = ? + SET name = ?, host = ?, port = ?, user = ?, password = ?, database = ?, use_ssl = ?, options = ?, cert_path = ? WHERE id = ?; """; const string delete_sql = """ @@ -46,8 +29,7 @@ public class ConnectionRepository : Object { public ConnectionRepository() { base(); - this.db = autowire (); - create_table(); + this.db = autowire (); this.insert_stmt = this.db.prepare(insert_sql); this.select_stmt = this.db.prepare(select_sql); this.update_stmt = this.db.prepare(update_sql); @@ -65,6 +47,7 @@ public class ConnectionRepository : Object { insert_stmt.bind_text(6, connection.database); insert_stmt.bind_int(7, connection.use_ssl ? 1 : 0); insert_stmt.bind_text(8, connection.options); + insert_stmt.bind_text(9, connection.cert_path); if (insert_stmt.step() != Sqlite.DONE) { @@ -94,7 +77,8 @@ public class ConnectionRepository : Object { update_stmt.bind_text(6, connection.database); update_stmt.bind_int(7, connection.use_ssl ? 1 : 0); update_stmt.bind_text(8, connection.options); - update_stmt.bind_int64(9, connection.id); + update_stmt.bind_text(9, connection.cert_path); + update_stmt.bind_int64(10, connection.id); int code = update_stmt.step(); if (code != Sqlite.DONE) @@ -199,6 +183,10 @@ public class ConnectionRepository : Object { conn.options = select_stmt.column_text(i); break; + case "cert_path": + conn.cert_path = select_stmt.column_text(i); + break; + default: debug("Unexpect column: %s\n", col_name); break; @@ -210,16 +198,5 @@ public class ConnectionRepository : Object { return(list); } - - private void create_table() { - string errmsg = null; - db.exec(DDL, out errmsg); - - if (errmsg != null) - { - debug("Error: %s\n", errmsg); - Process.exit(1); - } - } } } diff --git a/src/repositories/QueryRepository.vala b/src/repositories/QueryRepository.vala index b277a2b..ccf59a4 100644 --- a/src/repositories/QueryRepository.vala +++ b/src/repositories/QueryRepository.vala @@ -2,16 +2,6 @@ namespace Psequel { public class QueryRepository : Object { const string KEY = "queries"; - - const string DDL = """ - CREATE TABLE IF NOT EXISTS "queries" ( - "id" INTEGER, - "sql" TEXT NOT NULL, - "create_at" DATETIME DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY("id" AUTOINCREMENT) - ); - """; - const string select_sql = """ SELECT id, sql FROM "queries" ORDER BY create_at DESC; @@ -49,8 +39,6 @@ public class QueryRepository : Object { base(); this.db = autowire (); - create_table(); - select_stmt = db.prepare(select_sql); insert_stmt = db.prepare(insert_sql); update_stmt = db.prepare(update_sql); @@ -140,16 +128,5 @@ public class QueryRepository : Object { debug("Error: %s", db.err_message()); } } - - private void create_table() { - string errmsg = null; - db.exec(DDL, out errmsg); - - if (errmsg != null) - { - debug("Error: %s\n", errmsg); - Process.exit(1); - } - } } } diff --git a/src/services/MigrationService.vala b/src/services/MigrationService.vala new file mode 100644 index 0000000..beaf49d --- /dev/null +++ b/src/services/MigrationService.vala @@ -0,0 +1,83 @@ +namespace Psequel { +public class MigrationService : Object { + private StorageService storage; + + public MigrationService() { + this.storage = autowire (); + } + + public void set_up_baseline() { + try { + uint8[] file_content; + string ?err_msg = null; + + debug("Setup database baseline"); + var file = File.new_for_uri("resource:///me/ppvan/psequel/migrations/version-0.sql"); + + file.load_contents(null, out file_content, null); + storage.exec((string)file_content, out err_msg); + + if (err_msg != null) + { + debug("Sqlite Error: %s", err_msg); + } + } catch (GLib.Error err) { + debug("Error: %s", err.message); + } + } + + public void apply_migrations(int latest_version) { + int current_version = fetch_version_num(); + for (int i = current_version + 1; i <= latest_version; i++) + { + apply_migration(i); + } + } + + private void apply_migration(int version) { + try { + uint8[] file_content; + string ?err_msg = null; + + debug("Apply migrations version: %d", version); + var file = File.new_for_uri("resource:///me/ppvan/psequel/migrations/version-%d.sql".printf(version)); + + file.load_contents(null, out file_content, null); + storage.exec((string)file_content, out err_msg); + + if (err_msg != null) + { + debug("Sqlite Error: %s", err_msg); + } + } catch (GLib.Error err) { + debug("Error: %s", err.message); + } + } + + private int fetch_version_num() { + string ?err_msg; + + var result = storage.exec("SELECT version_num FROM migrations LIMIT 1;", out err_msg); + + if (err_msg != null) + { + if ("no such table: migrations" in err_msg) + { + return(0); + } + debug("SqliteError: %s", err_msg); + } + + if (result.rows <= 0) + { + debug("Empty migrations table"); + + return(0); + } + + var version_str = result[0][0]; + + return(int.parse(version_str)); + } +} +} From 208b92284c8fd31b427ba6da2c12cf016c01c8bc Mon Sep 17 00:00:00 2001 From: ppvan Date: Tue, 2 Apr 2024 23:01:05 +0700 Subject: [PATCH 05/10] refactor: use mimetype instead of file pattern --- src/ui/Window.vala | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/ui/Window.vala b/src/ui/Window.vala index 1d72474..c07c7fa 100644 --- a/src/ui/Window.vala +++ b/src/ui/Window.vala @@ -77,7 +77,7 @@ public class Window : Adw.ApplicationWindow { private async void open_file_dialog(string title = "Open File") { var filter = new Gtk.FileFilter(); - filter.add_pattern("*.json"); + filter.add_mime_type("application/json"); var filters = new ListStore(typeof(Gtk.FileFilter)); filters.append(filter); @@ -88,7 +88,7 @@ public class Window : Adw.ApplicationWindow { modal = true, initial_folder = File.new_for_path(Environment.get_home_dir()), title = title, - initial_name = "connections", + initial_name = "connections.json", default_filter = filter, filters = filters }; @@ -120,7 +120,8 @@ public class Window : Adw.ApplicationWindow { private async void save_file_dialog(string title = "Save to file") { var filter = new Gtk.FileFilter(); - filter.add_suffix("json"); + + filter.add_mime_type("application/json"); var filters = new ListStore(typeof(Gtk.FileFilter)); filters.append(filter); @@ -129,7 +130,7 @@ public class Window : Adw.ApplicationWindow { modal = true, initial_folder = File.new_for_path(Environment.get_home_dir()), title = title, - initial_name = "connections", + initial_name = "connections.json", default_filter = filter, filters = filters, }; From d1efdb1269917f595dcbf1a169427add980e3f23 Mon Sep 17 00:00:00 2001 From: ppvan Date: Sat, 13 Apr 2024 15:07:45 +0700 Subject: [PATCH 06/10] refactor: stop rely on regex to parse infomation, using postgres_catalog namespace instead --- .gitignore | 4 +- src/application.vala | 33 ++- src/meson.build | 5 - src/models/Schema.vala | 265 +++++--------------- src/models/Table.vala | 7 +- src/ui/schema/TableColumnInfo.vala | 102 -------- src/ui/schema/TableForeignKeyInfo.vala | 138 ---------- src/ui/schema/TableIndexInfo.vala | 124 --------- src/ui/widgets/TableGraph.vala | 221 ++++++++-------- src/utils/types.vala | 61 +++-- src/viewmodels/TableStructureViewModel.vala | 214 ---------------- src/viewmodels/TableViewModel.vala | 183 +++++++++++++- 12 files changed, 428 insertions(+), 929 deletions(-) delete mode 100644 src/ui/schema/TableColumnInfo.vala delete mode 100644 src/ui/schema/TableForeignKeyInfo.vala delete mode 100644 src/ui/schema/TableIndexInfo.vala delete mode 100644 src/viewmodels/TableStructureViewModel.vala diff --git a/.gitignore b/.gitignore index 8330a13..7a48184 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,6 @@ export /.cache -/build-aux \ No newline at end of file +/build-aux + +*.dot \ No newline at end of file diff --git a/src/application.vala b/src/application.vala index ef1f7d7..fc1d18f 100644 --- a/src/application.vala +++ b/src/application.vala @@ -38,7 +38,7 @@ public class Application : Adw.Application { public const int MAX_COLUMNS = 128; public const int PRE_ALLOCATED_CELL = 1024; public const int BATCH_SIZE = 16; - public const int MIGRATION_VERSION = 1; + public const int MIGRATION_VERSION = 1; public static List tasks; public static bool is_running = false; @@ -110,8 +110,8 @@ public class Application : Adw.Application { container.register(settings); container.register(this); - Application.tasks = new List (); - Application.is_running = true; + Application.tasks = new List (); + Application.is_running = true; debug("Begin to load resources"); try { @@ -185,13 +185,10 @@ public class Application : Adw.Application { typeof(Psequel.ConnectionView).ensure(); typeof(Psequel.QueryResults).ensure(); typeof(Psequel.QueryEditor).ensure(); - typeof(Psequel.TableStructureView).ensure(); + // typeof(Psequel.TableStructureView).ensure(); typeof(Psequel.ViewStructureView).ensure(); typeof(Psequel.TableDataView).ensure(); typeof(Psequel.ViewDataView).ensure(); - typeof(Psequel.TableColInfo).ensure(); - typeof(Psequel.TableIndexInfo).ensure(); - typeof(Psequel.TableFKInfo).ensure(); } private void on_about_action() { @@ -271,16 +268,16 @@ public class Application : Adw.Application { var completer = new CompleterService(sql_service); // viewmodels - var conn_vm = new ConnectionViewModel(connection_repo, sql_service, navigation); - var sche_vm = new SchemaViewModel(schema_service); - var table_vm = new TableViewModel(sql_service); - var view_vm = new ViewViewModel(sql_service); - var table_structure_vm = new TableStructureViewModel(sql_service); - var view_structure_vm = new ViewStructureViewModel(sql_service); - var table_data_vm = new TableDataViewModel(sql_service); - var view_data_vm = new ViewDataViewModel(sql_service); - var query_history_vm = new QueryHistoryViewModel(sql_service, query_repo); - var query_vm = new QueryViewModel(query_history_vm); + var conn_vm = new ConnectionViewModel(connection_repo, sql_service, navigation); + var sche_vm = new SchemaViewModel(schema_service); + var table_vm = new TableViewModel(sql_service); + var view_vm = new ViewViewModel(sql_service); + // var table_structure_vm = new TableStructureViewModel(sql_service); + var view_structure_vm = new ViewStructureViewModel(sql_service); + var table_data_vm = new TableDataViewModel(sql_service); + var view_data_vm = new ViewDataViewModel(sql_service); + var query_history_vm = new QueryHistoryViewModel(sql_service, query_repo); + var query_vm = new QueryViewModel(query_history_vm); container.register(sql_service); container.register(completer); @@ -292,7 +289,7 @@ public class Application : Adw.Application { container.register(sche_vm); container.register(table_vm); container.register(view_vm); - container.register(table_structure_vm); + // container.register(table_structure_vm); container.register(view_structure_vm); container.register(table_data_vm); container.register(view_data_vm); diff --git a/src/meson.build b/src/meson.build index 0fcdf1b..b594e16 100644 --- a/src/meson.build +++ b/src/meson.build @@ -27,7 +27,6 @@ psequel_sources = files([ 'viewmodels/BaseViewModel.vala', 'viewmodels/SchemaViewModel.vala', 'viewmodels/TableViewModel.vala', - 'viewmodels/TableStructureViewModel.vala', 'viewmodels/ViewStructureViewModel.vala', 'viewmodels/TableDataViewModel.vala', 'viewmodels/ViewViewModel.vala', @@ -45,11 +44,7 @@ ui_sources = files([ 'ui/views/ConnectionView.vala', 'ui/views/SchemaView.vala', 'ui/schema/QueryResult.vala', - 'ui/schema/TableStructureView.vala', 'ui/schema/ViewStructureView.vala', - 'ui/schema/TableColumnInfo.vala', - 'ui/schema/TableForeignKeyInfo.vala', - 'ui/schema/TableIndexInfo.vala', 'ui/schema/TableDataView.vala', 'ui/schema/ViewDataView.vala', diff --git a/src/models/Schema.vala b/src/models/Schema.vala index fef4293..4a1b51b 100644 --- a/src/models/Schema.vala +++ b/src/models/Schema.vala @@ -1,213 +1,86 @@ namespace Psequel { - - /** - * Carying schema info like tables and views. - */ - public class Schema : Object { - public string name { get; private set; } - - public Schema (string name) { - Object (); - this.name = name; - } +/** + * Carying schema info like tables and views. + */ +public class Schema : Object { + public string name { get; private set; } + + public Schema(string name) { + Object(); + this.name = name; } +} - /** Base type for hold info about Table */ - public abstract class BaseType : Object { - public string name { get; set; default = ""; } - public string schemaname { get; set; default = ""; } - public string table {get; set; default = "";} +/** Base type for hold info about Table */ +public abstract class BaseType : Object { + public string name { get; set; default = ""; } + public string schemaname { get; set; default = ""; } + public string table { get; set; default = ""; } - public string to_string () { - return @"$schemaname.$name"; - } + public string to_string() { + return(@"$schemaname.$name"); } - - /** Table Index info */ - public class Index : BaseType { - public bool unique { get; set; default = false; } - public IndexType index_type { get; set; default = BTREE; } - public string columns { get; set; default = ""; } - public string size {get; set; default = "0 kB";} - - private string _indexdef; - public string indexdef { - get { - return _indexdef; - } - set { - this._indexdef = value ?? ""; - this.extract_info (); - } +} + +/** Table Index info */ +public class Index : BaseType { + public bool unique { get; set; default = false; } + public string index_type { get; set; default = "BTREE"; } + public string columns { get; set; default = ""; } + public string size { get; set; default = "0 kB"; } + + private string _indexdef; + public string indexdef { + get { + return(_indexdef); } - - private void extract_info () { - unique = indexdef.contains ("UNIQUE"); - - // Match the index type and column from indexdef, group 1 is type, group 2 is the column list. - var regex = /USING (btree|hash|gist|spgist|gin|brin|[\w]+) \(([a-zA-Z1-9+\-*\/_, ()]+)\)/; - MatchInfo match_info; - if (regex.match (indexdef, 0, out match_info)) { - index_type = IndexType.from_string (match_info.fetch (1)); - columns = match_info.fetch (2); - } else { - warning ("Regex not match: %s", indexdef); - assert_not_reached (); - } - } - - // (btree|hash|gist|spgist|gin|brin|[\w]+ - public enum IndexType { - BTREE, - HASH, - GIST, - SPGIST, - GIN, - BRIN, - USER_DEFINED; - - public string to_string () { - switch (this) { - case Psequel.Index.IndexType.BTREE: - return "BTREE"; - case Psequel.Index.IndexType.HASH: - return "HASH"; - case Psequel.Index.IndexType.GIST: - return "GIST"; - case Psequel.Index.IndexType.SPGIST: - return "SPGIST"; - case Psequel.Index.IndexType.GIN: - return "GIN"; - case Psequel.Index.IndexType.BRIN: - return "BRIN"; - case Psequel.Index.IndexType.USER_DEFINED: - return "USER_DEFINED"; - } - - return ""; - } - - public static IndexType[] all () { - return { - BTREE, - HASH, - GIST, - SPGIST, - GIN, - BRIN, - USER_DEFINED - }; - } - - public static IndexType from_string (string str) { - var vals = IndexType.all (); - for (int i = 0; i < vals.length; i++) { - if (str.ascii_up () == vals[i].to_string ()) { - return vals[i]; - } - } - - return USER_DEFINED; - } + set { + this._indexdef = value ?? ""; + this.extract_info(); } } - /** Table Column info */ - public class Column : BaseType { - - public string column_type { get; set; default = ""; } - public bool nullable { get; set; default = false; } - public string default_val { get; set; default = ""; } - - public Column () { - - } + private void extract_info() { + // unique = indexdef.contains ("UNIQUE"); + + // // Match the index type and column from indexdef, group 1 is type, group 2 is the column list. + // var regex = /USING (btree|hash|gist|spgist|gin|brin|[\w]+) \(([a-zA-Z1-9+\-*\/_, ()]+)\)/; + // MatchInfo match_info; + // if (regex.match (indexdef, 0, out match_info)) { + // index_type = IndexType.from_string (match_info.fetch (1)); + // columns = match_info.fetch (2); + // } else { + // warning ("Regex not match: %s", indexdef); + // assert_not_reached (); + // } } +} +/** Table Column info */ +public class Column : BaseType { + public string column_type { get; set; default = ""; } + public bool nullable { get; set; default = false; } + public string default_val { get; set; default = ""; } - public class PrimaryKey : BaseType { - public string[] columns { get; set; } - - public PrimaryKey () { - } + public Column() { } - /** Table foreign key info */ - public class ForeignKey : BaseType { - public string columns { get; set; default = ""; } - public string[] columns_v2 { get; set; } - public string fk_table { get; set; default = ""; } - public string fk_columns { get; set; default = ""; } - public string[] fk_columns_v2 { get; set; } - public FKType on_update { get; set; default = NO_ACTION; } - public FKType on_delete { get; set; default = NO_ACTION; } - - private string _fk_def; - public string fk_def { - get { - return _fk_def; - } - set { - _fk_def = value; - extract_info (); - } - } - - private void extract_info () { - // Match the index type and column from fk_def - var regex = /FOREIGN KEY \(([$a-zA-Z_, ]+)\) REFERENCES ([a-zA-Z_,"]+)\(([a-zA-Z_, ]+)\)( ON UPDATE (CASCADE))?( ON DELETE (RESTRICT))?/; - MatchInfo match_info; - if (regex.match (fk_def, 0, out match_info)) { - - columns = match_info.fetch (1); - fk_table = match_info.fetch (2); - fk_columns = match_info.fetch (3); - - on_update = FKType.from_string (match_info.fetch (5)); - on_delete = FKType.from_string (match_info.fetch (7)); +} - } else { - warning ("Regex not match: %s", fk_def); - } - } - - public enum FKType { - NO_ACTION, - RESTRICT, - CASCADE; - - public string to_string () { - switch (this) { - case Psequel.ForeignKey.FKType.NO_ACTION: - return "NO_ACTION"; - case Psequel.ForeignKey.FKType.RESTRICT: - return "RESTRICT"; - case Psequel.ForeignKey.FKType.CASCADE: - return "CASCADE"; - - } - return ""; - } +public class PrimaryKey : Object { + public string table { get; set; default = ""; } + public string name { get; set; default = ""; } + public string[] columns { get; set; } - public static ForeignKey.FKType[] all () { - return { - NO_ACTION, - RESTRICT, - CASCADE - }; - } - - public static ForeignKey.FKType from_string (string str) { - var vals = ForeignKey.FKType.all (); - - for (int i = 0; i < vals.length; i++) { - if (vals[i].to_string () == str.ascii_up ()) { - return vals[i]; - } - } - - return NO_ACTION; - } - } + public PrimaryKey() { } -} \ No newline at end of file +} +/** Table foreign key info */ +public class ForeignKey : Object { + public string name { get; set; default = ""; } + public string table { get; set; default = ""; } + public string[] columns { get; set; } + public string fk_table { get; set; default = ""; } + public string[] fk_columns { get; set; } +} +} diff --git a/src/models/Table.vala b/src/models/Table.vala index 9ca1d66..a309a90 100644 --- a/src/models/Table.vala +++ b/src/models/Table.vala @@ -7,9 +7,10 @@ public abstract class BaseTable : Object { /** Table object in database, hold meta-data about the table */ public sealed class Table : BaseTable { - public List columns { get; owned set; default = new List (); } - public List indexes { get; owned set; default = new List (); } - public List foreign_keys { get; owned set; default = new List (); } + public Vec columns { get; owned set; default = new Vec (); } + public Vec indexes { get; owned set; default = new Vec (); } + public Vec foreign_keys { get; owned set; default = new Vec (); } + public Vec primaty_keys { get; owned set; default = new Vec (); } public Table(Schema schema) { Object(schema: schema); diff --git a/src/ui/schema/TableColumnInfo.vala b/src/ui/schema/TableColumnInfo.vala deleted file mode 100644 index f3cb04f..0000000 --- a/src/ui/schema/TableColumnInfo.vala +++ /dev/null @@ -1,102 +0,0 @@ -namespace Psequel { -[GtkTemplate(ui = "/me/ppvan/psequel/gtk/table-cols.ui")] -public class TableColInfo : Adw.Bin { - // public ObservableList columns {get; set;} - public GLib.ListModel columns { get; set; } - - public TableColInfo() { - Object(); - } - - construct { - setup_name_col(); - setup_datatype_col(); - setup_nullable_col(); - setup_default_col(); - } - - - private void setup_name_col() { - var factory = new Gtk.SignalListItemFactory(); - factory.setup.connect((obj) => { - var listitem = obj as Gtk.ListItem; - - var label = new Gtk.Label(null); - label.halign = Gtk.Align.START; - listitem.child = label; - }); - factory.bind.connect((obj) => { - var listitem = obj as Gtk.ListItem; - var item = listitem.item as Column; - var label = listitem.child as Gtk.Label; - label.label = item.name; - }); - var col = new Gtk.ColumnViewColumn("Column Name", factory); - col.fixed_width = 250; - view.append_column(col); - } - - private void setup_datatype_col() { - var factory = new Gtk.SignalListItemFactory(); - factory.setup.connect((obj) => { - var listitem = obj as Gtk.ListItem; - var label = new Gtk.Label(null); - label.halign = Gtk.Align.START; - - listitem.child = label; - }); - factory.bind.connect((obj) => { - var listitem = obj as Gtk.ListItem; - var item = listitem.item as Column; - var label = listitem.child as Gtk.Label; - label.label = item.column_type; - }); - var col = new Gtk.ColumnViewColumn("Data Type", factory); - col.fixed_width = 300; - view.append_column(col); - } - - private void setup_nullable_col() { - var factory = new Gtk.SignalListItemFactory(); - factory.setup.connect((obj) => { - var listitem = obj as Gtk.ListItem; - var label = new Gtk.Label(null); - listitem.child = label; - }); - factory.bind.connect((obj) => { - var listitem = obj as Gtk.ListItem; - var item = listitem.item as Column; - var label = listitem.child as Gtk.Label; - label.label = item.nullable ? "YES" : "NO"; - }); - var col = new Gtk.ColumnViewColumn("Nullable", factory); - col.fixed_width = 70; - - view.append_column(col); - } - - private void setup_default_col() { - var factory = new Gtk.SignalListItemFactory(); - factory.setup.connect((obj) => { - var listitem = obj as Gtk.ListItem; - var label = new Gtk.Label(null); - label.halign = Gtk.Align.END; - label.margin_end = 4; - label.margin_start = 4; - listitem.child = label; - }); - factory.bind.connect((obj) => { - var listitem = obj as Gtk.ListItem; - var item = listitem.item as Column; - var label = listitem.child as Gtk.Label; - label.label = item.default_val; - }); - var col = new Gtk.ColumnViewColumn("Default Value", factory); - col.set_expand(true); - view.append_column(col); - } - - [GtkChild] - private unowned Gtk.ColumnView view; -} -} diff --git a/src/ui/schema/TableForeignKeyInfo.vala b/src/ui/schema/TableForeignKeyInfo.vala deleted file mode 100644 index 1d0bc34..0000000 --- a/src/ui/schema/TableForeignKeyInfo.vala +++ /dev/null @@ -1,138 +0,0 @@ -namespace Psequel { -[GtkTemplate(ui = "/me/ppvan/psequel/gtk/table-fk.ui")] -public class TableFKInfo : Adw.Bin { - public GLib.ListModel fks { get; set; } - - - public TableFKInfo() { - Object(); - } - - construct { - setup_name_col(); - setup_table_columns_col(); - setup_fk_tbname_col(); - setup_fk_table_columns_col(); - setup_on_update_col(); - setup_fk_on_delete_col(); - } - - - private void setup_name_col() { - var factory = new Gtk.SignalListItemFactory(); - factory.setup.connect((obj) => { - var listitem = obj as Gtk.ListItem; - var label = new Gtk.Label(null); - label.halign = Gtk.Align.START; - listitem.child = label; - }); - factory.bind.connect((obj) => { - var listitem = obj as Gtk.ListItem; - var item = listitem.item as ForeignKey; - var label = listitem.child as Gtk.Label; - label.label = item.name; - }); - var col = new Gtk.ColumnViewColumn("Foreign Key", factory); - col.fixed_width = 250; - view.append_column(col); - } - - private void setup_table_columns_col() { - var factory = new Gtk.SignalListItemFactory(); - factory.setup.connect((obj) => { - var listitem = obj as Gtk.ListItem; - var label = new Gtk.Label(null); - label.halign = Gtk.Align.START; - listitem.child = label; - }); - factory.bind.connect((obj) => { - var listitem = obj as Gtk.ListItem; - var item = listitem.item as ForeignKey; - var label = listitem.child as Gtk.Label; - label.label = item.columns; - }); - var col = new Gtk.ColumnViewColumn("Columns", factory); - col.fixed_width = 250; - view.append_column(col); - } - - private void setup_fk_tbname_col() { - var factory = new Gtk.SignalListItemFactory(); - factory.setup.connect((obj) => { - var listitem = obj as Gtk.ListItem; - var label = new Gtk.Label(null); - label.halign = Gtk.Align.START; - listitem.child = label; - }); - factory.bind.connect((obj) => { - var listitem = obj as Gtk.ListItem; - var item = listitem.item as ForeignKey; - var label = listitem.child as Gtk.Label; - label.label = item.fk_table; - }); - var col = new Gtk.ColumnViewColumn("Foreign Table", factory); - col.expand = true; - view.append_column(col); - } - - private void setup_fk_table_columns_col() { - var factory = new Gtk.SignalListItemFactory(); - factory.setup.connect((obj) => { - var listitem = obj as Gtk.ListItem; - var label = new Gtk.Label(null); - label.halign = Gtk.Align.START; - listitem.child = label; - }); - factory.bind.connect((obj) => { - var listitem = obj as Gtk.ListItem; - var item = listitem.item as ForeignKey; - var label = listitem.child as Gtk.Label; - label.label = item.fk_columns; - }); - var col = new Gtk.ColumnViewColumn("Reference Columns", factory); - col.expand = true; - view.append_column(col); - } - - private void setup_on_update_col() { - var factory = new Gtk.SignalListItemFactory(); - factory.setup.connect((obj) => { - var listitem = obj as Gtk.ListItem; - var label = new Gtk.Label(null); - label.halign = Gtk.Align.CENTER; - listitem.child = label; - }); - factory.bind.connect((obj) => { - var listitem = obj as Gtk.ListItem; - var item = listitem.item as ForeignKey; - var label = listitem.child as Gtk.Label; - label.label = item.on_update.to_string(); - }); - var col = new Gtk.ColumnViewColumn("On Update", factory); - col.fixed_width = 100; - view.append_column(col); - } - - private void setup_fk_on_delete_col() { - var factory = new Gtk.SignalListItemFactory(); - factory.setup.connect((obj) => { - var listitem = obj as Gtk.ListItem; - var label = new Gtk.Label(null); - label.halign = Gtk.Align.CENTER; - listitem.child = label; - }); - factory.bind.connect((obj) => { - var listitem = obj as Gtk.ListItem; - var item = listitem.item as ForeignKey; - var label = listitem.child as Gtk.Label; - label.label = item.on_delete.to_string(); - }); - var col = new Gtk.ColumnViewColumn("On Delete", factory); - col.fixed_width = 100; - view.append_column(col); - } - - [GtkChild] - private unowned Gtk.ColumnView view; -} -} diff --git a/src/ui/schema/TableIndexInfo.vala b/src/ui/schema/TableIndexInfo.vala deleted file mode 100644 index d342277..0000000 --- a/src/ui/schema/TableIndexInfo.vala +++ /dev/null @@ -1,124 +0,0 @@ -namespace Psequel { -[GtkTemplate(ui = "/me/ppvan/psequel/gtk/table-index.ui")] -public class TableIndexInfo : Adw.Bin { - public GLib.ListModel indexes { get; set; } - - - public TableIndexInfo() { - Object(); - } - - construct { - setup_name_col(); - setup_indexcolumns_col(); - setup_indextype_col(); - setup_unique_col(); - setup_indexsize_col(); - } - - - private void setup_name_col() { - var factory = new Gtk.SignalListItemFactory(); - factory.setup.connect((obj) => { - var listitem = obj as Gtk.ListItem; - var label = new Gtk.Label(null); - label.halign = Gtk.Align.START; - label.css_classes = { "table-cell" }; - listitem.child = label; - }); - factory.bind.connect((obj) => { - var listitem = obj as Gtk.ListItem; - var item = listitem.item as Index; - var label = listitem.child as Gtk.Label; - label.label = item.name; - }); - var col = new Gtk.ColumnViewColumn("Index Name", factory); - col.fixed_width = 250; - view.append_column(col); - } - - private void setup_indextype_col() { - var factory = new Gtk.SignalListItemFactory(); - factory.setup.connect((obj) => { - var listitem = obj as Gtk.ListItem; - var label = new Gtk.Label(null); - label.halign = Gtk.Align.CENTER; - - listitem.child = label; - }); - factory.bind.connect((obj) => { - var listitem = obj as Gtk.ListItem; - var item = listitem.item as Index; - var label = listitem.child as Gtk.Label; - label.label = item.index_type.to_string(); - }); - var col = new Gtk.ColumnViewColumn("Index Type", factory); - col.fixed_width = 80; - view.append_column(col); - } - - private void setup_unique_col() { - var factory = new Gtk.SignalListItemFactory(); - factory.setup.connect((obj) => { - var listitem = obj as Gtk.ListItem; - var label = new Gtk.Label(null); - listitem.child = label; - }); - factory.bind.connect((obj) => { - var listitem = obj as Gtk.ListItem; - var item = listitem.item as Index; - var label = listitem.child as Gtk.Label; - label.label = item.unique ? "YES" : "NO"; - }); - var col = new Gtk.ColumnViewColumn("Unique", factory); - col.fixed_width = 70; - - view.append_column(col); - } - - private void setup_indexcolumns_col() { - var factory = new Gtk.SignalListItemFactory(); - factory.setup.connect((obj) => { - var listitem = obj as Gtk.ListItem; - var label = new Gtk.Label(null); - label.halign = Gtk.Align.START; - label.margin_end = 4; - label.margin_start = 4; - listitem.child = label; - }); - factory.bind.connect((obj) => { - var listitem = obj as Gtk.ListItem; - var item = listitem.item as Index; - var label = listitem.child as Gtk.Label; - label.label = item.columns; - }); - var col = new Gtk.ColumnViewColumn("Index Columns", factory); - col.expand = true; - view.append_column(col); - } - - private void setup_indexsize_col() { - var factory = new Gtk.SignalListItemFactory(); - factory.setup.connect((obj) => { - var listitem = obj as Gtk.ListItem; - var label = new Gtk.Label(null); - label.halign = Gtk.Align.END; - label.margin_end = 4; - label.margin_start = 4; - listitem.child = label; - }); - factory.bind.connect((obj) => { - var listitem = obj as Gtk.ListItem; - var item = listitem.item as Index; - var label = listitem.child as Gtk.Label; - label.label = item.size; - }); - var col = new Gtk.ColumnViewColumn("Index Size", factory); - col.fixed_width = 70; - view.append_column(col); - } - - [GtkChild] - private unowned Gtk.ColumnView view; -} -} diff --git a/src/ui/widgets/TableGraph.vala b/src/ui/widgets/TableGraph.vala index f863ebf..e6c6151 100644 --- a/src/ui/widgets/TableGraph.vala +++ b/src/ui/widgets/TableGraph.vala @@ -5,135 +5,134 @@ namespace Psequel { public class TableGraph : Gtk.Box { private uint8[] buff; - private TableStructureViewModel viewmodel; - public TableGraph() { Object(); } construct { - this.viewmodel = autowire (); + // this.viewmodel = autowire (); - this.viewmodel.notify["selected-table"].connect(() => { - debug ("Test: %s", this.viewmodel.selected_table.name); - var table = this.viewmodel.selected_table; - this.render_graph.begin(table); - }); + // this.viewmodel.notify["selected-table"].connect(() => { + // debug("Test: %s", this.viewmodel.selected_table.name); + // var table = this.viewmodel.selected_table; + // this.render_graph.begin(table); + // }); } public async void render_graph(Table table) { - var fks = this.viewmodel.foreign_keys; - uint8[] buff = generate_graph(table, fks.to_list()); - var svgPaintable = new SvgPaintable(buff); + // var fks = this.viewmodel.foreign_keys; + // uint8[] buff = generate_graph(table, fks.to_list()); + // var svgPaintable = new SvgPaintable(buff); - pic.set_paintable(svgPaintable); - debug ("Test svgPaintable: %d %d %d", fks.size, this.viewmodel.columns.size, this.viewmodel.indexes.size); + // pic.set_paintable(svgPaintable); } public uint8[] generate_graph(Table table, List fks) { - var gvc = new Gvc.Context(); - var g = new Gvc.Graph("g", Gvc.Agdirected, 0); - g.safe_set("rankdir", "LR", ""); - g.safe_set("fontname", "Roboto", ""); - g.safe_set("bgcolor", "transparent", ""); - - foreach (var item in fks) - { - if (item.table != table.name && item.fk_table != table.name) - { - continue; - } - - var begin = g.create_node(item.table); - var end = g.create_node(item.fk_table); - - var begin_label = generate_table_details(g, item.table); - var end_label = generate_table_details(g, item.fk_table); - - begin.safe_set("fontname", "Roboto", ""); - begin.safe_set("shape", "plaintext", ""); - begin.safe_set("label", begin_label, ""); - begin.safe_set("fontcolor", "#D1CDC7", ""); - begin.safe_set("color", "#858786", ""); - - - end.safe_set("fontname", "Roboto", ""); - end.safe_set("shape", "plaintext", ""); - end.safe_set("label", end_label, ""); - end.safe_set("fontcolor", "#D1CDC7", ""); - end.safe_set("color", "#858786", ""); - var edge = g.create_edge(begin, end); - edge.safe_set("color", "#858786", ""); - edge.safe_set("tailport", item.fk_columns_v2[0], ""); - edge.safe_set("headport", item.columns_v2[0], ""); - } - gvc.layout(g, "dot"); - gvc.render_data(g, "svg", out this.buff); - gvc.free_layout(g); + // var gvc = new Gvc.Context(); + // var g = new Gvc.Graph("g", Gvc.Agdirected, 0); + // g.safe_set("rankdir", "LR", ""); + // g.safe_set("fontname", "Roboto", ""); + // g.safe_set("bgcolor", "transparent", ""); + + // foreach (var item in fks) + // { + // if (item.table != table.name && item.fk_table != table.name) + // { + // continue; + // } + + // var begin = g.create_node(item.table); + // var end = g.create_node(item.fk_table); + + // // var begin_label = generate_table_details(g, item.table); + // // var end_label = generate_table_details(g, item.fk_table); + + // begin.safe_set("fontname", "Roboto", ""); + // begin.safe_set("shape", "plaintext", ""); + // begin.safe_set("label", begin_label, ""); + // begin.safe_set("fontcolor", "#D1CDC7", ""); + // begin.safe_set("color", "#858786", ""); + + + // end.safe_set("fontname", "Roboto", ""); + // end.safe_set("shape", "plaintext", ""); + // end.safe_set("label", end_label, ""); + // end.safe_set("fontcolor", "#D1CDC7", ""); + // end.safe_set("color", "#858786", ""); + // var edge = g.create_edge(begin, end); + // edge.safe_set("color", "#858786", ""); + // edge.safe_set("tailport", item.fk_columns[0], ""); + // edge.safe_set("headport", item.columns[0], ""); + // } + // gvc.layout(g, "dot"); + // gvc.render_data(g, "svg", out this.buff); + // gvc.free_layout(g); return(this.buff); } - private Gvc.HtmlString generate_table_details(Gvc.Graph g, string table) { - var stringBuilder = new StringBuilder("""
"""); - stringBuilder.append(@""); - string[] current_pks = new string[0]; - string[] current_fks = new string[0]; - - foreach (var pk in this.viewmodel.primary_keys) - { - if (pk.table == table) - { - current_pks = pk.columns; - break; - } - } - - foreach (var fk in this.viewmodel.foreign_keys) - { - if (fk.table == table) - { - current_fks = fk.columns_v2; - break; - } - } - - debug (table); - - for (int i = 0; i < current_pks.length; i++) { - debug("PK: %s", current_pks[i]); - } - - for (int i = 0; i < current_fks.length; i++) { - debug("FK: %s", current_fks[i]); - } - - foreach (var col in this.viewmodel.columns) - { - if (col.table != table) - { - continue; - } - - if (col.name in current_fks) - { - stringBuilder.append_printf("""""", col.name, col.name, col.column_type); - } - else if (col.name in current_pks) - { - stringBuilder.append_printf("""""", col.name, col.name, col.column_type); - } - else - { - stringBuilder.append_printf("""""", col.name, col.name, col.column_type); - } - } - - stringBuilder.append("
$(table)
%s%s
%s%s
%s%s
"); - var markup = stringBuilder.free_and_steal(); - return(Gvc.HtmlString.make_html(g, markup)); - } + // private Gvc.HtmlString generate_table_details(Gvc.Graph g, string table) { + // var stringBuilder = new StringBuilder(""""""); + // stringBuilder.append(@""); + // string[] current_pks = new string[0]; + // string[] current_fks = new string[0]; + + // foreach (var pk in this.viewmodel.primary_keys) + // { + // if (pk.table == table) + // { + // current_pks = pk.columns; + // break; + // } + // } + + // foreach (var fk in this.viewmodel.foreign_keys) + // { + // if (fk.table == table) + // { + // current_fks = fk.columns; + // break; + // } + // } + + // debug(table); + + // for (int i = 0; i < current_pks.length; i++) + // { + // debug("PK: %s", current_pks[i]); + // } + + // for (int i = 0; i < current_fks.length; i++) + // { + // debug("FK: %s", current_fks[i]); + // } + + // foreach (var col in this.viewmodel.columns) + // { + // if (col.table != table) + // { + // continue; + // } + + // if (col.name in current_fks) + // { + // stringBuilder.append_printf("""""", col.name, col.name, col.column_type); + // } + // else if (col.name in current_pks) + // { + // stringBuilder.append_printf("""""", col.name, col.name, col.column_type); + // } + // else + // { + // stringBuilder.append_printf("""""", col.name, col.name, col.column_type); + // } + // } + + // stringBuilder.append("
$(table)
%s%s
%s%s
%s%s
"); + // var markup = stringBuilder.free_and_steal(); + // return(Gvc.HtmlString.make_html(g, markup)); + // } [GtkChild] private unowned Gtk.Picture pic; diff --git a/src/utils/types.vala b/src/utils/types.vala index 30acca6..3ed27b8 100644 --- a/src/utils/types.vala +++ b/src/utils/types.vala @@ -47,6 +47,12 @@ public T autowire () { return((T)container.find_type(typeof(T))); } +public string[] parse_array_result(string array_str) { + int len = array_str.length - 2; + string content = array_str.substring(1, len); + return(Csv.parse_row(content)); +} + public class Vec : Object { static int DEFAULT_CAPACITY = 16; @@ -54,15 +60,15 @@ public class Vec : Object { private int size; private int capacity; - + public delegate bool Predicate (T item); public Vec() { this.with_capacity(DEFAULT_CAPACITY); } public Vec.with_data(owned T[] data) { - this.data = data; - this.size = data.length; + this.data = data; + this.size = data.length; this.capacity = data.length; } @@ -83,6 +89,30 @@ public class Vec : Object { this.data[size++] = item; } + public int index(T item) { + for (int i = 0; i < this.size; i++) + { + if (item == this.data[i]) + { + return(i); + } + } + + return(-1); + } + + public int find(Predicate pred) { + for (int i = 0; i < this.size; i++) + { + if (pred(this.data[i])) + { + return(i); + } + } + + return(-1); + } + public T pop() { if (size <= capacity / 4) { @@ -105,46 +135,49 @@ public class Vec : Object { this.data[index] = item; } - public new Vec slice(int begin, int end) { + public new Vec slice(int begin, int end) { bound_check(begin); bound_check(end - 1); - return new Vec.with_data(this.data[begin:end]); + return(new Vec .with_data(this.data[begin:end])); } public bool contains(T item) { bool flag = false; - for (int i = 0; i < size; i++) { - if (data[i] == item) { + for (int i = 0; i < size; i++) + { + if (data[i] == item) + { flag = true; break; } } - return flag; + return(flag); } - public class Iterator { + public class Iterator { private int index; - private Vec vec; + private Vec vec; public Iterator(Vec vec) { - this.vec = vec; + this.vec = vec; this.index = 0; } public bool next() { - return index < vec.size -1 ; + return(index < vec.size - 1); } public T get() { - return this.vec[this.index++]; + return(this.vec[this.index++]); } } private inline void bound_check(int index) { - if (index < 0 || index >= size) { + if (index < 0 || index >= size) + { error("Array index out of bound (index = %d, size = %d)", index, size); } } diff --git a/src/viewmodels/TableStructureViewModel.vala b/src/viewmodels/TableStructureViewModel.vala deleted file mode 100644 index ee5b7cc..0000000 --- a/src/viewmodels/TableStructureViewModel.vala +++ /dev/null @@ -1,214 +0,0 @@ -namespace Psequel { -public class TableStructureViewModel : Object { - public SQLService sql_service { get; set; } - public Table selected_table { get; set; } - public Schema current_schema { get; set; } - - public ObservableList columns { get; set; default = new ObservableList (); } - public ObservableList indexes { get; set; default = new ObservableList (); } - public ObservableList foreign_keys { get; set; default = new ObservableList (); } - public ObservableList primary_keys { get; set; default = new ObservableList (); } - - - public TableStructureViewModel(SQLService sql_service) { - base(); - // debug ("TableStructureViewModel created "); - this.sql_service = sql_service; - EventBus.instance().schema_changed.connect((schema) => { - this.current_schema = schema; - }); - - EventBus.instance().selected_table_changed.connect((table) => { - load_data.begin((obj, res) => { - load_data.end(res); - selected_table = table; - }); - }); - - // selected_table = table; - } - - private async void load_data() { - columns.clear(); - indexes.clear(); - foreign_keys.clear(); - primary_keys.clear(); - - columns.append_all(yield _get_columns(this.current_schema)); - indexes.append_all(yield _get_indexes(this.current_schema)); - foreign_keys.append_all(yield _get_fks(this.current_schema)); - primary_keys.append_all(yield _get_pks(this.current_schema)); - - debug("cols: %d indx: %d fks: %d", columns.size, indexes.size, foreign_keys.size); - } - - private async List _get_columns(Schema schema) { - var list = new List (); - - try { - var query = new Query.with_params(COLUMN_SQL, { schema.name }); - var relation = yield sql_service.exec_query_params(query); - - foreach (var row in relation) - { - var col = new Column(); - col.schemaname = schema.name; - col.name = row[0]; - col.table = row[1]; - col.column_type = row[2]; - col.nullable = row[3] == "YES" ? true : false; - col.default_val = row[4]; - - list.append(col); - } - } catch (PsequelError err) { - debug(err.message); - } - - return(list); - } - - private async List _get_indexes(Schema schema) { - var list = new List (); - - try { - var query = new Query.with_params(INDEX_SQL, { schema.name }); - var relation = yield sql_service.exec_query_params(query); - - foreach (var row in relation) - { - var index = new Index(); - index.schemaname = schema.name; - index.name = row[0]; - index.table = row[1]; - index.size = row[2]; - index.indexdef = row[3]; - - list.append(index); - } - } catch (PsequelError err) { - debug(err.message); - } - - return(list); - } - - private async List _get_fks(Schema schema) { - var list = new List (); - - try { - var query = new Query(FK_SQL2); - var relation = yield sql_service.exec_query(query); - - foreach (var row in relation) - { - var fk = new ForeignKey(); - fk.schemaname = schema.name; - fk.name = row[0]; - fk.table = row[1]; - fk.fk_table = row[3]; - - fk.columns_v2 = parse_array_result(row[2]); - fk.fk_columns_v2 = parse_array_result(row[4]); - - list.append(fk); - } - } catch (PsequelError err) { - debug(err.message); - } - - return(list); - } - - private async List _get_pks(Schema schema) { - var list = new List (); - - try { - var query = new Query.with_params(PK_SQL, { schema.name }); - var relation = yield sql_service.exec_query_params(query); - - foreach (var row in relation) - { - var pk = new PrimaryKey(); - pk.schemaname = schema.name; - pk.name = row[0]; - pk.table = row[1]; - pk.columns = parse_array_result(row[2]); - - list.append(pk); - } - } catch (PsequelError err) { - debug(err.message); - } - - return(list); - } - - private string[] parse_array_result(string array_str) { - int len = array_str.length - 2; - string content = array_str.substring(1, len); - return(Csv.parse_row(content)); - } - - public const string COLUMN_SQL = """ - SELECT column_name, table_name, - case - when domain_name is not null then domain_name - when data_type='character varying' THEN 'varchar('||character_maximum_length||')' - when data_type='numeric' THEN 'numeric('||numeric_precision||','||numeric_scale||')' - else data_type - end as data_type, - is_nullable, column_default - FROM information_schema.columns - WHERE table_schema = $1; - """; - public const string INDEX_SQL = """ - SELECT indexname, tablename, pg_size_pretty(pg_relation_size(indexname::regclass)) as size, indexdef - FROM pg_indexes - WHERE schemaname = $1; - """; - public const string FK_SQL = """ - SELECT con.conname, rel.relname, pg_catalog.pg_get_constraintdef(con.oid, true) as condef - FROM pg_catalog.pg_constraint con - INNER JOIN pg_catalog.pg_class rel - ON rel.oid = con.conrelid - INNER JOIN pg_catalog.pg_namespace nsp - ON nsp.oid = connamespace - WHERE con.contype = 'f' AND nsp.nspname = $1; - """; - - public const string PK_SQL = """ - SELECT - con.conname, - cls1.relname AS table, - ARRAY_AGG(attr1.attname) AS columns - FROM pg_catalog.pg_constraint con - JOIN pg_catalog.pg_class cls1 ON con.conrelid = cls1.oid - JOIN pg_catalog.pg_attribute attr1 ON attr1.attrelid = cls1.oid - JOIN pg_catalog.pg_namespace nsp ON nsp.oid = connamespace - WHERE con.contype = 'p' - AND attr1.attnum = ANY(con.conkey) - AND nsp.nspname = $1 - GROUP BY con.oid, cls1.relname; - """; - - public const string FK_SQL2 = """ - SELECT - con.conname, - cls1.relname AS src_table, - ARRAY_AGG(attr1.attname) AS src_columns, - cls2.relname AS dest_table, - ARRAY_AGG(attr2.attname) AS dest_columns - FROM pg_catalog.pg_constraint con - JOIN pg_catalog.pg_class cls1 ON con.conrelid = cls1.oid - JOIN pg_catalog.pg_class cls2 ON con.confrelid = cls2.oid - JOIN pg_catalog.pg_attribute attr1 ON attr1.attrelid = cls1.oid - JOIN pg_catalog.pg_attribute attr2 ON attr2.attrelid = cls2.oid - WHERE con.contype = 'f' - AND con.confrelid > 0 - AND attr1.attnum = ANY(con.conkey) - AND attr2.attnum = ANY(con.confkey) - GROUP BY con.oid, cls1.relname, cls2.relname - """; -} -} diff --git a/src/viewmodels/TableViewModel.vala b/src/viewmodels/TableViewModel.vala index bfe591e..cdbf2f6 100644 --- a/src/viewmodels/TableViewModel.vala +++ b/src/viewmodels/TableViewModel.vala @@ -17,9 +17,9 @@ public class TableViewModel : BaseViewModel { }); EventBus.instance().schema_changed.connect((schema) => { - tables.clear(); - load_tables.begin(schema); - }); + tables.clear(); + load_tables.begin(schema); + }); } public void select_table(Table ?table) { @@ -45,18 +45,195 @@ public class TableViewModel : BaseViewModel { var query = new Query.with_params(TABLE_LIST, { schema.name }); var relation = yield sql_service.exec_query_params(query); + var table_vec = new Vec (); + foreach (var item in relation) { var table = new Table(schema); table.name = item[0]; tables.append(table); + // table_vec.append(table); } debug("%d tables loaded", tables.size); + + var columns_query = new Query.with_params(COLUMN_SQL, { schema.name }); + var columns_relation = yield sql_service.exec_query_params(columns_query); + + foreach (var item in columns_relation) + { + var col = new Column(); + col.table = item[0]; + col.name = item[1]; + col.column_type = item[2]; + col.nullable = item[3] == "t" ? true : false; + col.default_val = item[4]; + + int index = table_vec.find((table) => { + return(table.name == col.table); + }); + + if (index == -1) + { + var new_table = new Table(schema); + new_table.name = col.table; + new_table.columns.append(col); + table_vec.append(new_table); + continue; + } + + table_vec[index].columns.append(col); + } + + + var indexes_query = new Query.with_params(INDEX_SQL, { schema.name }); + var indexes_relation = yield sql_service.exec_query_params(indexes_query); + + foreach (var item in indexes_relation) + { + var index = new Index(); + index.name = item[0]; + index.table = item[1]; + index.size = item[2]; + index.unique = item[3] == "t" ? true: false; + index.index_type = item[4]; + index.indexdef = item[5]; + + int idx = table_vec.find((table) => { + return(table.name == index.table); + }); + + if (idx == -1) + { + var new_table = new Table(schema); + new_table.name = index.table; + new_table.indexes.append(index); + table_vec.append(new_table); + continue; + } + + table_vec[idx].indexes.append(index); + } + + var primary_query = new Query.with_params(PK_SQL, { schema.name }); + var primary_relation = yield sql_service.exec_query_params(primary_query); + + foreach (var item in primary_relation) + { + var pk = new PrimaryKey(); + pk.name = item[0]; + pk.table = item[1]; + pk.columns = parse_array_result(item[2]); + + + + int idx = table_vec.find((table) => { + return(table.name == pk.table); + }); + + if (idx == -1) + { + var new_table = new Table(schema); + new_table.name = pk.table; + new_table.primaty_keys.append(pk); + table_vec.append(new_table); + continue; + } + + table_vec[idx].primaty_keys.append(pk); + } + + var foreignkey_query = new Query.with_params(FK_SQL, { schema.name }); + var foreignkey_relation = yield sql_service.exec_query_params(foreignkey_query); + + foreach (var item in foreignkey_relation) + { + var fk = new ForeignKey(); + fk.name = item[0]; + fk.table = item[1]; + fk.columns = parse_array_result(item[2]); + fk.fk_table = item[3]; + fk.fk_columns = parse_array_result(item[4]); + + int idx = table_vec.find((table) => { + return(table.name == fk.table); + }); + + if (idx == -1) + { + var new_table = new Table(schema); + new_table.name = fk.table; + new_table.foreign_keys.append(fk); + table_vec.append(new_table); + continue; + } + + table_vec[idx].foreign_keys.append(fk); + } } public const string TABLE_LIST = """ SELECT tablename FROM pg_tables WHERE schemaname=$1; """; + + public const string COLUMN_SQL = """ + SELECT cls.relname AS tbl ,attname AS col, atttypid::regtype AS datatype, attnotnull, pg_get_expr(d.adbin, d.adrelid) AS default_value + FROM pg_attribute a + LEFT JOIN pg_catalog.pg_attrdef d ON (a.attrelid, a.attnum) = (d.adrelid, d.adnum) + LEFT JOIN pg_class cls ON cls.oid = a.attrelid + LEFT JOIN pg_catalog.pg_namespace n ON n.oid = cls.relnamespace + WHERE n.nspname = $1 + AND attnum > 0 + AND NOT attisdropped + AND cls.relkind = 'r' + ORDER BY attnum; + """; + public const string INDEX_SQL = """ + SELECT cls.relname, rel_cls.relname, pg_size_pretty(pg_relation_size(cls.relname::regclass)) as size, indisunique, am.amname + FROM pg_index idx + JOIN pg_class cls ON idx.indexrelid = cls.oid + JOIN pg_class rel_cls ON idx.indrelid = rel_cls.oid + JOIN pg_namespace nsp ON cls.relnamespace = nsp.oid + JOIN pg_am am ON am.oid = cls.relam + WHERE nsp.nspname = $1 AND cls.relkind = 'i'; + + """; + + public const string PK_SQL = """ + SELECT + con.conname, + cls1.relname AS table, + ARRAY_AGG(attr1.attname) AS columns + FROM pg_catalog.pg_constraint con + JOIN pg_catalog.pg_class cls1 ON con.conrelid = cls1.oid + JOIN pg_catalog.pg_attribute attr1 ON attr1.attrelid = cls1.oid + JOIN pg_catalog.pg_namespace nsp ON nsp.oid = connamespace + WHERE con.contype = 'p' + AND attr1.attnum = ANY(con.conkey) + AND nsp.nspname = $1 + GROUP BY con.oid, cls1.relname; + """; + + public const string FK_SQL = """ + SELECT + con.conname, + cls1.relname AS src_table, + ARRAY_AGG(attr1.attname) AS src_columns, + cls2.relname AS dest_table, + ARRAY_AGG(attr2.attname) AS dest_columns + FROM pg_catalog.pg_constraint con + JOIN pg_catalog.pg_class cls1 ON con.conrelid = cls1.oid + JOIN pg_catalog.pg_class cls2 ON con.confrelid = cls2.oid + JOIN pg_catalog.pg_attribute attr1 ON attr1.attrelid = cls1.oid + JOIN pg_catalog.pg_attribute attr2 ON attr2.attrelid = cls2.oid + JOIN pg_catalog.pg_namespace nsp ON con.connamespace = nsp.oid + WHERE + nsp.nspname = $1 + AND con.contype = 'f' + AND con.confrelid > 0 + AND attr1.attnum = ANY(con.conkey) + AND attr2.attnum = ANY(con.confkey) + GROUP BY nsp.nspname, con.oid, cls1.relname, cls2.relname + """; } } From e59476e444b2ce8c43a8dd49a1c1becb3391cccf Mon Sep 17 00:00:00 2001 From: ppvan Date: Mon, 15 Apr 2024 23:33:59 +0700 Subject: [PATCH 07/10] feat: table graph scale --- res/gtk/table-graph.blp | 8 +- src/meson.build | 1 + src/ui/widgets/Shape.vala | 169 +++++++++++++++++++++++ src/ui/widgets/TableGraph.vala | 211 +++++++++++++---------------- src/utils/types.vala | 9 +- src/viewmodels/TableViewModel.vala | 15 +- 6 files changed, 287 insertions(+), 126 deletions(-) create mode 100644 src/ui/widgets/Shape.vala diff --git a/res/gtk/table-graph.blp b/res/gtk/table-graph.blp index d00531d..f6409df 100644 --- a/res/gtk/table-graph.blp +++ b/res/gtk/table-graph.blp @@ -4,15 +4,15 @@ template $PsequelTableGraph: Box { spacing: 12; vexpand: true; hexpand: true; - halign: center; margin-top: 4; margin-bottom: 4; margin-start: 4; margin-end: 4; - + styles ["view"] - Picture pic { - content-fit: contain; + DrawingArea area { + hexpand: true; + vexpand: true; } } diff --git a/src/meson.build b/src/meson.build index b594e16..f4659b2 100644 --- a/src/meson.build +++ b/src/meson.build @@ -51,6 +51,7 @@ ui_sources = files([ 'ui/widgets/DataCell.vala', 'ui/widgets/TableRow.vala', 'ui/widgets/TableGraph.vala', + 'ui/widgets/Shape.vala', 'ui/widgets/StyleSwitcher.vala', 'ui/editor/QueryEditor.vala', diff --git a/src/ui/widgets/Shape.vala b/src/ui/widgets/Shape.vala new file mode 100644 index 0000000..53a290f --- /dev/null +++ b/src/ui/widgets/Shape.vala @@ -0,0 +1,169 @@ +using Gtk; +using Gdk; +namespace Psequel { +public interface Shape : Object { + public abstract void draw(Cairo.Context cr); +} + +public sealed class TextBox : Object, Shape { + public static int DEFAULT_PAD = 8; + + private string text; + private Gdk.Rectangle boundary; + + public enum Align + { + CENTER, + LEFT, + RIGHT + } + + public Pango.FontDescription custom_font { get; set; default = Pango.FontDescription.from_string("Roboto 16"); } + public Gdk.RGBA color { get; set; default = { 0, 0, 0, 1 }; } + public Gdk.RGBA bg_color { get; set; default = { 0, 0, 0, 0.0f }; } + public Align text_align { get; set; default = Align.CENTER; } + public bool show_box { get; set; default = true; } + + public TextBox(string text, Gdk.Rectangle rect) { + base(); + this.text = text; + this.boundary = rect; + } + + public void draw(Cairo.Context cr) { + cr.move_to(boundary.x, boundary.y); + + if ((show_box)) + { + cr.save(); + cr.set_source_rgba(bg_color.red, bg_color.green, bg_color.blue, bg_color.alpha); + cr.rectangle(boundary.x, boundary.y, boundary.width, boundary.height); + cr.fill(); + + cr.restore(); + cr.set_source_rgba(color.red, color.green, color.blue, color.alpha); + cr.rectangle(boundary.x, boundary.y, boundary.width, boundary.height); + cr.stroke(); + } + + int text_w = 0, text_h = 0; + + var layout = Pango.cairo_create_layout(cr); + layout.set_font_description(custom_font); + layout.set_text(text, -1); + layout.get_pixel_size(out text_w, out text_h); + layout.set_width((boundary.width - 2 * TextBox.DEFAULT_PAD) * Pango.SCALE); + layout.set_height((boundary.height - 2 * TextBox.DEFAULT_PAD) * Pango.SCALE); + layout.set_ellipsize(Pango.EllipsizeMode.MIDDLE); + layout.set_alignment(Pango.Alignment.CENTER); + + cr.move_to(boundary.x + TextBox.DEFAULT_PAD, boundary.y + (boundary.height - text_h) / 2); + switch (text_align) + { + case Align.CENTER: + layout.set_alignment(Pango.Alignment.CENTER); + break; + + case Align.LEFT: + layout.set_alignment(Pango.Alignment.LEFT); + break; + + case Align.RIGHT: + layout.set_alignment(Pango.Alignment.RIGHT); + break; + + default: + assert_not_reached(); + } + + cr.set_source_rgba(color.red, color.green, color.blue, color.alpha); + Pango.cairo_show_layout(cr, layout); + cr.stroke(); + } +} + +public sealed class TableBox : Object, Shape { + private Table table; + + + public Gdk.Rectangle boundary { get; set; default = { 0, 0, 100, 100 }; } + public Gdk.RGBA color { get; set; default = { 1, 1, 1, 1 }; } + + + // Computed from ui-state + + private bool isHover = false; + + + public TableBox(Table table) { + this.table = table; + } + + public void update(UIContext ctx) { + this.isHover = this.boundary.contains_point((int)ctx.mouse_x, (int)ctx.mouse_y); + } + + public void draw(Cairo.Context cr) { + cr.move_to(boundary.x, boundary.y); + + if (isHover) + { + cr.set_source_rgba(1, 0, 0, color.alpha); + } + else + { + cr.set_source_rgba(color.red, color.green, color.blue, color.alpha); + } + + cr.rectangle(boundary.x, boundary.y, boundary.width, boundary.height); + cr.stroke(); + + int row_height = boundary.height / (1 + table.columns.length); + var header = new TextBox(table.name, { boundary.x, boundary.y, boundary.width, row_height }); + header.custom_font.set_weight(Pango.Weight.BOLD); + header.bg_color = { 64 / 255f, 64 / 255f, 64 / 255f, 1 }; + header.color = this.color; + header.draw(cr); + + + int index = 1; + foreach (var column in table.columns) + { + var col_name = new TextBox(column.name, { boundary.x, boundary.y + index * row_height, boundary.width / 2, row_height }); + col_name.color = this.color; + col_name.text_align = TextBox.Align.LEFT; + + var col_type = new TextBox(column.column_type, { boundary.x + boundary.width / 2, boundary.y + index * row_height, boundary.width / 2, row_height }); + col_type.color = this.color; + col_type.text_align = TextBox.Align.RIGHT; + + col_name.draw(cr); + col_type.draw(cr); + index++; + } + } +} + + +public sealed class CairoIcon : Object, Shape { + private string filepath; + + public CairoIcon(string iconname) { + // uint8[] file_content; + + var filename = "resource:///me/ppvan/psequel/icons/scalable/actions/%s.svg".printf(iconname); + var file = File.new_for_uri(filename); + // file.load_contents(null, out file_content, null); + + this.filepath = file.get_path(); + + // debug(filepath); + // debug ((string)file_content); + } + + public void draw(Cairo.Context cr) { + // Cairo.SvgSurface surface = new Cairo.SvgSurface(this.filepath, 48, 48); + // cr.set_source_surface(surface, 0, 0); + } +} +} diff --git a/src/ui/widgets/TableGraph.vala b/src/ui/widgets/TableGraph.vala index e6c6151..0b3801b 100644 --- a/src/ui/widgets/TableGraph.vala +++ b/src/ui/widgets/TableGraph.vala @@ -3,7 +3,10 @@ using Rsvg; namespace Psequel { [GtkTemplate(ui = "/me/ppvan/psequel/gtk/table-graph.ui")] public class TableGraph : Gtk.Box { - private uint8[] buff; + private TableViewModel viewmodel; + + private TableBox current_table; + private UIContext ctx; public TableGraph() { @@ -11,130 +14,106 @@ public class TableGraph : Gtk.Box { } construct { - // this.viewmodel = autowire (); + this.viewmodel = autowire (); + + this.viewmodel.notify["selected-table"].connect(() => { + debug("Test: %s", this.viewmodel.selected_table.name); + var table = this.viewmodel.selected_table; + this.current_table = new TableBox(table); + + area.queue_draw(); + }); + + this.ctx = new UIContext(); - // this.viewmodel.notify["selected-table"].connect(() => { - // debug("Test: %s", this.viewmodel.selected_table.name); - // var table = this.viewmodel.selected_table; - // this.render_graph.begin(table); - // }); + + this.realize.connect(() => { + var scrollEvent = new Gtk.EventControllerScroll(Gtk.EventControllerScrollFlags.VERTICAL); + scrollEvent.scroll.connect(this.handle_scroll); + + + area.add_controller(scrollEvent); + area.set_draw_func(redraw); + }); } - public async void render_graph(Table table) { - // var fks = this.viewmodel.foreign_keys; - // uint8[] buff = generate_graph(table, fks.to_list()); - // var svgPaintable = new SvgPaintable(buff); + private bool handle_scroll(Gtk.EventControllerScroll event, double dx, double dy) { + Gdk.ModifierType mask = event.get_current_event_state(); + if (mask != Gdk.ModifierType.CONTROL_MASK) + { + return(false); + } + + if (dy > 0) + { + this.ctx.zoom *= 0.9; + } + else + { + this.ctx.zoom *= 1.1; + } - // pic.set_paintable(svgPaintable); + debug("scrolling, zoom: %.2f", this.ctx.zoom); + + this.area.queue_draw(); + + return(true); } - public uint8[] generate_graph(Table table, List fks) { - // var gvc = new Gvc.Context(); - // var g = new Gvc.Graph("g", Gvc.Agdirected, 0); - // g.safe_set("rankdir", "LR", ""); - // g.safe_set("fontname", "Roboto", ""); - // g.safe_set("bgcolor", "transparent", ""); - - // foreach (var item in fks) - // { - // if (item.table != table.name && item.fk_table != table.name) - // { - // continue; - // } - - // var begin = g.create_node(item.table); - // var end = g.create_node(item.fk_table); - - // // var begin_label = generate_table_details(g, item.table); - // // var end_label = generate_table_details(g, item.fk_table); - - // begin.safe_set("fontname", "Roboto", ""); - // begin.safe_set("shape", "plaintext", ""); - // begin.safe_set("label", begin_label, ""); - // begin.safe_set("fontcolor", "#D1CDC7", ""); - // begin.safe_set("color", "#858786", ""); - - - // end.safe_set("fontname", "Roboto", ""); - // end.safe_set("shape", "plaintext", ""); - // end.safe_set("label", end_label, ""); - // end.safe_set("fontcolor", "#D1CDC7", ""); - // end.safe_set("color", "#858786", ""); - // var edge = g.create_edge(begin, end); - // edge.safe_set("color", "#858786", ""); - // edge.safe_set("tailport", item.fk_columns[0], ""); - // edge.safe_set("headport", item.columns[0], ""); - // } - // gvc.layout(g, "dot"); - // gvc.render_data(g, "svg", out this.buff); - // gvc.free_layout(g); - - return(this.buff); + private void redraw(Gtk.DrawingArea area, Cairo.Context cr, int width, int height) { + cr.translate(width / 2, height / 2); + cr.scale(ctx.zoom, ctx.zoom); + + cr.set_source_rgb(30 / 255.0, 30 / 255.0, 30 / 255.0); + cr.paint(); + + debug("draw, zoom: %.2f", this.ctx.zoom); + + + var text_h = line_height(cr); + var cur_color = this.get_color(); + + + var table = this.viewmodel.selected_table; + var table_width = width / 2; + var table_height = (table.columns.length + 1) * (text_h + 2 * TextBox.DEFAULT_PAD); + + this.current_table.boundary = { -table_width / 2, -table_height / 2, table_width, table_height }; + this.current_table.color = cur_color; + + current_table.update(ctx); + current_table.draw(cr); + } + + private void mouse_hover() { + debug("Hover"); + } + + private void mouse_click() { + debug("Click"); } - // private Gvc.HtmlString generate_table_details(Gvc.Graph g, string table) { - // var stringBuilder = new StringBuilder("""
"""); - // stringBuilder.append(@""); - // string[] current_pks = new string[0]; - // string[] current_fks = new string[0]; - - // foreach (var pk in this.viewmodel.primary_keys) - // { - // if (pk.table == table) - // { - // current_pks = pk.columns; - // break; - // } - // } - - // foreach (var fk in this.viewmodel.foreign_keys) - // { - // if (fk.table == table) - // { - // current_fks = fk.columns; - // break; - // } - // } - - // debug(table); - - // for (int i = 0; i < current_pks.length; i++) - // { - // debug("PK: %s", current_pks[i]); - // } - - // for (int i = 0; i < current_fks.length; i++) - // { - // debug("FK: %s", current_fks[i]); - // } - - // foreach (var col in this.viewmodel.columns) - // { - // if (col.table != table) - // { - // continue; - // } - - // if (col.name in current_fks) - // { - // stringBuilder.append_printf("""""", col.name, col.name, col.column_type); - // } - // else if (col.name in current_pks) - // { - // stringBuilder.append_printf("""""", col.name, col.name, col.column_type); - // } - // else - // { - // stringBuilder.append_printf("""""", col.name, col.name, col.column_type); - // } - // } - - // stringBuilder.append("
$(table)
%s%s
%s%s
%s%s
"); - // var markup = stringBuilder.free_and_steal(); - // return(Gvc.HtmlString.make_html(g, markup)); - // } + private int line_height(Cairo.Context cr) { + var layout = Pango.cairo_create_layout(cr); + layout.set_font_description(Pango.FontDescription.from_string("Roboto 16")); + layout.set_text("jjjjjjjjjj", -1); + + int text_w = 0, text_h = 0; + layout.get_pixel_size(out text_w, out text_h); + + return(text_h); + } [GtkChild] - private unowned Gtk.Picture pic; + private unowned Gtk.DrawingArea area; +} + +public class UIContext : Object { + public double mouse_x { get; set; } + public double mouse_y { get; set; } + public double zoom { get; set; default = 1.0; } + + public UIContext() { + } } } diff --git a/src/utils/types.vala b/src/utils/types.vala index 3ed27b8..151aac1 100644 --- a/src/utils/types.vala +++ b/src/utils/types.vala @@ -60,6 +60,9 @@ public class Vec : Object { private int size; private int capacity; + public int length {get { + return this.size; + }} public delegate bool Predicate (T item); public Vec() { @@ -123,6 +126,10 @@ public class Vec : Object { return(this.data[--size]); } + public Iterator iterator () { + return new Iterator(this); + } + public new T get(int index) { bound_check(index); @@ -167,7 +174,7 @@ public class Vec : Object { } public bool next() { - return(index < vec.size - 1); + return(index < vec.size); } public T get() { diff --git a/src/viewmodels/TableViewModel.vala b/src/viewmodels/TableViewModel.vala index cdbf2f6..1d33444 100644 --- a/src/viewmodels/TableViewModel.vala +++ b/src/viewmodels/TableViewModel.vala @@ -44,15 +44,13 @@ public class TableViewModel : BaseViewModel { debug("loading tables"); var query = new Query.with_params(TABLE_LIST, { schema.name }); var relation = yield sql_service.exec_query_params(query); - var table_vec = new Vec (); foreach (var item in relation) { var table = new Table(schema); table.name = item[0]; - tables.append(table); - // table_vec.append(table); + table_vec.append(table); } debug("%d tables loaded", tables.size); @@ -83,8 +81,9 @@ public class TableViewModel : BaseViewModel { } table_vec[index].columns.append(col); - } + debug("table-name: %s, columns: %d, col-name: %s", table_vec[index].name, table_vec[index].columns.length, col.name); + } var indexes_query = new Query.with_params(INDEX_SQL, { schema.name }); var indexes_relation = yield sql_service.exec_query_params(indexes_query); @@ -170,6 +169,12 @@ public class TableViewModel : BaseViewModel { table_vec[idx].foreign_keys.append(fk); } + + this.tables.clear(); + foreach (var item in table_vec) { + this.tables.append(item); + } + } public const string TABLE_LIST = """ @@ -177,7 +182,7 @@ public class TableViewModel : BaseViewModel { """; public const string COLUMN_SQL = """ - SELECT cls.relname AS tbl ,attname AS col, atttypid::regtype AS datatype, attnotnull, pg_get_expr(d.adbin, d.adrelid) AS default_value + SELECT cls.relname AS tbl, attname AS col, format_type(a.atttypid, a.atttypmod) AS datatype, attnotnull, pg_get_expr(d.adbin, d.adrelid) AS default_value FROM pg_attribute a LEFT JOIN pg_catalog.pg_attrdef d ON (a.attrelid, a.attnum) = (d.adrelid, d.adnum) LEFT JOIN pg_class cls ON cls.oid = a.attrelid From 22030bd66f42e33068062d8584d4e732c370b016 Mon Sep 17 00:00:00 2001 From: ppvan Date: Tue, 16 Apr 2024 22:26:10 +0700 Subject: [PATCH 08/10] feat: table graph foreign table --- src/ui/widgets/Shape.vala | 56 ++++++++++++++++++++++++---------- src/ui/widgets/TableGraph.vala | 21 +++---------- 2 files changed, 44 insertions(+), 33 deletions(-) diff --git a/src/ui/widgets/Shape.vala b/src/ui/widgets/Shape.vala index 53a290f..f1bf2ed 100644 --- a/src/ui/widgets/Shape.vala +++ b/src/ui/widgets/Shape.vala @@ -2,7 +2,7 @@ using Gtk; using Gdk; namespace Psequel { public interface Shape : Object { - public abstract void draw(Cairo.Context cr); + public abstract void draw(Cairo.Context cr, int width, int height); } public sealed class TextBox : Object, Shape { @@ -30,7 +30,7 @@ public sealed class TextBox : Object, Shape { this.boundary = rect; } - public void draw(Cairo.Context cr) { + public void draw(Cairo.Context cr, int width, int height) { cr.move_to(boundary.x, boundary.y); if ((show_box)) @@ -103,18 +103,10 @@ public sealed class TableBox : Object, Shape { this.isHover = this.boundary.contains_point((int)ctx.mouse_x, (int)ctx.mouse_y); } - public void draw(Cairo.Context cr) { + public void draw(Cairo.Context cr, int width, int height) { cr.move_to(boundary.x, boundary.y); - if (isHover) - { - cr.set_source_rgba(1, 0, 0, color.alpha); - } - else - { - cr.set_source_rgba(color.red, color.green, color.blue, color.alpha); - } - + cr.set_source_rgba(color.red, color.green, color.blue, color.alpha); cr.rectangle(boundary.x, boundary.y, boundary.width, boundary.height); cr.stroke(); @@ -123,7 +115,7 @@ public sealed class TableBox : Object, Shape { header.custom_font.set_weight(Pango.Weight.BOLD); header.bg_color = { 64 / 255f, 64 / 255f, 64 / 255f, 1 }; header.color = this.color; - header.draw(cr); + header.draw(cr, width, height); int index = 1; @@ -137,10 +129,42 @@ public sealed class TableBox : Object, Shape { col_type.color = this.color; col_type.text_align = TextBox.Align.RIGHT; - col_name.draw(cr); - col_type.draw(cr); + col_name.draw(cr, width, height); + col_type.draw(cr, width, height); index++; } + + int spacing = TextBox.DEFAULT_PAD * 16; + int next_y = -(table.foreign_keys.length * row_height + (table.foreign_keys.length - 1) * spacing / 2); + int next_x = width / 2 - boundary.width / 2 - TextBox.DEFAULT_PAD * 8; + foreach (var fk in table.foreign_keys) + { + debug("name = %s: %s -> %s", fk.name, fk.table, fk.fk_table); + var fk_header = new TextBox(fk.fk_table, { next_x + TextBox.DEFAULT_PAD, next_y, boundary.width / 2, row_height }); + fk_header.custom_font.set_weight(Pango.Weight.BOLD); + fk_header.bg_color = { 64 / 255f, 64 / 255f, 64 / 255f, 1 }; + fk_header.color = this.color; + fk_header.draw(cr, width, height); + + string fk_compose = string.joinv(", ", fk.fk_columns); + var fk_compose_box = new TextBox(fk_compose, { next_x + TextBox.DEFAULT_PAD, next_y + row_height, boundary.width / 2, row_height }); + fk_compose_box.color = this.color; + fk_compose_box.text_align = TextBox.Align.CENTER; + + fk_compose_box.draw(cr, width, height); + + next_y += 2 * row_height + spacing; + } + } +} + +public class Arrow: Object, Shape { + + public Arrow (int x1, int y1, int x2, int y2) { + + } + public void draw(Cairo.Context cr, int width, int height) { + assert_not_reached(); } } @@ -161,7 +185,7 @@ public sealed class CairoIcon : Object, Shape { // debug ((string)file_content); } - public void draw(Cairo.Context cr) { + public void draw(Cairo.Context cr, int width, int height) { // Cairo.SvgSurface surface = new Cairo.SvgSurface(this.filepath, 48, 48); // cr.set_source_surface(surface, 0, 0); } diff --git a/src/ui/widgets/TableGraph.vala b/src/ui/widgets/TableGraph.vala index 0b3801b..6b31b72 100644 --- a/src/ui/widgets/TableGraph.vala +++ b/src/ui/widgets/TableGraph.vala @@ -53,8 +53,6 @@ public class TableGraph : Gtk.Box { this.ctx.zoom *= 1.1; } - debug("scrolling, zoom: %.2f", this.ctx.zoom); - this.area.queue_draw(); return(true); @@ -67,36 +65,25 @@ public class TableGraph : Gtk.Box { cr.set_source_rgb(30 / 255.0, 30 / 255.0, 30 / 255.0); cr.paint(); - debug("draw, zoom: %.2f", this.ctx.zoom); - - var text_h = line_height(cr); var cur_color = this.get_color(); var table = this.viewmodel.selected_table; - var table_width = width / 2; + var table_width = width * 2 / 5; var table_height = (table.columns.length + 1) * (text_h + 2 * TextBox.DEFAULT_PAD); - this.current_table.boundary = { -table_width / 2, -table_height / 2, table_width, table_height }; + this.current_table.boundary = { -(width / 2 - TextBox.DEFAULT_PAD * 8), -table_height / 2, table_width, table_height }; this.current_table.color = cur_color; current_table.update(ctx); - current_table.draw(cr); - } - - private void mouse_hover() { - debug("Hover"); - } - - private void mouse_click() { - debug("Click"); + current_table.draw(cr, width, height); } private int line_height(Cairo.Context cr) { var layout = Pango.cairo_create_layout(cr); layout.set_font_description(Pango.FontDescription.from_string("Roboto 16")); - layout.set_text("jjjjjjjjjj", -1); + layout.set_text("jjjjjjjjjj", -1); // j is the highest character, good for line height measure. int text_w = 0, text_h = 0; layout.get_pixel_size(out text_w, out text_h); From 6d01535c27175fe5598ed5ba6cf7670c40c6f516 Mon Sep 17 00:00:00 2001 From: ppvan Date: Thu, 18 Apr 2024 00:08:58 +0700 Subject: [PATCH 09/10] feat: draw arrow between fks, alow drag gesture on graph --- res/gtk/style.css | 4 -- src/ui/widgets/Shape.vala | 79 +++++++++++++++++++++++++++--- src/ui/widgets/TableGraph.vala | 40 +++++++++++++-- src/viewmodels/TableViewModel.vala | 7 +-- 4 files changed, 112 insertions(+), 18 deletions(-) diff --git a/res/gtk/style.css b/res/gtk/style.css index 27ebe16..3833011 100644 --- a/res/gtk/style.css +++ b/res/gtk/style.css @@ -29,10 +29,6 @@ -gtk-icon-size: 24px; } -border { - border: #1e1e1e 1px solid; -} - .icon-xl image { -gtk-icon-size: 50%; } diff --git a/src/ui/widgets/Shape.vala b/src/ui/widgets/Shape.vala index f1bf2ed..4568bd5 100644 --- a/src/ui/widgets/Shape.vala +++ b/src/ui/widgets/Shape.vala @@ -7,6 +7,7 @@ public interface Shape : Object { public sealed class TextBox : Object, Shape { public static int DEFAULT_PAD = 8; + public static int DEFAULT_LINE_HEIGHT = 20; private string text; private Gdk.Rectangle boundary; @@ -97,6 +98,10 @@ public sealed class TableBox : Object, Shape { public TableBox(Table table) { this.table = table; + + // this.boundary.height = (table.columns.length + 1) * (TextBox.DEFAULT_LINE_HEIGHT + 2 * TextBox.DEFAULT_PAD); + // this.boundary.width = 0; + } public void update(UIContext ctx) { @@ -134,12 +139,11 @@ public sealed class TableBox : Object, Shape { index++; } - int spacing = TextBox.DEFAULT_PAD * 16; - int next_y = -(table.foreign_keys.length * row_height + (table.foreign_keys.length - 1) * spacing / 2); - int next_x = width / 2 - boundary.width / 2 - TextBox.DEFAULT_PAD * 8; + int spacing = (height - table.foreign_keys.length * 2 * row_height) / (table.foreign_keys.length + 1); + int next_y = -(table.foreign_keys.length * row_height + (table.foreign_keys.length - 1) * spacing / 2); + int next_x = width / 2 - boundary.width / 2 - TextBox.DEFAULT_PAD * 8; foreach (var fk in table.foreign_keys) { - debug("name = %s: %s -> %s", fk.name, fk.table, fk.fk_table); var fk_header = new TextBox(fk.fk_table, { next_x + TextBox.DEFAULT_PAD, next_y, boundary.width / 2, row_height }); fk_header.custom_font.set_weight(Pango.Weight.BOLD); fk_header.bg_color = { 64 / 255f, 64 / 255f, 64 / 255f, 1 }; @@ -153,18 +157,79 @@ public sealed class TableBox : Object, Shape { fk_compose_box.draw(cr, width, height); + string col_compose = string.joinv(", ", fk.columns); + var col_index = table.columns.find((_col) => { + return _col.name == col_compose; + }); + + if (col_index != -1) { + var arrow = new Arrow({ boundary.x + boundary.width, boundary.y + (col_index + 1) * row_height + row_height / 2 }, { next_x + TextBox.DEFAULT_PAD, next_y + row_height }); + arrow.draw(cr, width, height); + } else { + // TODO: handle compose foreign key case (2 or more column in 1 fk) + } + next_y += 2 * row_height + spacing; } } } -public class Arrow: Object, Shape { +public struct Vec2D +{ + double x; + double y; + + public Vec2D add(Vec2D other) { + return({ this.x + other.x, this.y + other.y }); + } + + public Vec2D substract(Vec2D other) { + return({ this.x - other.x, this.y - other.y }); + } + + public Vec2D orthogonal() { + return({ -this.y, this.x }); + } + + public Vec2D divide(double d) { + return({ this.x / d, this.y / d }); + } + + public Vec2D normalize() { + var length = GLib.Math.hypot(this.x, this.y); + return({ this.x / length, this.y / length }); + } + + public string to_str() { + return("(%.2f, %.2f)".printf(x, y)); + } +} + +public class Arrow : Object, Shape { + private Vec2D tail; + private Vec2D head; - public Arrow (int x1, int y1, int x2, int y2) { + public Arrow(Vec2D tail, Vec2D head) { + this.tail = tail; + this.head = head; } + public void draw(Cairo.Context cr, int width, int height) { - assert_not_reached(); + var orthogonal = tail.substract(head).orthogonal().normalize(); + var mid = tail.add(head).divide(2); + var p2 = mid.add(orthogonal.divide(1 / 64.0)); + var p1 = mid.substract(orthogonal.divide(1 / 64.0)); + + cr.move_to(tail.x, tail.y); + + if (tail.y < head.y) { + cr.curve_to(p2.x, p2.y, p1.x, p1.y, head.x, head.y); + } else { + cr.curve_to(p1.x, p1.y, p2.x, p2.y, head.x, head.y); + } + + cr.stroke(); } } diff --git a/src/ui/widgets/TableGraph.vala b/src/ui/widgets/TableGraph.vala index 6b31b72..31e7595 100644 --- a/src/ui/widgets/TableGraph.vala +++ b/src/ui/widgets/TableGraph.vala @@ -17,9 +17,9 @@ public class TableGraph : Gtk.Box { this.viewmodel = autowire (); this.viewmodel.notify["selected-table"].connect(() => { - debug("Test: %s", this.viewmodel.selected_table.name); var table = this.viewmodel.selected_table; this.current_table = new TableBox(table); + this.ctx = new UIContext(); area.queue_draw(); }); @@ -31,8 +31,14 @@ public class TableGraph : Gtk.Box { var scrollEvent = new Gtk.EventControllerScroll(Gtk.EventControllerScrollFlags.VERTICAL); scrollEvent.scroll.connect(this.handle_scroll); + var dragEvent = new Gtk.GestureDrag(); + dragEvent.drag_update.connect(this.drag_update); + dragEvent.drag_end.connect(this.drag_end); + + area.add_controller(scrollEvent); + area.add_controller(dragEvent); area.set_draw_func(redraw); }); } @@ -58,14 +64,34 @@ public class TableGraph : Gtk.Box { return(true); } + private void drag_end(Gtk.GestureDrag drag, double x, double y) { + this.ctx.last_x += x; + this.ctx.last_y += y; + this.ctx.offset_x = 0; + this.ctx.offset_y = 0; + area.queue_draw(); + } + + private void drag_update(Gtk.GestureDrag drag, double x, double y) { + drag.get_offset(out x, out y); + this.ctx.offset_x = x; + this.ctx.offset_y = y; + area.queue_draw(); + } + + + private void redraw(Gtk.DrawingArea area, Cairo.Context cr, int width, int height) { - cr.translate(width / 2, height / 2); + + + cr.translate(width / 2 + ctx.last_x + ctx.offset_x, height / 2 + ctx.last_y + ctx.offset_y); cr.scale(ctx.zoom, ctx.zoom); cr.set_source_rgb(30 / 255.0, 30 / 255.0, 30 / 255.0); cr.paint(); - var text_h = line_height(cr); + var text_h = line_height(cr); + var cur_color = this.get_color(); @@ -82,7 +108,7 @@ public class TableGraph : Gtk.Box { private int line_height(Cairo.Context cr) { var layout = Pango.cairo_create_layout(cr); - layout.set_font_description(Pango.FontDescription.from_string("Roboto 16")); + layout.set_font_description(Pango.FontDescription.from_string("Roboto 12")); layout.set_text("jjjjjjjjjj", -1); // j is the highest character, good for line height measure. int text_w = 0, text_h = 0; @@ -98,6 +124,12 @@ public class TableGraph : Gtk.Box { public class UIContext : Object { public double mouse_x { get; set; } public double mouse_y { get; set; } + public double offset_x { get; set; default = 0; } + public double offset_y { get; set; default = 0; } + + public double last_x { get; set; default = 0; } + public double last_y { get; set; default = 0; } + public double zoom { get; set; default = 1.0; } public UIContext() { diff --git a/src/viewmodels/TableViewModel.vala b/src/viewmodels/TableViewModel.vala index 1d33444..56334ab 100644 --- a/src/viewmodels/TableViewModel.vala +++ b/src/viewmodels/TableViewModel.vala @@ -200,8 +200,7 @@ public class TableViewModel : BaseViewModel { JOIN pg_class rel_cls ON idx.indrelid = rel_cls.oid JOIN pg_namespace nsp ON cls.relnamespace = nsp.oid JOIN pg_am am ON am.oid = cls.relam - WHERE nsp.nspname = $1 AND cls.relkind = 'i'; - + WHERE nsp.nspname = $1 AND cls.relkind = 'i' AND NOT indisprimary; """; public const string PK_SQL = """ @@ -225,7 +224,8 @@ public class TableViewModel : BaseViewModel { cls1.relname AS src_table, ARRAY_AGG(attr1.attname) AS src_columns, cls2.relname AS dest_table, - ARRAY_AGG(attr2.attname) AS dest_columns + ARRAY_AGG(attr2.attname) AS dest_columns, + ARRAY_AGG(attr1.attnum) AS src_columns_num FROM pg_catalog.pg_constraint con JOIN pg_catalog.pg_class cls1 ON con.conrelid = cls1.oid JOIN pg_catalog.pg_class cls2 ON con.confrelid = cls2.oid @@ -239,6 +239,7 @@ public class TableViewModel : BaseViewModel { AND attr1.attnum = ANY(con.conkey) AND attr2.attnum = ANY(con.confkey) GROUP BY nsp.nspname, con.oid, cls1.relname, cls2.relname + ORDER BY src_table, src_columns_num; """; } } From d223bc6523e9c2f0183dc10627712feed96c279c Mon Sep 17 00:00:00 2001 From: ppvan Date: Thu, 18 Apr 2024 21:28:58 +0700 Subject: [PATCH 10/10] fix: fix build error --- src/meson.build | 6 +++-- src/ui/widgets/TableGraph.vala | 2 +- src/utils/helpers.vala | 48 ---------------------------------- 3 files changed, 5 insertions(+), 51 deletions(-) diff --git a/src/meson.build b/src/meson.build index f4659b2..b1b7fcc 100644 --- a/src/meson.build +++ b/src/meson.build @@ -84,6 +84,9 @@ if not csv_dep.found() libsimple_dep = libsimple_proj.get_variable('libcsv_deps') endif +cc = meson.get_compiler('c') +math_dep = cc.find_library('m', required : false) + psequel_deps = [ dependency('glib-2.0', version: '>=2.74'), @@ -93,9 +96,8 @@ psequel_deps = [ dependency('gtksourceview-5', version: '>= 5.0'), dependency('libpq', version: '>= 15.3'), dependency('sqlite3'), - dependency('libgvc'), - dependency('librsvg-2.0'), csv_dep, + math_dep, dependency('pgquery-vala'), valac.find_library('config', dirs: vapi_dir), ] diff --git a/src/ui/widgets/TableGraph.vala b/src/ui/widgets/TableGraph.vala index 31e7595..219b952 100644 --- a/src/ui/widgets/TableGraph.vala +++ b/src/ui/widgets/TableGraph.vala @@ -1,4 +1,4 @@ -using Rsvg; + namespace Psequel { [GtkTemplate(ui = "/me/ppvan/psequel/gtk/table-graph.ui")] diff --git a/src/utils/helpers.vala b/src/utils/helpers.vala index 341e178..af43187 100644 --- a/src/utils/helpers.vala +++ b/src/utils/helpers.vala @@ -79,52 +79,4 @@ public class MonospaceFilter : Gtk.Filter { } } - -public class SvgPaintable : Gdk.Paintable, Object { - public uint8[] data { get; set; } - public Rsvg.Handle handle { get; private set; } - - public SvgPaintable(uint8[] data) { - this.data = data; - this.handle = new Rsvg.Handle.from_data(this.data); - } - - - public void snapshot(Gdk.Snapshot snapshot, double width, double height) { - var gtkSnapshot = snapshot as Gtk.Snapshot; - Graphene.Rect rect = Graphene.Rect() { - }; - rect = rect.init(0, 0, (float)width, (float)height); - - var svgRect = Rsvg.Rectangle() { - x = 0, - y = 0, - width = width, - height = height - }; - - - var cr = gtkSnapshot.append_cairo(rect); - - try { - this.handle.render_document(cr, svgRect); - } catch (GLib.Error err) { - debug(err.message); - } - } - - public int get_intrinsic_width() { - double width; - handle.get_intrinsic_size_in_pixels(out width, null); - - return((int)Math.ceil(width)); - } - - public int get_intrinsic_height() { - double height; - handle.get_intrinsic_size_in_pixels(null, out height); - - return((int)Math.ceil(height)); - } -} }