From 931fbb148a88967ab9e6c3086a567bc4849ee0d0 Mon Sep 17 00:00:00 2001 From: ppvan Date: Tue, 18 Jul 2023 14:39:50 +0700 Subject: [PATCH 1/9] Table column info done --- res/gtk/query-view.blp | 1 + res/gtk/table-structure.blp | 37 +++++++++-------- src/models/table.vala | 15 +++++++ src/services/signal.vala | 2 + src/ui/query_view.vala | 18 +++++++++ src/ui/table_structure.vala | 79 ++++++++++++++++++++++++++++++++++--- 6 files changed, 129 insertions(+), 23 deletions(-) diff --git a/res/gtk/query-view.blp b/res/gtk/query-view.blp index f2eab86..92bf854 100644 --- a/res/gtk/query-view.blp +++ b/res/gtk/query-view.blp @@ -90,6 +90,7 @@ template $PsequelQueryView : Adw.Bin { vexpand: true; ListBox table_list { styles ["navigation-sidebar"] + selected-rows-changed => $table_selected(); } } diff --git a/res/gtk/table-structure.blp b/res/gtk/table-structure.blp index b3b1d58..896f8d0 100644 --- a/res/gtk/table-structure.blp +++ b/res/gtk/table-structure.blp @@ -9,7 +9,6 @@ template $PsequelTableStructure : Gtk.Box { margin-top: 12; margin-bottom: 12; - styles ["frame"] // width-request: 960; // height-request: 800; @@ -19,12 +18,12 @@ template $PsequelTableStructure : Gtk.Box { label: "Columns"; halign: start; } - ColumnView columns { - - show-column-separators: true; - show-row-separators: true; - vexpand: true; - styles ["frame"] + ScrolledWindow { + ColumnView columns { + show-column-separators: true; + show-row-separators: true; + vexpand: true; + } } Label { @@ -32,12 +31,14 @@ template $PsequelTableStructure : Gtk.Box { label: "Indexes"; halign: start; } - ColumnView indexes { + + ScrolledWindow { + ColumnView indexes { - show-column-separators: true; - show-row-separators: true; - styles ["frame"] - vexpand: true; + show-column-separators: true; + show-row-separators: true; + vexpand: true; + } } Label { @@ -45,11 +46,13 @@ template $PsequelTableStructure : Gtk.Box { label: "Foreign Keys"; halign: start; } - ColumnView foreign_key { - show-column-separators: true; - show-row-separators: true; - styles ["frame"] - vexpand: true; + ScrolledWindow { + ColumnView foreign_key { + + show-column-separators: true; + show-row-separators: true; + vexpand: true; + } } } diff --git a/src/models/table.vala b/src/models/table.vala index bc87f94..742e8cc 100644 --- a/src/models/table.vala +++ b/src/models/table.vala @@ -53,6 +53,7 @@ namespace Psequel { return data.get (index); } + /** * Helper class for ease of use with Table. DO NOT use it outside of Table class. */ @@ -80,6 +81,16 @@ namespace Psequel { return data.get (index); } + public unowned string? get_field (string name) { + + int index = header.index_of (name); + if (index >= 0) { + return @get (index); + } + + return null; + } + public string to_string () { var builder = new StringBuilder (""); @@ -109,6 +120,10 @@ namespace Psequel { data.add (item); } + public int index_of (string item) { + return data.index_of (item); + } + public new unowned string @get (int index) { return data.get (index); } diff --git a/src/services/signal.vala b/src/services/signal.vala index a9987b7..f180330 100644 --- a/src/services/signal.vala +++ b/src/services/signal.vala @@ -11,6 +11,8 @@ namespace Psequel { public signal void table_list_changed (); + public signal void table_selected_changed (string schema, string table); + public signal void database_connected (); /** * Should only init onces by the resource manager. Should be single on but i'm lazy diff --git a/src/ui/query_view.vala b/src/ui/query_view.vala index e008b50..bb3b3db 100644 --- a/src/ui/query_view.vala +++ b/src/ui/query_view.vala @@ -61,6 +61,23 @@ namespace Psequel { signals.table_list_changed (); } + + [GtkCallback] + private void table_selected () { + var row = table_list.get_selected_row (); + + if (row == null) { + signals.table_selected_changed ("", ""); + } else { + var cur_schema = schema.get_selected_item () as Gtk.StringObject; + assert_nonnull (cur_schema); + + var label = row.child.get_last_child () as Gtk.Label; + + debug ("Emit table_selected_changed"); + signals.table_selected_changed (cur_schema.string, label.get_label ()); + } + } /** * Filter table name base on seach entry. */ @@ -104,6 +121,7 @@ namespace Psequel { var relations = yield query_service.db_tablenames (cur_schema); + table_list.unselect_all (); table_names.clear (); foreach (var item in relations) { table_names.add (item); diff --git a/src/ui/table_structure.vala b/src/ui/table_structure.vala index 81ae23a..810a641 100644 --- a/src/ui/table_structure.vala +++ b/src/ui/table_structure.vala @@ -6,6 +6,10 @@ namespace Psequel { private AppSignals signals; private QueryService query_service; + private Table table; + private ObservableArrayList data_model; + private Gtk.SingleSelection selection_model; + public TableStructure () { Object (); } @@ -15,17 +19,50 @@ namespace Psequel { query_service = ResourceManager.instance ().query_service; signals = ResourceManager.instance ().signals; - signals.database_connected.connect (() => { - query_service.db_table_info.begin ("public", "city", (object, res) => { - Table table = query_service.db_table_info.end (res); + signals.table_selected_changed.connect ((schema, tbname) => { + debug ("%s, %s", schema, tbname); + + query_service.db_table_info.begin (schema, tbname, (obj, res) => { + this.table = query_service.db_table_info.end (res); - debug (table.to_string ()); - foreach (var item in table) { - debug (item.to_string ()); + data_model.clear (); + foreach (var row in table) { + data_model.add (row); } }); }); + var titles = new string[] { + "Column Name", + "Type", + "Length", + "Nullable", + "Default Value" + }; + + data_model = new ObservableArrayList (); + + foreach (var title in titles) { + var factory = new Gtk.SignalListItemFactory (); + + factory.setup.connect ((_fact, _item) => { + var label = new Gtk.Label (null); + _item.child = label; + }); + + factory.bind.connect ((_fact, _item) => { + var row = _item.item as Table.Row; + var label = _item.child as Gtk.Label; + label.label = row.get_field (title) ?? "UNKNOWN"; + }); + + Gtk.ColumnViewColumn column = new Gtk.ColumnViewColumn (title, factory); + column.set_expand (true); + columns.append_column (column); + } + + selection_model = new Gtk.SingleSelection (data_model); + columns.set_model (selection_model); } [GtkChild] @@ -35,4 +72,34 @@ namespace Psequel { [GtkChild] private unowned Gtk.ColumnView foreign_key; } + + public class TableFactory : Object { + public Gtk.SignalListItemFactory data { get; private set; } + + + public TableFactory () { + Object (); + } + + construct { + data = new Gtk.SignalListItemFactory (); + data.setup.connect (list_item_setup); + data.bind.connect (list_item_bind); + } + + public void list_item_setup (Gtk.SignalListItemFactory fact, Gtk.ListItem item) { + var switch_ = new Gtk.Switch (); + item.set_child (switch_); + + debug ("setup"); + } + + public void list_item_bind (Gtk.SignalListItemFactory fact, Gtk.ListItem item) { + var row = item.item as Table.Row; + // var label = item.child as Gtk.Label; + // label.set_label ("Ahihi"); + + // debug ("bind: %s", label.label); + } + } } \ No newline at end of file From eba0185918a4715f0139721f0e50388496a7f5b9 Mon Sep 17 00:00:00 2001 From: ppvan Date: Tue, 18 Jul 2023 21:03:03 +0700 Subject: [PATCH 2/9] Add table index UI --- src/services/query_service.vala | 14 +++++ src/ui/table_structure.vala | 107 +++++++++++++++++--------------- 2 files changed, 72 insertions(+), 49 deletions(-) diff --git a/src/services/query_service.vala b/src/services/query_service.vala index bd787d6..f0555ca 100644 --- a/src/services/query_service.vala +++ b/src/services/query_service.vala @@ -43,6 +43,20 @@ namespace Psequel { return yield exec_query_params (stmt, params); } + public async Table db_table_indexes (string schema, string table_name) throws PsequelError { + string stmt = """ + SELECT indexname, indexdef FROM pg_indexes + WHERE schemaname = $1 + AND tablename = $2; + """; + + var params = new ArrayList (); + params.add (new Variant.string (schema)); + params.add (new Variant.string (table_name)); + + return yield exec_query_params (stmt, params); + } + public async Table db_tablenames (string schema = "public") throws PsequelError { var builder = new StringBuilder ("select tablename from pg_tables where schemaname="); diff --git a/src/ui/table_structure.vala b/src/ui/table_structure.vala index 810a641..495c48c 100644 --- a/src/ui/table_structure.vala +++ b/src/ui/table_structure.vala @@ -6,9 +6,10 @@ namespace Psequel { private AppSignals signals; private QueryService query_service; - private Table table; - private ObservableArrayList data_model; - private Gtk.SingleSelection selection_model; + // private Table table; + private ObservableArrayList columns_model; + private ObservableArrayList index_model; + // private Gtk.SingleSelection selection_model; public TableStructure () { Object (); @@ -19,20 +20,17 @@ namespace Psequel { query_service = ResourceManager.instance ().query_service; signals = ResourceManager.instance ().signals; - signals.table_selected_changed.connect ((schema, tbname) => { - debug ("%s, %s", schema, tbname); + columns_model = new ObservableArrayList (); + index_model = new ObservableArrayList (); - query_service.db_table_info.begin (schema, tbname, (obj, res) => { - this.table = query_service.db_table_info.end (res); - data_model.clear (); - foreach (var row in table) { - data_model.add (row); - } - }); + signals.table_selected_changed.connect ((schema, tbname) => { + debug ("%s, %s", schema, tbname); + reload_table_columns.begin (schema, tbname); + reload_table_indexes.begin (schema, tbname); }); - var titles = new string[] { + var columns_title = new string[] { "Column Name", "Type", "Length", @@ -40,29 +38,69 @@ namespace Psequel { "Default Value" }; - data_model = new ObservableArrayList (); + var index_titles = new string[] { + "Index Name", + "Index Definition" + }; + + set_up_view (columns_title, columns_model, columns); + set_up_view (index_titles, index_model, indexes); + } - foreach (var title in titles) { + void set_up_view (string[] titles, ListModel model, Gtk.ColumnView view) { + for (int i = 0; i < titles.length; i++) { var factory = new Gtk.SignalListItemFactory (); + factory.set_data ("index", i); factory.setup.connect ((_fact, _item) => { var label = new Gtk.Label (null); + label.halign = Gtk.Align.START; + label.margin_start = 8; _item.child = label; }); factory.bind.connect ((_fact, _item) => { var row = _item.item as Table.Row; var label = _item.child as Gtk.Label; - label.label = row.get_field (title) ?? "UNKNOWN"; + int index = _fact.get_data ("index"); + label.label = row[index]; }); - Gtk.ColumnViewColumn column = new Gtk.ColumnViewColumn (title, factory); + Gtk.ColumnViewColumn column = new Gtk.ColumnViewColumn (titles[i], factory); column.set_expand (true); - columns.append_column (column); + view.append_column (column); } - selection_model = new Gtk.SingleSelection (data_model); - columns.set_model (selection_model); + var selection_model = new Gtk.SingleSelection (model); + view.set_model (selection_model); + } + + private async void reload_table_columns (string schema, string tbname) { + try { + var relation = yield query_service.db_table_info (schema, tbname); + columns_model.clear (); + foreach (var row in relation) { + columns_model.add (row); + } + + } catch (PsequelError err) { + debug (err.message); + // ResourceManager.instance ().app + } + } + + private async void reload_table_indexes (string schema, string tbname) { + try { + var relation = yield query_service.db_table_info (schema, tbname); + index_model.clear (); + foreach (var row in relation) { + index_model.add (row); + } + + } catch (PsequelError err) { + debug (err.message); + // ResourceManager.instance ().app + } } [GtkChild] @@ -73,33 +111,4 @@ namespace Psequel { private unowned Gtk.ColumnView foreign_key; } - public class TableFactory : Object { - public Gtk.SignalListItemFactory data { get; private set; } - - - public TableFactory () { - Object (); - } - - construct { - data = new Gtk.SignalListItemFactory (); - data.setup.connect (list_item_setup); - data.bind.connect (list_item_bind); - } - - public void list_item_setup (Gtk.SignalListItemFactory fact, Gtk.ListItem item) { - var switch_ = new Gtk.Switch (); - item.set_child (switch_); - - debug ("setup"); - } - - public void list_item_bind (Gtk.SignalListItemFactory fact, Gtk.ListItem item) { - var row = item.item as Table.Row; - // var label = item.child as Gtk.Label; - // label.set_label ("Ahihi"); - - // debug ("bind: %s", label.label); - } - } } \ No newline at end of file From c445041ae688d83353c2d55ec3f9f097e33cfb9a Mon Sep 17 00:00:00 2001 From: ppvan Date: Wed, 19 Jul 2023 17:16:41 +0700 Subject: [PATCH 3/9] Refactor table to onwed data --- src/models/table.vala | 23 ++++++++++------------- src/services/query_service.vala | 5 ++++- src/ui/table_structure.vala | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/models/table.vala b/src/models/table.vala index 742e8cc..a9ed67a 100644 --- a/src/models/table.vala +++ b/src/models/table.vala @@ -4,21 +4,18 @@ using Gee; namespace Psequel { public class Table : Object { - // Keep references to result so data is not destroy. - private Result result; public int rows { get; private set; } private int cols { get; private set; } private Header header { get; private set; } - public ArrayList data; + private ArrayList data; public Table (owned Result res) { Object (); - result = (owned) res; - load_data (); + load_data ((owned) res); } - private void load_data () { + private void load_data (owned Result result) { assert_nonnull (result); rows = result.get_n_tuples (); @@ -60,7 +57,7 @@ namespace Psequel { public class Row : Object { - private ArrayList data; + private ArrayList data; private Header header; public int size { @@ -68,7 +65,7 @@ namespace Psequel { } internal Row (Header header) { - this.data = new ArrayList (); + this.data = new ArrayList (); this.header = header; } @@ -77,11 +74,11 @@ namespace Psequel { data.add (item); } - public new unowned string @get (int index) { + public new string @get (int index) { return data.get (index); } - public unowned string? get_field (string name) { + public string? get_field (string name) { int index = header.index_of (name); if (index >= 0) { @@ -106,14 +103,14 @@ namespace Psequel { * Helper class for ease of use with Table. DO NOT use it outside of Table class. */ public class Header : Object { - private ArrayList data; + private ArrayList data; public int size { get { return data.size; } } internal Header () { - this.data = new ArrayList (); + this.data = new ArrayList (); } public void add_field (string item) { @@ -124,7 +121,7 @@ namespace Psequel { return data.index_of (item); } - public new unowned string @get (int index) { + public new string @get (int index) { return data.get (index); } diff --git a/src/services/query_service.vala b/src/services/query_service.vala index f0555ca..386c3ca 100644 --- a/src/services/query_service.vala +++ b/src/services/query_service.vala @@ -54,7 +54,10 @@ namespace Psequel { params.add (new Variant.string (schema)); params.add (new Variant.string (table_name)); - return yield exec_query_params (stmt, params); + var raw_result = yield exec_query_params (stmt, params); + + + return raw_result; } public async Table db_tablenames (string schema = "public") throws PsequelError { diff --git a/src/ui/table_structure.vala b/src/ui/table_structure.vala index 495c48c..0574873 100644 --- a/src/ui/table_structure.vala +++ b/src/ui/table_structure.vala @@ -91,7 +91,7 @@ namespace Psequel { private async void reload_table_indexes (string schema, string tbname) { try { - var relation = yield query_service.db_table_info (schema, tbname); + var relation = yield query_service.db_table_indexes (schema, tbname); index_model.clear (); foreach (var row in relation) { index_model.add (row); From 2ee91dce98cf8c94ff24971c667df48eb5e59e79 Mon Sep 17 00:00:00 2001 From: ppvan Date: Wed, 19 Jul 2023 18:22:46 +0700 Subject: [PATCH 4/9] add table transform function --- src/models/table.vala | 84 +++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 48 deletions(-) diff --git a/src/models/table.vala b/src/models/table.vala index a9ed67a..04abb75 100644 --- a/src/models/table.vala +++ b/src/models/table.vala @@ -2,40 +2,62 @@ using Postgres; using Gee; namespace Psequel { + + public delegate Table.Row TransFormsFunc (Table.Row row); + public class Table : Object { public int rows { get; private set; } private int cols { get; private set; } - private Header header { get; private set; } private ArrayList data; + private ArrayList headers; public Table (owned Result res) { Object (); load_data ((owned) res); } + private Table.from_data (ArrayList headers, ArrayList data) { + this.headers = headers; + this.data = data; + this.rows = data.size; + this.cols = headers.size; + } + private void load_data (owned Result result) { assert_nonnull (result); rows = result.get_n_tuples (); cols = result.get_n_fields (); - header = new Header (); + this.headers = new ArrayList (); for (int i = 0; i < cols; i++) { - header.add_field (result.get_field_name (i)); + headers.add (result.get_field_name (i)); } this.data = new ArrayList (); for (int i = 0; i < rows; i++) { - data.add (new Row (header)); + data.add (new Row ()); for (int j = 0; j < cols; j++) { data[i].add_field (result.get_value (i, j)); } } } + public Table transform (ArrayList new_headers, TransFormsFunc func) { + + var new_rows = new ArrayList (); + + assert_nonnull (this.data); + foreach (var row in this.data) { + new_rows.add (func (row)); + } + + return new Table.from_data (new_headers, new_rows); + } + public string to_string () { return @"Table ($rows x $cols)"; } @@ -58,34 +80,32 @@ namespace Psequel { private ArrayList data; - private Header header; public int size { get { return data.size; } } - internal Row (Header header) { + internal Row () { this.data = new ArrayList (); - this.header = header; } public void add_field (string item) { - assert (data.size < header.size); data.add (item); } - public new string @get (int index) { - return data.get (index); + public void insert_field (int index, string item) { + data.insert (index, item); } - public string? get_field (string name) { + public void remove_at (int index) { + assert (index < size); + assert (index >= 0); - int index = header.index_of (name); - if (index >= 0) { - return @get (index); - } + data.remove_at (index); + } - return null; + public new string @get (int index) { + return data.get (index); } public string to_string () { @@ -98,37 +118,5 @@ namespace Psequel { return builder.free_and_steal (); } } - - /** - * Helper class for ease of use with Table. DO NOT use it outside of Table class. - */ - public class Header : Object { - private ArrayList data; - - public int size { - get { return data.size; } - } - - internal Header () { - this.data = new ArrayList (); - } - - public void add_field (string item) { - data.add (item); - } - - public int index_of (string item) { - return data.index_of (item); - } - - public new string @get (int index) { - return data.get (index); - } - - public string to_string () { - var row_data = data.fold ((seed, item) => seed + item.dup () + ",", ""); - return row_data; - } - } } } \ No newline at end of file From 12df179d78c8799f9d057e95c32de3d68924cfd7 Mon Sep 17 00:00:00 2001 From: ppvan Date: Wed, 19 Jul 2023 18:22:56 +0700 Subject: [PATCH 5/9] table index view --- src/services/query_service.vala | 37 +++++++++++++++++++++++++++++++-- src/ui/table_structure.vala | 4 +++- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/services/query_service.vala b/src/services/query_service.vala index 386c3ca..5fd3b64 100644 --- a/src/services/query_service.vala +++ b/src/services/query_service.vala @@ -54,10 +54,43 @@ namespace Psequel { params.add (new Variant.string (schema)); params.add (new Variant.string (table_name)); + var headers = new ArrayList (); + headers.add_all_array ({ + "Index Name", + "Unique", + "Type", + "Columns", + }); + var raw_result = yield exec_query_params (stmt, params); - - return raw_result; + var result = raw_result.transform (headers, (old_row) => { + var new_row = new Table.Row (); + new_row.add_field (old_row[0]); + + var indexdef = old_row[1]; + if (indexdef.contains ("UNIQUE")) { + new_row.add_field ("YES"); + } else { + new_row.add_field ("NO"); + } + + // 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)) { + + new_row.add_field (match_info.fetch (1)); + new_row.add_field (match_info.fetch (2)); + } else { + debug ("Regex not match: %s", indexdef); + assert_not_reached (); + } + + return new_row; + }); + + return result; } public async Table db_tablenames (string schema = "public") throws PsequelError { diff --git a/src/ui/table_structure.vala b/src/ui/table_structure.vala index 0574873..6877817 100644 --- a/src/ui/table_structure.vala +++ b/src/ui/table_structure.vala @@ -40,7 +40,9 @@ namespace Psequel { var index_titles = new string[] { "Index Name", - "Index Definition" + "Unique", + "Type", + "Columns", }; set_up_view (columns_title, columns_model, columns); From e3ca514d629ea62c21e68779c4e3089c5fc38da2 Mon Sep 17 00:00:00 2001 From: ppvan Date: Thu, 20 Jul 2023 01:49:28 +0700 Subject: [PATCH 6/9] rename table to relation --- src/application.vala | 2 +- src/models/table.vala | 16 ++++++++-------- src/services/query_service.vala | 20 ++++++++++---------- src/services/resource_manager.vala | 2 +- src/ui/query_view.vala | 10 +++++----- src/ui/table_structure.vala | 17 +++++++++++------ 6 files changed, 36 insertions(+), 31 deletions(-) diff --git a/src/application.vala b/src/application.vala index 395b787..2d12dde 100644 --- a/src/application.vala +++ b/src/application.vala @@ -59,7 +59,7 @@ namespace Psequel { } query_service = new QueryService (background); - table_list = new ObservableArrayList (); + table_list = new ObservableArrayList (); signals = new AppSignals (); load_user_data (); diff --git a/src/models/table.vala b/src/models/table.vala index 04abb75..654476e 100644 --- a/src/models/table.vala +++ b/src/models/table.vala @@ -3,22 +3,22 @@ using Gee; namespace Psequel { - public delegate Table.Row TransFormsFunc (Table.Row row); + public delegate Relation.Row TransFormsFunc (Relation.Row row); - public class Table : Object { + public class Relation : Object { public int rows { get; private set; } - private int cols { get; private set; } + public int cols { get; private set; } private ArrayList data; private ArrayList headers; - public Table (owned Result res) { + public Relation (owned Result res) { Object (); load_data ((owned) res); } - private Table.from_data (ArrayList headers, ArrayList data) { + private Relation.from_data (ArrayList headers, ArrayList data) { this.headers = headers; this.data = data; this.rows = data.size; @@ -46,16 +46,16 @@ namespace Psequel { } } - public Table transform (ArrayList new_headers, TransFormsFunc func) { + public Relation transform (ArrayList new_headers, TransFormsFunc func) { - var new_rows = new ArrayList (); + var new_rows = new ArrayList (); assert_nonnull (this.data); foreach (var row in this.data) { new_rows.add (func (row)); } - return new Table.from_data (new_headers, new_rows); + return new Relation.from_data (new_headers, new_rows); } public string to_string () { diff --git a/src/services/query_service.vala b/src/services/query_service.vala index 5fd3b64..7730de1 100644 --- a/src/services/query_service.vala +++ b/src/services/query_service.vala @@ -15,7 +15,7 @@ namespace Psequel { this.background = background; } - public async Table db_schemas () throws PsequelError { + public async Relation db_schemas () throws PsequelError { var stmt = "select schema_name from information_schema.schemata;"; var res = yield exec_query (stmt); @@ -23,7 +23,7 @@ namespace Psequel { return res; } - public async Table db_table_info (string schema, string table_name) throws PsequelError { + public async Relation db_table_info (string schema, string table_name) throws PsequelError { string stmt = """ SELECT column_name AS "Column Name", data_type AS "Type", @@ -43,7 +43,7 @@ namespace Psequel { return yield exec_query_params (stmt, params); } - public async Table db_table_indexes (string schema, string table_name) throws PsequelError { + public async Relation db_table_indexes (string schema, string table_name) throws PsequelError { string stmt = """ SELECT indexname, indexdef FROM pg_indexes WHERE schemaname = $1 @@ -65,7 +65,7 @@ namespace Psequel { var raw_result = yield exec_query_params (stmt, params); var result = raw_result.transform (headers, (old_row) => { - var new_row = new Table.Row (); + var new_row = new Relation.Row (); new_row.add_field (old_row[0]); var indexdef = old_row[1]; @@ -93,7 +93,7 @@ namespace Psequel { return result; } - public async Table db_tablenames (string schema = "public") throws PsequelError { + public async Relation db_tablenames (string schema = "public") throws PsequelError { var builder = new StringBuilder ("select tablename from pg_tables where schemaname="); builder.append (@"\'$schema\';"); @@ -101,7 +101,7 @@ namespace Psequel { string stmt = builder.free_and_steal (); var res = yield exec_query_internal (stmt); - var table = new Table ((owned) res); + var table = new Relation ((owned) res); return table; } @@ -143,24 +143,24 @@ namespace Psequel { } } - public async Table exec_query (string query) throws PsequelError { + public async Relation exec_query (string query) throws PsequelError { var result = yield exec_query_internal (query); // check query status check_query_status (result); - var table = new Table ((owned) result); + var table = new Relation ((owned) result); return table; } - public async Table exec_query_params (string query, ArrayList params) throws PsequelError { + public async Relation exec_query_params (string query, ArrayList params) throws PsequelError { var result = yield exec_query_params_internal (query, params); // check query status check_query_status (result); - var table = new Table ((owned) result); + var table = new Relation ((owned) result); return table; } diff --git a/src/services/resource_manager.vala b/src/services/resource_manager.vala index 6788f13..1d2086c 100644 --- a/src/services/resource_manager.vala +++ b/src/services/resource_manager.vala @@ -31,7 +31,7 @@ namespace Psequel { /** * List of table in current schema. */ - public ObservableArrayList table_list; + public ObservableArrayList table_list; /** * Application diff --git a/src/ui/query_view.vala b/src/ui/query_view.vala index bb3b3db..053cc41 100644 --- a/src/ui/query_view.vala +++ b/src/ui/query_view.vala @@ -9,7 +9,7 @@ namespace Psequel { private unowned AppSignals signals; - private ObservableArrayList table_names; + private ObservableArrayList table_names; private Gtk.FilterListModel tablelist_model; private Gtk.StringList schema_model; @@ -82,9 +82,9 @@ namespace Psequel { * Filter table name base on seach entry. */ private bool search_filter_func (Object item) { - assert (item is Table.Row); + assert (item is Relation.Row); - var row = item as Table.Row; + var row = item as Relation.Row; var table_name = row.get (0); var search_text = search_entry.text; @@ -133,7 +133,7 @@ namespace Psequel { /** Create row widget from query result. */ private Gtk.ListBoxRow table_row_factory (Object obj) { - var row_data = obj as Table.Row; + var row_data = obj as Relation.Row; var row = new Gtk.ListBoxRow (); @@ -151,7 +151,7 @@ namespace Psequel { } private void set_up_table_list () { - this.table_names = new ObservableArrayList (); + this.table_names = new ObservableArrayList (); var filter = new Gtk.CustomFilter (search_filter_func); this.tablelist_model = new Gtk.FilterListModel (table_names, filter); diff --git a/src/ui/table_structure.vala b/src/ui/table_structure.vala index 6877817..97826e1 100644 --- a/src/ui/table_structure.vala +++ b/src/ui/table_structure.vala @@ -7,8 +7,8 @@ namespace Psequel { private QueryService query_service; // private Table table; - private ObservableArrayList columns_model; - private ObservableArrayList index_model; + private ObservableArrayList columns_model; + private ObservableArrayList index_model; // private Gtk.SingleSelection selection_model; public TableStructure () { @@ -20,8 +20,8 @@ namespace Psequel { query_service = ResourceManager.instance ().query_service; signals = ResourceManager.instance ().signals; - columns_model = new ObservableArrayList (); - index_model = new ObservableArrayList (); + columns_model = new ObservableArrayList (); + index_model = new ObservableArrayList (); signals.table_selected_changed.connect ((schema, tbname) => { @@ -42,7 +42,7 @@ namespace Psequel { "Index Name", "Unique", "Type", - "Columns", + // "Columns", }; set_up_view (columns_title, columns_model, columns); @@ -62,7 +62,7 @@ namespace Psequel { }); factory.bind.connect ((_fact, _item) => { - var row = _item.item as Table.Row; + var row = _item.item as Relation.Row; var label = _item.child as Gtk.Label; int index = _fact.get_data ("index"); label.label = row[index]; @@ -80,6 +80,8 @@ namespace Psequel { private async void reload_table_columns (string schema, string tbname) { try { var relation = yield query_service.db_table_info (schema, tbname); + warn_if_fail (relation.cols == columns.columns.get_n_items ()); + columns_model.clear (); foreach (var row in relation) { columns_model.add (row); @@ -94,6 +96,9 @@ namespace Psequel { private async void reload_table_indexes (string schema, string tbname) { try { var relation = yield query_service.db_table_indexes (schema, tbname); + + warn_if_fail (relation.cols == indexes.columns.get_n_items ()); + index_model.clear (); foreach (var row in relation) { index_model.add (row); From ad17989011af494990d1eb3c0c757bbfee3ebec5 Mon Sep 17 00:00:00 2001 From: ppvan Date: Thu, 20 Jul 2023 02:50:42 +0700 Subject: [PATCH 7/9] table foreign key_view --- src/services/query_service.vala | 51 +++++++++++++++++++++++++++++++++ src/ui/table_structure.vala | 46 +++++++++++++++++++++++++++-- 2 files changed, 94 insertions(+), 3 deletions(-) diff --git a/src/services/query_service.vala b/src/services/query_service.vala index 7730de1..986fc9e 100644 --- a/src/services/query_service.vala +++ b/src/services/query_service.vala @@ -23,6 +23,57 @@ namespace Psequel { return res; } + public async Relation db_table_fk (string schema, string table_name) throws PsequelError { + string stmt = """ + SELECT conname, pg_catalog.pg_get_constraintdef(r.oid, true) as condef + FROM pg_catalog.pg_constraint r + WHERE r.conrelid = $1::regclass AND r.contype = 'f'; + """; + + var params = new ArrayList (); + params.add (new Variant.string (@"$schema.$table_name")); + + var headers = new ArrayList (); + headers.add_all_array ({ + "Key Name", + "Columns", + "Foreign Table", + "Foreign Columns", + "On Update", + "On Delete", + }); + + var raw = yield exec_query_params (stmt, params); + var result = raw.transform (headers, (old_row) => { + var new_row = new Relation.Row (); + new_row.add_field (old_row[0]); + + var fk_def = old_row[1]; + + // 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)) { + + new_row.add_field (match_info.fetch (1)); + new_row.add_field (match_info.fetch (2)); + new_row.add_field (match_info.fetch (3)); + new_row.add_field (match_info.fetch (5) == "" ? "NO ACTION" : match_info.fetch (5)); + new_row.add_field (match_info.fetch (7) == "" ? "NO ACTION" : match_info.fetch (7)); + } else { + debug ("Regex not match: %s", fk_def); + assert_not_reached (); + } + debug ("%s", new_row.to_string ()); + + return new_row; + }); + + + return result; + + } + public async Relation db_table_info (string schema, string table_name) throws PsequelError { string stmt = """ SELECT column_name AS "Column Name", diff --git a/src/ui/table_structure.vala b/src/ui/table_structure.vala index 97826e1..0c0aae2 100644 --- a/src/ui/table_structure.vala +++ b/src/ui/table_structure.vala @@ -9,6 +9,7 @@ namespace Psequel { // private Table table; private ObservableArrayList columns_model; private ObservableArrayList index_model; + private ObservableArrayList fk_model; // private Gtk.SingleSelection selection_model; public TableStructure () { @@ -22,12 +23,14 @@ namespace Psequel { columns_model = new ObservableArrayList (); index_model = new ObservableArrayList (); + fk_model = new ObservableArrayList (); signals.table_selected_changed.connect ((schema, tbname) => { debug ("%s, %s", schema, tbname); reload_table_columns.begin (schema, tbname); reload_table_indexes.begin (schema, tbname); + reload_table_fk.begin (schema, tbname); }); var columns_title = new string[] { @@ -42,11 +45,21 @@ namespace Psequel { "Index Name", "Unique", "Type", - // "Columns", + "Columns", + }; + + var fk_titles = new string[] { + "Key Name", + "Columns", + "Foreign Table", + "Foreign Columns", + "On Update", + "On Delete", }; set_up_view (columns_title, columns_model, columns); set_up_view (index_titles, index_model, indexes); + set_up_view (fk_titles, fk_model, foreign_key); } void set_up_view (string[] titles, ListModel model, Gtk.ColumnView view) { @@ -80,7 +93,11 @@ namespace Psequel { private async void reload_table_columns (string schema, string tbname) { try { var relation = yield query_service.db_table_info (schema, tbname); - warn_if_fail (relation.cols == columns.columns.get_n_items ()); + + if (relation.cols != columns.columns.get_n_items ()) { + debug ("Programming Error: Query result cols != view columns"); + assert_not_reached (); + } columns_model.clear (); foreach (var row in relation) { @@ -97,7 +114,10 @@ namespace Psequel { try { var relation = yield query_service.db_table_indexes (schema, tbname); - warn_if_fail (relation.cols == indexes.columns.get_n_items ()); + if (relation.cols != indexes.columns.get_n_items ()) { + debug ("Programming Error: Query result cols != view columns"); + assert_not_reached (); + } index_model.clear (); foreach (var row in relation) { @@ -110,6 +130,26 @@ namespace Psequel { } } + private async void reload_table_fk (string schema, string tbname) { + try { + var relation = yield query_service.db_table_fk (schema, tbname); + + if (relation.cols != foreign_key.columns.get_n_items ()) { + debug ("Programming Error: Query result cols != view columns"); + assert_not_reached (); + } + + fk_model.clear (); + foreach (var row in relation) { + fk_model.add (row); + } + + } catch (PsequelError err) { + debug (err.message); + // ResourceManager.instance ().app + } + } + [GtkChild] private unowned Gtk.ColumnView columns; [GtkChild] From 924ebb023b390be918f7b3c4dfb13449b67f4c75 Mon Sep 17 00:00:00 2001 From: ppvan Date: Thu, 20 Jul 2023 04:21:04 +0700 Subject: [PATCH 8/9] Implement database views view --- res/gtk/icons/category-search-symbolic.svg | 4 + res/gtk/query-view.blp | 171 +++++++++++++++------ res/gtk/table-data.blp | 0 res/gtk/welcome.blp | 2 +- res/psequel.gresource.xml | 1 + src/services/query_service.vala | 15 ++ src/services/signal.vala | 1 + src/ui/query_view.vala | 95 +++++++++++- 8 files changed, 238 insertions(+), 51 deletions(-) create mode 100644 res/gtk/icons/category-search-symbolic.svg create mode 100644 res/gtk/table-data.blp diff --git a/res/gtk/icons/category-search-symbolic.svg b/res/gtk/icons/category-search-symbolic.svg new file mode 100644 index 0000000..e4de46c --- /dev/null +++ b/res/gtk/icons/category-search-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/gtk/query-view.blp b/res/gtk/query-view.blp index 92bf854..ce85def 100644 --- a/res/gtk/query-view.blp +++ b/res/gtk/query-view.blp @@ -14,7 +14,7 @@ template $PsequelQueryView : Adw.Bin { Box { orientation: vertical; - width-request: 280; + width-request: 300; Box { margin-start: 8; @@ -40,59 +40,136 @@ template $PsequelQueryView : Adw.Bin { Separator {} - Box { + Adw.ViewSwitcher tables_views_switcher { + + height-request: 40; margin-start: 8; margin-end: 8; - margin-top: 4; - margin-bottom: 8; - - Label { - styles ["title-3"] - margin-top: 8; - margin-start: 4; - halign: start; - margin-bottom: 0; - label: "Tables"; - } - - Separator { - hexpand: true; - styles ["spacer"] - } - - ToggleButton search_btn { - styles ["flat"] - tooltip-text: "Search tables"; - icon-name: "loupe-large-symbolic"; - - toggled => $on_show_search(); - } - + margin-top: 8; + // margin-bottom: 8; + policy: wide; + stack: sql_views; } - - - Revealer { - transition-duration: 300; - reveal-child: bind search_btn.active; - styles ["background"] - SearchEntry search_entry { - placeholder-text: "Search Tables"; - margin-start: 4; - margin-bottom: 4; + Adw.ViewStack sql_views { + hexpand: true; + vexpand: true; + Adw.ViewStackPage { + icon-name: "table-symbolic"; + + child: Box { + orientation: vertical; + Box { + margin-start: 8; + margin-end: 8; + margin-top: 4; + margin-bottom: 8; + + Label { + styles ["title-3"] + margin-top: 8; + margin-start: 4; + halign: start; + margin-bottom: 0; + label: "Tables"; + } + + Separator { + hexpand: true; + styles ["spacer"] + } + + ToggleButton search_btn { + styles ["flat"] + tooltip-text: "Search tables"; + icon-name: "loupe-large-symbolic"; + + toggled => $on_show_search(); + } + } - search-changed => $on_search(); + Revealer { + transition-duration: 300; + reveal-child: bind search_btn.active; + styles ["background"] + SearchEntry search_table_entry { + placeholder-text: "Search Tables"; + margin-start: 4; + margin-bottom: 4; + + search-changed => $on_search(); + } + + } + + ScrolledWindow { + vexpand: true; + ListBox table_list { + styles ["navigation-sidebar"] + selected-rows-changed => $table_selected(); + } + + } + }; } - - } - - ScrolledWindow { - vexpand: true; - ListBox table_list { - styles ["navigation-sidebar"] - selected-rows-changed => $table_selected(); + + Adw.ViewStackPage { + icon-name: "category-search-symbolic"; + + child: Box { + orientation: vertical; + Box { + margin-start: 8; + margin-end: 8; + margin-top: 4; + margin-bottom: 8; + + Label { + styles ["title-3"] + margin-top: 8; + margin-start: 4; + halign: start; + margin-bottom: 0; + label: "Views"; + } + + Separator { + hexpand: true; + styles ["spacer"] + } + + ToggleButton search_views_btn { + styles ["flat"] + tooltip-text: "Search tables"; + icon-name: "loupe-large-symbolic"; + + toggled => $on_show_view_search(); + } + } + + Revealer { + transition-duration: 300; + reveal-child: bind search_views_btn.active; + styles ["background"] + SearchEntry search_views_entry { + placeholder-text: "Search Views"; + margin-start: 4; + margin-bottom: 4; + + search-changed => $on_view_search(); + } + + } + + ScrolledWindow { + vexpand: true; + ListBox views_list { + styles ["navigation-sidebar"] + selected-rows-changed => $view_selected(); + } + } + }; } - } Box { diff --git a/res/gtk/table-data.blp b/res/gtk/table-data.blp new file mode 100644 index 0000000..e69de29 diff --git a/res/gtk/welcome.blp b/res/gtk/welcome.blp index c25c248..c1b02de 100644 --- a/res/gtk/welcome.blp +++ b/res/gtk/welcome.blp @@ -14,7 +14,7 @@ template $PsequelConnectionView : Adw.Bin { [start] $PsequelConnectionSidebar sidebar { - width-request: 280; + width-request: 300; form: form; } diff --git a/res/psequel.gresource.xml b/res/psequel.gresource.xml index fbf43ef..55ba3bd 100644 --- a/res/psequel.gresource.xml +++ b/res/psequel.gresource.xml @@ -22,5 +22,6 @@ gtk/icons/columns-symbolic.svg gtk/icons/step-out-symbolic.svg gtk/icons/arrow-into-box-symbolic.svg + gtk/icons/category-search-symbolic.svg diff --git a/src/services/query_service.vala b/src/services/query_service.vala index 986fc9e..febefc8 100644 --- a/src/services/query_service.vala +++ b/src/services/query_service.vala @@ -157,6 +157,21 @@ namespace Psequel { return table; } + public async Relation db_views (string schema = "public") throws PsequelError { + + string stmt = """ + select table_name from INFORMATION_SCHEMA.views WHERE table_schema = $1; + """; + var params = new ArrayList (); + params.add (new Variant.string (schema)); + + var res = yield exec_query_params_internal (stmt, params); + + var table = new Relation ((owned) res); + + return table; + } + public async string db_version () throws PsequelError { string stmt = "SELECT version ();"; diff --git a/src/services/signal.vala b/src/services/signal.vala index f180330..f6f7a92 100644 --- a/src/services/signal.vala +++ b/src/services/signal.vala @@ -9,6 +9,7 @@ namespace Psequel { * Emit when the table list changed. */ public signal void table_list_changed (); + public signal void views_list_changed (); public signal void table_selected_changed (string schema, string table); diff --git a/src/ui/query_view.vala b/src/ui/query_view.vala index 053cc41..eea0a7e 100644 --- a/src/ui/query_view.vala +++ b/src/ui/query_view.vala @@ -10,8 +10,11 @@ namespace Psequel { private ObservableArrayList table_names; + private ObservableArrayList views_names; + private Gtk.FilterListModel tablelist_model; + private Gtk.FilterListModel viewslist_model; private Gtk.StringList schema_model; public QueryView () { @@ -21,9 +24,11 @@ namespace Psequel { construct { query_service = ResourceManager.instance ().query_service; signals = ResourceManager.instance ().signals; + // tables_views_switcher.get_ set_up_table_list (); set_up_schema (); + set_up_views_list (); connect_signals (); } @@ -47,10 +52,28 @@ namespace Psequel { tablelist_model.get_filter ().changed (Gtk.FilterChange.DIFFERENT); } + [GtkCallback] + private void on_view_search (Gtk.SearchEntry entry) { + debug ("Search views: %s", entry.text); + viewslist_model.get_filter ().changed (Gtk.FilterChange.DIFFERENT); + } + + [GtkCallback] + private void view_selected () { + debug ("view selected."); + } + [GtkCallback] private void on_show_search (Gtk.ToggleButton btn) { if (btn.active) { - search_entry.grab_focus (); + search_table_entry.grab_focus (); + } + } + + [GtkCallback] + private void on_show_view_search (Gtk.ToggleButton btn) { + if (btn.active) { + search_views_entry.grab_focus (); } } @@ -59,6 +82,7 @@ namespace Psequel { */ private void schema_changed () { signals.table_list_changed (); + signals.views_list_changed (); } @@ -86,11 +110,24 @@ namespace Psequel { var row = item as Relation.Row; var table_name = row.get (0); - var search_text = search_entry.text; + var search_text = search_table_entry.text; return table_name.contains (search_text); } + /** + * Filter table name base on seach entry. + */ + private bool view_filter_func (Object item) { + assert (item is Relation.Row); + + var row = item as Relation.Row; + var view_name = row.get (0); + var search_text = search_views_entry.text; + + return view_name.contains (search_text); + } + /** * Reload schema list to the drop down by fetching database. */ @@ -130,6 +167,21 @@ namespace Psequel { debug ("Tables list reloaded, got %d tables in schema %s", table_names.size, cur_schema); } + private async void reload_views () throws PsequelError { + + var cur_schema = ((Gtk.StringObject) schema.selected_item).string ?? "public"; + + var relations = yield query_service.db_views (cur_schema); + + views_list.unselect_all (); + views_names.clear (); + foreach (var item in relations) { + views_names.add (item); + } + + debug ("Views list reloaded, got %d views in schema %s", table_names.size, cur_schema); + } + /** Create row widget from query result. */ private Gtk.ListBoxRow table_row_factory (Object obj) { @@ -150,6 +202,24 @@ namespace Psequel { return row; } + private Gtk.ListBoxRow view_row_factory (Object obj) { + var row_data = obj as Relation.Row; + + var row = new Gtk.ListBoxRow (); + + var box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 8); + var icon = new Gtk.Image (); + icon.icon_name = "category-search-symbolic"; + var label = new Gtk.Label (row_data[0]); + + box.append (icon); + box.append (label); + + row.child = box; + + return row; + } + private void set_up_table_list () { this.table_names = new ObservableArrayList (); @@ -158,6 +228,14 @@ namespace Psequel { this.table_list.bind_model (tablelist_model, table_row_factory); } + private void set_up_views_list () { + this.views_names = new ObservableArrayList (); + + var filter = new Gtk.CustomFilter (view_filter_func); + this.viewslist_model = new Gtk.FilterListModel (views_names, filter); + this.views_list.bind_model (viewslist_model, view_row_factory); + } + private void set_up_schema () { this.schema_model = new Gtk.StringList (null); this.schema.set_model (schema_model); @@ -169,6 +247,11 @@ namespace Psequel { reload_tables.begin (); }); + signals.views_list_changed.connect (() => { + debug ("Handle views_list_changed."); + reload_views.begin (); + }); + signals.database_connected.connect (() => { debug ("Handle database_connected."); reload_schema.begin (); @@ -181,7 +264,13 @@ namespace Psequel { private unowned Gtk.ListBox table_list; [GtkChild] - private unowned Gtk.SearchEntry search_entry; + private unowned Gtk.SearchEntry search_table_entry; + + [GtkChild] + private unowned Gtk.ListBox views_list; + + [GtkChild] + private unowned Gtk.SearchEntry search_views_entry; [GtkChild] private unowned Gtk.DropDown schema; From ecc2c9cdaeb2fae07719c96a33e3351166e16fde Mon Sep 17 00:00:00 2001 From: ppvan Date: Thu, 20 Jul 2023 04:33:58 +0700 Subject: [PATCH 9/9] change icon bigger --- res/gtk/query-view.blp | 1 + res/gtk/style.css | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/res/gtk/query-view.blp b/res/gtk/query-view.blp index ce85def..82b461b 100644 --- a/res/gtk/query-view.blp +++ b/res/gtk/query-view.blp @@ -42,6 +42,7 @@ template $PsequelQueryView : Adw.Bin { Adw.ViewSwitcher tables_views_switcher { + styles ["big-icon"] height-request: 40; margin-start: 8; margin-end: 8; diff --git a/res/gtk/style.css b/res/gtk/style.css index 69acb06..49d9fce 100644 --- a/res/gtk/style.css +++ b/res/gtk/style.css @@ -1,3 +1,7 @@ .text-bold { font-size: 105%; +} + +.big-icon image { + -gtk-icon-size: 20px; } \ No newline at end of file