From b312de056d7b84120b5452e5adf299d8da2d6140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20De=20los=20Santos?= Date: Wed, 20 Mar 2024 22:50:51 +0100 Subject: [PATCH 1/3] feat: initial support for other schemas tries to add some support for completion when working with other schemas. join snippets are not supported yet as foreign key information gathered for the other schemas is not updated in the cache as it is done with the column description this partially solves issue https://github.com/sqls-server/sqls/issues/99 --- internal/completer/candidates.go | 51 +++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/internal/completer/candidates.go b/internal/completer/candidates.go index c512a7cc..fe47ade8 100644 --- a/internal/completer/candidates.go +++ b/internal/completer/candidates.go @@ -68,7 +68,17 @@ func (c *Completer) columnCandidates(targetTables []*parseutil.TableInfo, parent if table.Name != parent.Name && table.Alias != parent.Name { continue } - columns, ok := c.DBCache.ColumnDescs(table.Name) + + var columns []*database.ColumnDesc + var ok bool + + if table.DatabaseSchema != "" { + columns, ok = c.DBCache.ColumnDatabase(table.DatabaseSchema, table.Name) + + } else { + columns, ok = c.DBCache.ColumnDescs(table.Name) + } + if !ok { continue } @@ -113,7 +123,13 @@ func (c *Completer) ReferencedTableCandidates(targetTables []*parseutil.TableInf for _, targetTable := range targetTables { includeTables := []*parseutil.TableInfo{} - for _, table := range c.DBCache.SortedTables() { + var schemaTables []string + if (targetTable.DatabaseSchema != "") { + schemaTables, _ = c.DBCache.SortedTablesByDBName(targetTable.DatabaseSchema) + } else { + schemaTables = c.DBCache.SortedTables() + } + for _, table := range schemaTables { if table == targetTable.Name { includeTables = append(includeTables, targetTable) } @@ -146,7 +162,7 @@ func (c *Completer) TableCandidates(parent *completionParent, targetTables []*pa case ParentTypeSchema: tables, ok := c.DBCache.SortedTablesByDBName(parent.Name) if ok { - candidates = append(candidates, generateTableCandidates(tables, c.DBCache)...) + candidates = append(candidates, generateTableCandidatesBySchema(parent.Name, tables, c.DBCache)...) } case ParentTypeTable: // pass @@ -339,6 +355,26 @@ func generateTableCandidates(tables []string, dbCache *database.DBCache) []lsp.C return candidates } +func generateTableCandidatesBySchema(schemaName string, tables []string, dbCache *database.DBCache) []lsp.CompletionItem { + candidates := []lsp.CompletionItem{} + for _, tableName := range tables { + candidate := lsp.CompletionItem{ + Label: tableName, + Kind: lsp.ClassCompletion, + Detail: "table", + } + cols, ok := dbCache.ColumnDatabase(schemaName, tableName) + if ok { + candidate.Documentation = lsp.MarkupContent{ + Kind: lsp.Markdown, + Value: database.TableDoc(tableName, cols), + } + } + candidates = append(candidates, candidate) + } + return candidates +} + func generateTableCandidatesByInfos(tables []*parseutil.TableInfo, dbCache *database.DBCache) []lsp.CompletionItem { candidates := []lsp.CompletionItem{} for _, table := range tables { @@ -353,7 +389,14 @@ func generateTableCandidatesByInfos(tables []*parseutil.TableInfo, dbCache *data Kind: lsp.ClassCompletion, Detail: detail, } - cols, ok := dbCache.ColumnDescs(table.Name) + var cols []*database.ColumnDesc + var ok bool + + if table.DatabaseSchema != "" { + cols, ok = dbCache.ColumnDatabase(table.DatabaseSchema, table.Name) + } else { + cols, ok = dbCache.ColumnDescs(table.Name) + } if ok { candidate.Documentation = lsp.MarkupContent{ Kind: lsp.Markdown, From ebfa13037b22b9f1c516bb35ac5509006952527f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20De=20los=20Santos?= Date: Wed, 20 Mar 2024 22:54:16 +0100 Subject: [PATCH 2/3] feat: add microsecond precision to log --- main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/main.go b/main.go index 8acf1cfe..be3a2d23 100644 --- a/main.go +++ b/main.go @@ -109,6 +109,7 @@ func serve(c *cli.Context) error { logWriter = io.MultiWriter(os.Stderr) } log.SetOutput(logWriter) + log.SetFlags(log.LstdFlags | log.Lmicroseconds) // Initialize language server server := handler.NewServer() From 32e420da920f4cddbf83b2f82226707b3c2d313c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20De=20los=20Santos?= Date: Wed, 20 Mar 2024 22:57:58 +0100 Subject: [PATCH 3/3] perf: postgres schema query optimization querying information schema views is slow, and more so when joining multiple of these relations some of these queries have been changed to query the catalog tables instead --- internal/database/postgresql.go | 211 ++++++++++++++++++-------------- 1 file changed, 119 insertions(+), 92 deletions(-) diff --git a/internal/database/postgresql.go b/internal/database/postgresql.go index 2d08c2fb..158148c1 100644 --- a/internal/database/postgresql.go +++ b/internal/database/postgresql.go @@ -221,43 +221,60 @@ func (db *PostgreSQLDBRepository) Tables(ctx context.Context) ([]string, error) } func (db *PostgreSQLDBRepository) DescribeDatabaseTable(ctx context.Context) ([]*ColumnDesc, error) { + log.Println("repository: describing all database tables") rows, err := db.Conn.QueryContext( ctx, ` - SELECT - c.table_schema, - c.table_name, - c.column_name, - c.data_type, - c.is_nullable, - CASE t.constraint_type - WHEN 'PRIMARY KEY' THEN 'YES' - ELSE 'NO' - END, - c.column_default, - '' - FROM - information_schema.columns c - LEFT JOIN ( - SELECT - ccu.table_schema as table_schema, - ccu.table_name as table_name, - ccu.column_name as column_name, - tc.constraint_type as constraint_type - FROM information_schema.constraint_column_usage ccu - LEFT JOIN information_schema.table_constraints tc ON - tc.table_schema = ccu.table_schema - AND tc.table_name = ccu.table_name - AND tc.constraint_name = ccu.constraint_name - WHERE - tc.constraint_type = 'PRIMARY KEY' - ) as t - ON c.table_schema = t.table_schema - AND c.table_name = t.table_name - AND c.column_name = t.column_name - ORDER BY - c.table_name, - c.ordinal_position + SELECT DISTINCT ON (table_name, a.attnum) + c1.relnamespace::regnamespace AS table_schema, c1.relname AS table_name, a.attname AS column_name, CASE WHEN + t.typtype = 'd'::"char" THEN + CASE WHEN bt.typelem <> 0::oid + AND bt.typlen = '-1'::integer THEN + 'ARRAY'::text + WHEN nbt.nspname = 'pg_catalog'::name THEN + format_type(t.typbasetype, NULL::integer) + ELSE + 'USER-DEFINED'::text + END + ELSE + CASE WHEN t.typelem <> 0::oid + AND t.typlen = '-1'::integer THEN + 'ARRAY'::text + WHEN nt.nspname = 'pg_catalog'::name THEN + format_type(a.atttypid, NULL::integer) + ELSE + 'USER-DEFINED'::text + END + END::information_schema.character_data AS data_type, CASE WHEN a.attnotnull + OR t.typtype = 'd'::"char" + AND t.typnotnull THEN + 'NO'::text + ELSE + 'YES'::text + END::information_schema.yes_or_no AS is_nullable, CASE WHEN conn.contype = 'p' THEN + 'YES' + ELSE + 'NO' + END AS is_primary_key, CASE WHEN a.attgenerated = ''::"char" THEN + pg_get_expr(ad.adbin, ad.adrelid) + ELSE + NULL::text + END::information_schema.character_data AS column_default, + '' + FROM pg_catalog.pg_class c1 + JOIN pg_catalog.pg_attribute a ON a.attrelid = c1.oid + LEFT JOIN (pg_type t + JOIN pg_namespace nt ON t.typnamespace = nt.oid) ON a.atttypid = t.oid + LEFT JOIN (pg_type bt + JOIN pg_namespace nbt ON bt.typnamespace = nbt.oid) ON t.typtype = 'd'::char + AND t.typbasetype = bt.oid + LEFT JOIN pg_catalog.pg_constraint conn ON conn.conrelid = c1.oid + AND a.attnum = ANY (conn.conkey) + AND conn.contype = 'p' + LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid + AND a.attnum = ad.adnum + WHERE a.attnum > 0 + ORDER BY table_name, a.attnum `) if err != nil { log.Fatal(err) @@ -285,47 +302,63 @@ func (db *PostgreSQLDBRepository) DescribeDatabaseTable(ctx context.Context) ([] } func (db *PostgreSQLDBRepository) DescribeDatabaseTableBySchema(ctx context.Context, schemaName string) ([]*ColumnDesc, error) { + log.Printf("repository: describing database tables in schema %s", schemaName) + rows, err := db.Conn.QueryContext( ctx, ` - SELECT - c.table_schema, - c.table_name, - c.column_name, - c.data_type, - c.is_nullable, - CASE t.constraint_type - WHEN 'PRIMARY KEY' THEN 'YES' - ELSE 'NO' - END, - c.column_default, - '' - FROM - information_schema.columns c - LEFT JOIN ( - SELECT - ccu.table_schema as table_schema, - ccu.table_name as table_name, - ccu.column_name as column_name, - tc.constraint_type as constraint_type - FROM information_schema.constraint_column_usage ccu - LEFT JOIN information_schema.table_constraints tc ON - tc.table_schema = ccu.table_schema - AND tc.table_name = ccu.table_name - AND tc.constraint_name = ccu.constraint_name - WHERE - ccu.table_schema = $1 - AND tc.constraint_type = 'PRIMARY KEY' - ) as t - ON c.table_schema = t.table_schema - AND c.table_name = t.table_name - AND c.column_name = t.column_name - WHERE - c.table_schema = $2 - ORDER BY - c.table_name, - c.ordinal_position - `, schemaName, schemaName) + SELECT DISTINCT ON (table_name, a.attnum) + c1.relnamespace::regnamespace AS table_schema, c1.relname AS table_name, a.attname AS column_name, CASE WHEN + t.typtype = 'd'::"char" THEN + CASE WHEN bt.typelem <> 0::oid + AND bt.typlen = '-1'::integer THEN + 'ARRAY'::text + WHEN nbt.nspname = 'pg_catalog'::name THEN + format_type(t.typbasetype, NULL::integer) + ELSE + 'USER-DEFINED'::text + END + ELSE + CASE WHEN t.typelem <> 0::oid + AND t.typlen = '-1'::integer THEN + 'ARRAY'::text + WHEN nt.nspname = 'pg_catalog'::name THEN + format_type(a.atttypid, NULL::integer) + ELSE + 'USER-DEFINED'::text + END + END::information_schema.character_data AS data_type, CASE WHEN a.attnotnull + OR t.typtype = 'd'::"char" + AND t.typnotnull THEN + 'NO'::text + ELSE + 'YES'::text + END::information_schema.yes_or_no AS is_nullable, CASE WHEN conn.contype = 'p' THEN + 'YES' + ELSE + 'NO' + END AS is_primary_key, CASE WHEN a.attgenerated = ''::"char" THEN + pg_get_expr(ad.adbin, ad.adrelid) + ELSE + NULL::text + END::information_schema.character_data AS column_default, + '' + FROM pg_catalog.pg_class c1 + JOIN pg_catalog.pg_attribute a ON a.attrelid = c1.oid + LEFT JOIN (pg_type t + JOIN pg_namespace nt ON t.typnamespace = nt.oid) ON a.atttypid = t.oid + LEFT JOIN (pg_type bt + JOIN pg_namespace nbt ON bt.typnamespace = nbt.oid) ON t.typtype = 'd'::char + AND t.typbasetype = bt.oid + LEFT JOIN pg_catalog.pg_constraint conn ON conn.conrelid = c1.oid + AND a.attnum = ANY (conn.conkey) + AND conn.contype = 'p' + LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid + AND a.attnum = ad.adnum + WHERE c1.relnamespace = $1::regnamespace::oid + AND a.attnum > 0 + ORDER BY table_name, a.attnum + `, schemaName) if err != nil { log.Fatal(err) } @@ -352,29 +385,23 @@ func (db *PostgreSQLDBRepository) DescribeDatabaseTableBySchema(ctx context.Cont } func (db *PostgreSQLDBRepository) DescribeForeignKeysBySchema(ctx context.Context, schemaName string) ([]*ForeignKey, error) { + log.Printf("repository: describing foreign keys in schema %s", schemaName) + rows, err := db.Conn.QueryContext( ctx, ` - select kcu.CONSTRAINT_NAME, - kcu.TABLE_NAME, - kcu.COLUMN_NAME, - rel_kcu.TABLE_NAME, - rel_kcu.COLUMN_NAME - from INFORMATION_SCHEMA.TABLE_CONSTRAINTS tco - join INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu - on tco.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA - and tco.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME - join INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rco - on tco.CONSTRAINT_SCHEMA = rco.CONSTRAINT_SCHEMA - and tco.CONSTRAINT_NAME = rco.CONSTRAINT_NAME - join INFORMATION_SCHEMA.KEY_COLUMN_USAGE rel_kcu - on rco.UNIQUE_CONSTRAINT_SCHEMA = rel_kcu.CONSTRAINT_SCHEMA - and rco.UNIQUE_CONSTRAINT_NAME = rel_kcu.CONSTRAINT_NAME - and kcu.ORDINAL_POSITION = rel_kcu.ORDINAL_POSITION - where tco.CONSTRAINT_TYPE = 'FOREIGN KEY' - and tco.CONSTRAINT_SCHEMA = $1 - order by kcu.CONSTRAINT_NAME, - kcu.ORDINAL_POSITION + SELECT fk.conname AS constraint_name, c1.relname AS table_name, a1.attname AS column_name, c2.relname AS + foreign_table_name, a2.attname AS foreign_column_name + FROM pg_catalog.pg_constraint fk + JOIN pg_catalog.pg_class c1 ON c1.oid = fk.conrelid + JOIN pg_catalog.pg_attribute a1 ON a1.attrelid = c1.oid + AND a1.attnum = ANY (fk.conkey) + JOIN pg_catalog.pg_class c2 ON c2.oid = fk.confrelid + JOIN pg_catalog.pg_attribute a2 ON a2.attrelid = c2.oid + AND a2.attnum = ANY (fk.confkey) + WHERE fk.contype = 'f' + AND fk.connamespace = $1::regnamespace::oid + ORDER BY constraint_name, a1.attnum; `, schemaName) if err != nil { log.Fatal(err)