From eb9ab2a7ab96b41def229f97d73a0cdcb55572bd Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Tue, 30 Jul 2024 20:15:19 +0800 Subject: [PATCH] chore: update table def (#28278) * chore: update table def * fix double not null * update test --- .../grid/components/footer/Footer.tsx | 23 +- .../database/database-table-definition.ts | 395 ++++++++++++------ .../tests/snapshot/spec/table-editor.spec.ts | 2 +- 3 files changed, 284 insertions(+), 136 deletions(-) diff --git a/apps/studio/components/grid/components/footer/Footer.tsx b/apps/studio/components/grid/components/footer/Footer.tsx index 57cb0019c9e..f3a4881622a 100644 --- a/apps/studio/components/grid/components/footer/Footer.tsx +++ b/apps/studio/components/grid/components/footer/Footer.tsx @@ -1,6 +1,8 @@ import { useParams } from 'common' import { GridFooter } from 'components/ui/GridFooter' import TwoOptionToggle from 'components/ui/TwoOptionToggle' +import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants' +import useEntityType from 'hooks/misc/useEntityType' import useTable from 'hooks/misc/useTable' import { useUrlState } from 'hooks/ui/useUrlState' import RefreshButton from '../header/RefreshButton' @@ -14,6 +16,7 @@ const Footer = ({ isRefetching }: FooterProps) => { const { id: _id } = useParams() const id = _id ? Number(_id) : undefined const { data: selectedTable } = useTable(id) + const entityType = useEntityType(selectedTable?.id) const [{ view: selectedView = 'data' }, setUrlState] = useUrlState() @@ -25,6 +28,10 @@ const Footer = ({ isRefetching }: FooterProps) => { } } + const isViewSelected = + entityType?.type === ENTITY_TYPE.VIEW || entityType?.type === ENTITY_TYPE.MATERIALIZED_VIEW + const isTableSelected = entityType?.type === ENTITY_TYPE.TABLE + return ( {selectedView === 'data' && } @@ -34,13 +41,15 @@ const Footer = ({ isRefetching }: FooterProps) => { )} - + {(isViewSelected || isTableSelected) && ( + + )} ) diff --git a/apps/studio/data/database/database-table-definition.ts b/apps/studio/data/database/database-table-definition.ts index bc968581287..0e09cb40507 100644 --- a/apps/studio/data/database/database-table-definition.ts +++ b/apps/studio/data/database/database-table-definition.ts @@ -2,38 +2,66 @@ import minify from 'pg-minify' // Derived from https://github.com/MichaelDBA/pg_get_tabledef // NOTE: when updating, \n must be replaced with \\n in the SQL below +// public.x is replaced with pg_temp.x + +// REMOVE (as it breaks the encoding for some reason): +// SELECT REPLACE(REPLACE(setting, '"$user"', '$user'), '$user', '"$user"') INTO search_path_old +// FROM pg_settings +// WHERE name = 'search_path'; export const CREATE_PG_GET_TABLEDEF_SQL = minify( /* SQL */ ` DROP TYPE IF EXISTS pg_temp.tabledefs CASCADE; - CREATE TYPE pg_temp.tabledefs AS ENUM ('PKEY_INTERNAL','PKEY_EXTERNAL','FKEYS_INTERNAL', 'FKEYS_EXTERNAL', 'FKEYS_COMMENTED', 'FKEYS_NONE', 'INCLUDE_TRIGGERS', 'NO_TRIGGERS'); + CREATE TYPE pg_temp.tabledefs AS ENUM ('PKEY_INTERNAL','PKEY_EXTERNAL','FKEYS_INTERNAL', 'FKEYS_EXTERNAL', 'COMMENTS', 'FKEYS_NONE', 'INCLUDE_TRIGGERS', 'NO_TRIGGERS'); + -- SELECT * FROM pg_temp.pg_get_coldef('sample','orders','id'); -- DROP FUNCTION pg_temp.pg_get_coldef(text,text,text,boolean); CREATE OR REPLACE FUNCTION pg_temp.pg_get_coldef( in_schema text, - in_table text, + in_table text, in_column text, - oldway boolean default False + oldway boolean default False ) RETURNS text LANGUAGE plpgsql VOLATILE AS $$ DECLARE - coldef text; + v_coldef text; + v_dt1 text; + v_dt2 text; + v_dt3 text; + v_nullable boolean; + v_position int; + v_identity text; + v_generated text; + v_hasdflt boolean; + v_dfltexpr text; + BEGIN IF oldway THEN - SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) INTO coldef FROM pg_namespace n, pg_class c, pg_attribute a, pg_type t + SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) INTO v_coldef FROM pg_namespace n, pg_class c, pg_attribute a, pg_type t WHERE n.nspname = in_schema AND n.oid = c.relnamespace AND c.relname = in_table AND a.attname = in_column and a.attnum > 0 AND a.attrelid = c.oid AND a.atttypid = t.oid ORDER BY a.attnum; + -- RAISE NOTICE 'DEBUG: oldway=%',v_coldef; ELSE -- a.attrelid::regclass::text, a.attname SELECT CASE WHEN a.atttypid = ANY ('{int,int8,int2}'::regtype[]) AND EXISTS (SELECT FROM pg_attrdef ad WHERE ad.adrelid = a.attrelid AND ad.adnum = a.attnum AND pg_get_expr(ad.adbin, ad.adrelid) = 'nextval(''' || (pg_get_serial_sequence (a.attrelid::regclass::text, a.attname))::regclass || '''::regclass)') THEN CASE a.atttypid WHEN 'int'::regtype THEN 'serial' WHEN 'int8'::regtype THEN 'bigserial' WHEN 'int2'::regtype THEN 'smallserial' END ELSE format_type(a.atttypid, a.atttypmod) END AS data_type - INTO coldef FROM pg_namespace n, pg_class c, pg_attribute a, pg_type t + INTO v_coldef FROM pg_namespace n, pg_class c, pg_attribute a, pg_type t WHERE n.nspname = in_schema AND n.oid = c.relnamespace AND c.relname = in_table AND a.attname = in_column and a.attnum > 0 AND a.attrelid = c.oid AND a.atttypid = t.oid ORDER BY a.attnum; + -- RAISE NOTICE 'DEBUG: newway=%',v_coldef; + + -- Issue#24: not implemented yet + -- might replace with this below to do more detailed parsing... + -- SELECT a.atttypid::regtype AS dt1, format_type(a.atttypid, a.atttypmod) as dt2, t.typname as dt3, CASE WHEN not(a.attnotnull) THEN True ELSE False END AS nullable, + -- a.attnum, a.attidentity, a.attgenerated, a.atthasdef, pg_get_expr(ad.adbin, ad.adrelid) dfltexpr + -- INTO v_dt1, v_dt2, v_dt3, v_nullable, v_position, v_identity, v_generated, v_hasdflt, v_dfltexpr + -- FROM pg_attribute a JOIN pg_class c ON (a.attrelid = c.oid) JOIN pg_type t ON (a.atttypid = t.oid) LEFT JOIN pg_attrdef ad ON (a.attrelid = ad.adrelid AND a.attnum = ad.adnum) + -- WHERE c.relkind in ('r','p') AND a.attnum > 0 AND NOT a.attisdropped AND c.relnamespace::regnamespace::text = in_schema AND c.relname = in_table AND a.attname = in_column; + -- RAISE NOTICE 'schema=% table=% column=% dt1=% dt2=% dt3=% nullable=% pos=% identity=% generated=% HasDefault=% DeftExpr=%', in_schema, in_table, in_column, v_dt1,v_dt2,v_dt3,v_nullable,v_position,v_identity,v_generated,v_hasdflt,v_dfltexpr; END IF; - RETURN coldef; + RETURN v_coldef; END; $$; @@ -57,11 +85,12 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify( v_constraintrec record; v_trigrec record; v_indexrec record; - v_primary boolean := False; + v_rec record; v_constraint_name text; v_constraint_def text; v_pkey_def text := ''; - v_fkey_defs text; + v_fkey_def text := ''; + v_fkey_defs text := ''; v_trigger text := ''; v_partition_key text := ''; v_partbound text; @@ -69,6 +98,7 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify( v_parent_schema text; v_persist text; v_temp text := ''; + v_temp2 text; v_relopts text; v_tablespace text; v_pgversion int; @@ -82,17 +112,22 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify( bVerbose boolean := False; v_cnt1 integer; v_cnt2 integer; + search_path_old text := ''; + search_path_new text := ''; + v_partial boolean; + v_pos integer; -- assume defaults for ENUMs at the getgo pkcnt int := 0; fkcnt int := 0; trigcnt int := 0; - pktype tabledefs := 'PKEY_INTERNAL'; - fktype tabledefs := 'FKEYS_INTERNAL'; - trigtype tabledefs := 'NO_TRIGGERS'; + cmtcnt int := 0; + pktype pg_temp.tabledefs := 'PKEY_INTERNAL'; + fktype pg_temp.tabledefs := 'FKEYS_INTERNAL'; + trigtype pg_temp.tabledefs := 'NO_TRIGGERS'; arglen integer; vargs text; - avarg tabledefs; + avarg pg_temp.tabledefs; -- exception variables v_ret text; @@ -120,8 +155,8 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify( -- select array_to_string($4, ',', '***') INTO vargs; IF bVerbose THEN RAISE NOTICE 'arguments=%', $4; END IF; FOREACH avarg IN ARRAY $4 LOOP - IF bVerbose THEN RAISE INFO 'arg=%', avarg; END IF; - IF avarg = 'FKEYS_INTERNAL' OR avarg = 'FKEYS_EXTERNAL' OR avarg = 'FKEYS_COMMENTED' THEN + IF bVerbose THEN RAISE NOTICE 'arg=%', avarg; END IF; + IF avarg = 'FKEYS_INTERNAL' OR avarg = 'FKEYS_EXTERNAL' OR avarg = 'FKEYS_NONE' THEN fkcnt = fkcnt + 1; fktype = avarg; ELSEIF avarg = 'INCLUDE_TRIGGERS' OR avarg = 'NO_TRIGGERS' THEN @@ -130,6 +165,9 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify( ELSEIF avarg = 'PKEY_EXTERNAL' THEN pkcnt = pkcnt + 1; pktype = avarg; + ELSEIF avarg = 'COMMENTS' THEN + cmtcnt = cmtcnt + 1; + END IF; END LOOP; IF fkcnt > 1 THEN @@ -141,12 +179,24 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify( ELSEIF pkcnt > 1 THEN RAISE WARNING 'Only one pkey option can be provided. You provided %', pkcnt; RETURN ''; + ELSEIF cmtcnt > 1 THEN + RAISE WARNING 'Only one comments option can be provided. You provided %', cmtcnt; + RETURN ''; + END IF; END IF; SELECT c.oid, (select setting from pg_settings where name = 'server_version_num') INTO v_table_oid, v_pgversion FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind in ('r','p') AND c.relname = in_table AND n.nspname = in_schema; - + + -- set search_path = public before we do anything to force explicit schema qualification but dont forget to set it back before exiting... + SELECT setting INTO search_path_old FROM pg_settings WHERE name = 'search_path'; + + -- RAISE NOTICE 'DEBUG tableddl: saving old search_path: ***%***', search_path_old; + EXECUTE 'SET search_path = "public"'; + SELECT setting INTO search_path_new FROM pg_settings WHERE name = 'search_path'; + -- RAISE NOTICE 'DEBUG tableddl: using new search path=***%***', search_path_new; + -- throw an error if table was not found IF (v_table_oid IS NULL) THEN RAISE EXCEPTION 'table does not exist'; @@ -206,7 +256,9 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify( WHERE t.table_schema=s.table_schema AND t.table_name=s.table_name AND t.table_schema = in_schema AND t.table_name = in_table AND t.table_type = 'BASE TABLE'); --Issue#19 put double-quotes around SQL keyword column names - SELECT COUNT(*) INTO v_cnt2 FROM pg_get_keywords() WHERE word = v_colrec.column_name AND catcode = 'R'; + -- Issue#121: fix keyword lookup for table name not column name that does not apply here + -- SELECT COUNT(*) INTO v_cnt2 FROM pg_get_keywords() WHERE word = v_colrec.column_name AND catcode = 'R'; + SELECT COUNT(*) INTO v_cnt2 FROM pg_get_keywords() WHERE word = in_table AND catcode = 'R'; IF bInheritance THEN -- inheritance-based @@ -235,7 +287,7 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify( -- Jump to constraints and index section to add the check constraints and indexes and perhaps FKeys END IF; END IF; - IF bVerbose THEN RAISE INFO '(1)tabledef so far: %', v_table_ddl; END IF; + IF bVerbose THEN RAISE NOTICE '(1)tabledef so far: %', v_table_ddl; END IF; IF NOT bPartition THEN -- see if this is unlogged or temporary table @@ -262,14 +314,15 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify( v_table_ddl := 'CREATE ' || v_temp || ' TABLE ' || in_schema || '.' || in_table || ' (' || E'\\n'; END IF; END IF; - -- RAISE INFO 'DEBUG2: tabledef so far: %', v_table_ddl; + -- RAISE NOTICE 'DEBUG2: tabledef so far: %', v_table_ddl; -- define all of the columns in the table unless we are in progress creating an inheritance-based child table IF NOT bPartition THEN FOR v_colrec IN - SELECT c.column_name, c.data_type, c.udt_name, c.udt_schema, c.character_maximum_length, c.is_nullable, c.column_default, c.numeric_precision, c.numeric_scale, c.is_identity, c.identity_generation + SELECT c.column_name, c.data_type, c.udt_name, c.udt_schema, c.character_maximum_length, c.is_nullable, c.column_default, c.numeric_precision, c.numeric_scale, c.is_identity, c.identity_generation, c.is_generated, c.generation_expression FROM information_schema.columns c WHERE (table_schema, table_name) = (in_schema, in_table) ORDER BY ordinal_position LOOP - IF bVerbose THEN RAISE INFO '(col loop) name=% type=% udt_name=% udt_schema=%', v_colrec.column_name, v_colrec.data_type, v_colrec.udt_name, v_colrec.udt_schema; END IF; + IF bVerbose THEN RAISE NOTICE '(col loop) name=% type=% udt_name=% default=% is_generated=% gen_expr=%', v_colrec.column_name, v_colrec.data_type, v_colrec.udt_name, v_colrec.column_default, v_colrec.is_generated, v_colrec.generation_expression; END IF; + -- v17 fix: handle case-sensitive for pg_get_serial_sequence that requires SQL Identifier handling -- SELECT CASE WHEN pg_get_serial_sequence(v_qualified, v_colrec.column_name) IS NOT NULL THEN True ELSE False END into bSerial; SELECT CASE WHEN pg_get_serial_sequence(quote_ident(in_schema) || '.' || quote_ident(in_table), v_colrec.column_name) IS NOT NULL THEN True ELSE False END into bSerial; @@ -279,8 +332,8 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify( SELECT pg_get_serial_sequence(quote_ident(in_schema) || '.' || quote_ident(in_table), v_colrec.column_name) into v_temp; IF v_temp IS NULL THEN v_temp = 'NA'; END IF; SELECT pg_temp.pg_get_coldef(in_schema, in_table,v_colrec.column_name) INTO v_diag1; - --RAISE NOTICE 'DEBUG table: % Column: % datatype: % Serial=% serialval=% coldef=%', v_qualified, v_colrec.column_name, v_colrec.data_type, bSerial, v_temp, v_diag1; - --RAISE NOTICE 'DEBUG tabledef: %', v_table_ddl; + RAISE NOTICE 'DEBUG table: % Column: % datatype: % Serial=% serialval=% coldef=%', v_qualified, v_colrec.column_name, v_colrec.data_type, bSerial, v_temp, v_diag1; + RAISE NOTICE 'DEBUG tabledef: %', v_table_ddl; END IF; --Issue#17 put double-quotes around case-sensitive column names @@ -295,32 +348,64 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify( ELSE v_table_ddl := v_table_ddl || ' ' || v_colrec.column_name || ' '; END IF; - - v_table_ddl := v_table_ddl || - CASE WHEN v_colrec.udt_name in ('geometry', 'box2d', 'box2df', 'box3d', 'geography', 'geometry_dump', 'gidx', 'spheroid', 'valid_detail') - THEN v_colrec.udt_name - WHEN v_colrec.data_type = 'USER-DEFINED' - THEN v_colrec.udt_schema || '.' || v_colrec.udt_name - WHEN v_colrec.data_type = 'ARRAY' - -- Issue#6 fix: handle arrays - THEN pg_temp.pg_get_coldef(in_schema, in_table,v_colrec.column_name) - -- v17 fix: handle case-sensitive for pg_get_serial_sequence that requires SQL Identifier handling - -- WHEN pg_get_serial_sequence(v_qualified, v_colrec.column_name) IS NOT NULL - WHEN pg_get_serial_sequence(quote_ident(in_schema) || '.' || quote_ident(in_table), v_colrec.column_name) IS NOT NULL - -- Issue#8 fix: handle serial. Note: NOT NULL is implied so no need to declare it explicitly - THEN pg_temp.pg_get_coldef(in_schema, in_table,v_colrec.column_name) - ELSE v_colrec.data_type END - || CASE WHEN v_colrec.is_identity = 'YES' THEN CASE WHEN v_colrec.identity_generation = 'ALWAYS' THEN ' GENERATED ALWAYS AS IDENTITY' ELSE ' GENERATED BY DEFAULT AS IDENTITY' END ELSE '' END - || CASE WHEN v_colrec.character_maximum_length IS NOT NULL THEN ('(' || v_colrec.character_maximum_length || ')') - WHEN v_colrec.numeric_precision > 0 AND v_colrec.numeric_scale > 0 THEN '(' || v_colrec.numeric_precision || ',' || v_colrec.numeric_scale || ')' - ELSE '' END || ' ' - || CASE WHEN bSerial THEN '' ELSE CASE WHEN v_colrec.is_nullable = 'NO' THEN 'NOT NULL' ELSE 'NULL' END END - || CASE WHEN bSerial THEN '' ELSE CASE WHEN v_colrec.column_default IS NOT null THEN (' DEFAULT ' || v_colrec.column_default) ELSE '' END END - || ',' || E'\\n'; + + -- Issue#23: Handle autogenerated columns and rewrite as a simpler IF THEN ELSE branch instead of a much more complex embedded CASE STATEMENT + IF v_colrec.is_generated = 'ALWAYS' and v_colrec.generation_expression IS NOT NULL THEN + -- searchable tsvector GENERATED ALWAYS AS (to_tsvector('simple'::regconfig, COALESCE(translate(email, '@.-'::citext, ' '::text), ''::text)) ) STORED + v_temp = v_colrec.data_type || ' GENERATED ALWAYS AS (' || v_colrec.generation_expression || ') STORED '; + ELSEIF v_colrec.udt_name in ('geometry', 'box2d', 'box2df', 'box3d', 'geography', 'geometry_dump', 'gidx', 'spheroid', 'valid_detail') THEN + v_temp = v_colrec.udt_name; + ELSEIF v_colrec.data_type = 'USER-DEFINED' THEN + v_temp = v_colrec.udt_schema || '.' || v_colrec.udt_name; + ELSEIF v_colrec.data_type = 'ARRAY' THEN + -- Issue#6 fix: handle arrays + v_temp = pg_temp.pg_get_coldef(in_schema, in_table,v_colrec.column_name); + -- v17 fix: handle case-sensitive for pg_get_serial_sequence that requires SQL Identifier handling + -- WHEN pg_get_serial_sequence(v_qualified, v_colrec.column_name) IS NOT NULL + ELSEIF pg_get_serial_sequence(quote_ident(in_schema) || '.' || quote_ident(in_table), v_colrec.column_name) IS NOT NULL THEN + -- Issue#8 fix: handle serial. Note: NOT NULL is implied so no need to declare it explicitly + v_temp = pg_temp.pg_get_coldef(in_schema, in_table,v_colrec.column_name); + ELSE + v_temp = v_colrec.data_type; + END IF; + -- RAISE NOTICE 'column def1=%', v_temp; + + -- handle IDENTITY columns + IF v_colrec.is_identity = 'YES' THEN + IF v_colrec.identity_generation = 'ALWAYS' THEN + v_temp = v_temp || ' GENERATED ALWAYS AS IDENTITY'; + ELSE + v_temp = v_temp || ' GENERATED BY DEFAULT AS IDENTITY'; + END IF; + ELSEIF v_colrec.character_maximum_length IS NOT NULL THEN + v_temp = v_temp || ('(' || v_colrec.character_maximum_length || ')'); + ELSEIF v_colrec.numeric_precision > 0 AND v_colrec.numeric_scale > 0 THEN + v_temp = v_temp || '(' || v_colrec.numeric_precision || ',' || v_colrec.numeric_scale || ')'; + END IF; + + -- Handle NULL/NOT NULL + IF bSerial THEN + v_temp = v_temp || ' NOT NULL'; + ELSEIF v_colrec.is_nullable = 'NO' THEN + v_temp = v_temp || ' NOT NULL'; + ELSEIF v_colrec.is_nullable = 'YES' THEN + v_temp = v_temp || ' NULL'; + END IF; + + -- Handle defaults + IF v_colrec.column_default IS NOT null AND NOT bSerial THEN + -- RAISE NOTICE 'Setting default for column, %', v_colrec.column_name; + v_temp = v_temp || (' DEFAULT ' || v_colrec.column_default); + END IF; + v_temp = v_temp || ',' || E'\\n'; + -- RAISE NOTICE 'column def2=%', v_temp; + v_table_ddl := v_table_ddl || v_temp; + -- RAISE NOTICE 'tabledef=%', v_table_ddl; + END LOOP; END IF; - IF bVerbose THEN RAISE INFO '(2)tabledef so far: %', v_table_ddl; END IF; - + IF bVerbose THEN RAISE NOTICE '(2)tabledef so far: %', v_table_ddl; END IF; + -- define all the constraints: conparentid does not exist pre PGv11 IF v_pgversion < 110000 THEN FOR v_constraintrec IN @@ -335,42 +420,61 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify( pg_get_constraintdef(con.oid) as constraint_definition FROM pg_catalog.pg_constraint con JOIN pg_catalog.pg_class rel ON rel.oid = con.conrelid JOIN pg_catalog.pg_namespace nsp ON nsp.oid = connamespace WHERE nsp.nspname = in_schema AND rel.relname = in_table ORDER BY type_rank - LOOP + LOOP + v_constraint_name := v_constraintrec.constraint_name; + v_constraint_def := v_constraintrec.constraint_definition; IF v_constraintrec.type_rank = 1 THEN - v_primary := True; - IF pkcnt = 0 THEN + IF pkcnt = 0 OR pktype = 'PKEY_INTERNAL' THEN + -- internal def v_constraint_name := v_constraintrec.constraint_name; v_constraint_def := v_constraintrec.constraint_definition; + v_table_ddl := v_table_ddl || ' ' -- note: two char spacer to start, to indent the column + || 'CONSTRAINT' || ' ' + || v_constraint_name || ' ' + || v_constraint_def + || ',' || E'\\n'; ELSE -- Issue#16 handle external PG def - v_constraint_name := v_constraintrec.constraint_name; SELECT 'ALTER TABLE ONLY ' || in_schema || '.' || c.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_pkey_def - FROM pg_catalog.pg_constraint r, pg_class c, pg_namespace n where r.conrelid = c.oid and r.contype = 'p' and n.oid = r.connamespace and n.nspname = in_schema AND c.relname = in_table; + FROM pg_catalog.pg_constraint r, pg_class c, pg_namespace n where r.conrelid = c.oid and r.contype = 'p' and n.oid = r.connamespace and n.nspname = in_schema AND c.relname = in_table and r.conname = v_constraint_name; END IF; IF bPartition THEN continue; END IF; + ELSIF v_constraintrec.type_rank = 3 THEN + -- handle foreign key constraints + --Issue#22 fix: added FKEY_NONE check + IF fktype = 'FKEYS_NONE' THEN + -- skip + continue; + ELSIF fkcnt = 0 OR fktype = 'FKEYS_INTERNAL' THEN + -- internal def + v_table_ddl := v_table_ddl || ' ' -- note: two char spacer to start, to indent the column + || 'CONSTRAINT' || ' ' + || v_constraint_name || ' ' + || v_constraint_def + || ',' || E'\\n'; + ELSE + -- external def + SELECT 'ALTER TABLE ONLY ' || n.nspname || '.' || c2.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_fkey_def + FROM pg_constraint r, pg_class c1, pg_namespace n, pg_class c2 where r.conrelid = c1.oid and r.contype = 'f' and n.nspname = in_schema and n.oid = r.connamespace and r.conrelid = c2.oid and c2.relname = in_table; + v_fkey_defs = v_fkey_defs || v_fkey_def || E'\\n'; + END IF; ELSE - v_constraint_name := v_constraintrec.constraint_name; - v_constraint_def := v_constraintrec.constraint_definition; + -- handle all other constraints besides PKEY and FKEYS as internal defs by default + v_table_ddl := v_table_ddl || ' ' -- note: two char spacer to start, to indent the column + || 'CONSTRAINT' || ' ' + || v_constraint_name || ' ' + || v_constraint_def + || ',' || E'\\n'; END IF; - if bVerbose THEN RAISE INFO 'DEBUG4: constraint name=% constraint_def=%', v_constraint_name,v_constraint_def; END IF; + if bVerbose THEN RAISE NOTICE 'DEBUG4: constraint name=% constraint_def=%', v_constraint_name,v_constraint_def; END IF; constraintarr := constraintarr || v_constraintrec.constraint_name:: text; - IF fktype <> 'FKEYS_INTERNAL' AND v_constraintrec.constraint_type = 'f' THEN - continue; - END IF; - - IF pkcnt = 0 THEN - v_table_ddl := v_table_ddl || ' ' -- note: two char spacer to start, to indent the column - || 'CONSTRAINT' || ' ' - || v_constraint_name || ' ' - || v_constraint_def - || ',' || E'\\n'; - END IF; END LOOP; - ELSE + -- handle PG versions 11 and up + -- Issue#20: Fix logic for external PKEY and FKEYS FOR v_constraintrec IN SELECT con.conname as constraint_name, con.contype as constraint_type, CASE @@ -386,51 +490,73 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify( --Issue#13 added this condition: AND con.conparentid = 0 ORDER BY type_rank - LOOP + LOOP + v_constraint_name := v_constraintrec.constraint_name; + v_constraint_def := v_constraintrec.constraint_definition; IF v_constraintrec.type_rank = 1 THEN - v_primary := True; - IF pkcnt = 0 THEN + IF pkcnt = 0 OR pktype = 'PKEY_INTERNAL' THEN + -- internal def v_constraint_name := v_constraintrec.constraint_name; v_constraint_def := v_constraintrec.constraint_definition; + v_table_ddl := v_table_ddl || ' ' -- note: two char spacer to start, to indent the column + || 'CONSTRAINT' || ' ' + || v_constraint_name || ' ' + || v_constraint_def + || ',' || E'\\n'; ELSE -- Issue#16 handle external PG def - v_constraint_name := v_constraintrec.constraint_name; SELECT 'ALTER TABLE ONLY ' || in_schema || '.' || c.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_pkey_def FROM pg_catalog.pg_constraint r, pg_class c, pg_namespace n where r.conrelid = c.oid and r.contype = 'p' and n.oid = r.connamespace and n.nspname = in_schema AND c.relname = in_table; END IF; IF bPartition THEN continue; - END IF; + END IF; + ELSIF v_constraintrec.type_rank = 3 THEN + -- handle foreign key constraints + --Issue#22 fix: added FKEY_NONE check + IF fktype = 'FKEYS_NONE' THEN + -- skip + continue; + ELSIF fkcnt = 0 OR fktype = 'FKEYS_INTERNAL' THEN + -- internal def + v_table_ddl := v_table_ddl || ' ' -- note: two char spacer to start, to indent the column + || 'CONSTRAINT' || ' ' + || v_constraint_name || ' ' + || v_constraint_def + || ',' || E'\\n'; + ELSE + -- external def + SELECT 'ALTER TABLE ONLY ' || n.nspname || '.' || c2.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_fkey_def + FROM pg_constraint r, pg_class c1, pg_namespace n, pg_class c2 where r.conrelid = c1.oid and r.contype = 'f' and n.nspname = in_schema and n.oid = r.connamespace and r.conrelid = c2.oid and c2.relname = in_table and + r.conname = v_constraint_name and r.conparentid = 0; + v_fkey_defs = v_fkey_defs || v_fkey_def || E'\\n'; + END IF; ELSE - v_constraint_name := v_constraintrec.constraint_name; - v_constraint_def := v_constraintrec.constraint_definition; + -- handle all other constraints besides PKEY and FKEYS as internal defs by default + v_table_ddl := v_table_ddl || ' ' -- note: two char spacer to start, to indent the column + || 'CONSTRAINT' || ' ' + || v_constraint_name || ' ' + || v_constraint_def + || ',' || E'\\n'; END IF; - -- SELECT 'ALTER TABLE ONLY ' || c.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' as pkeyddl FROM pg_catalog.pg_constraint r, pg_class c, pg_namespace n where r.conrelid = c.oid and r.contype = 'p' and n.oid = r.connamespace and n.nspname = 'sample' AND c.relname = 'extensions_table'; - if bVerbose THEN RAISE INFO 'DEBUG4: constraint name=% constraint_def=%', v_constraint_name,v_constraint_def; END IF; + if bVerbose THEN RAISE NOTICE 'DEBUG4: constraint name=% constraint_def=%', v_constraint_name,v_constraint_def; END IF; constraintarr := constraintarr || v_constraintrec.constraint_name:: text; - IF fktype <> 'FKEYS_INTERNAL' AND v_constraintrec.constraint_type = 'f' THEN - continue; - END IF; - - IF pkcnt = 0 THEN - v_table_ddl := v_table_ddl || ' ' -- note: two char spacer to start, to indent the column - || 'CONSTRAINT' || ' ' - || v_constraint_name || ' ' - || v_constraint_def - || ',' || E'\\n'; - END IF; END LOOP; END IF; - IF bVerbose THEN RAISE INFO '(3)tabledef so far: %', v_table_ddl; END IF; - -- drop the last comma before ending the create statement - v_table_ddl = substr(v_table_ddl, 0, length(v_table_ddl) - 1) || E'\\n'; + -- drop the last comma before ending the create statement, which should be right before the carriage return character + -- Issue#24: make sure the comma is there before removing it + select substring(v_table_ddl, length(v_table_ddl) - 1, 1) INTO v_temp; + IF v_temp = ',' THEN + v_table_ddl = substr(v_table_ddl, 0, length(v_table_ddl) - 1) || E'\\n'; + END IF; + IF bVerbose THEN RAISE NOTICE '(3)tabledef so far: %', trim(v_table_ddl); END IF; -- --------------------------------------------------------------------------- -- at this point we have everything up to the last table-enclosing parenthesis -- --------------------------------------------------------------------------- - IF bVerbose THEN RAISE INFO '(4)tabledef so far: %', v_table_ddl; END IF; + IF bVerbose THEN RAISE NOTICE '(4)tabledef so far: %', v_table_ddl; END IF; -- See if this is an inheritance-based child table and finish up the table create. IF bPartition and bInheritance THEN @@ -458,7 +584,7 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify( END IF; END IF; - IF bVerbose THEN RAISE INFO '(5)tabledef so far: %', v_table_ddl; END IF; + IF bVerbose THEN RAISE NOTICE '(5)tabledef so far: %', v_table_ddl; END IF; -- Add closing paren for regular tables -- IF NOT bPartition THEN @@ -471,19 +597,24 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify( v_table_ddl := v_table_ddl || v_pkey_def || E'\\n'; END IF; - IF bVerbose THEN RAISE INFO '(6)tabledef so far: %', v_table_ddl; END IF; + -- Issue#20 + IF v_fkey_defs <> '' THEN + v_table_ddl := v_table_ddl || v_fkey_defs || E'\\n'; + END IF; + + IF bVerbose THEN RAISE NOTICE '(6)tabledef so far: %', v_table_ddl; END IF; -- create indexes FOR v_indexrec IN SELECT indexdef, COALESCE(tablespace, 'pg_default') as tablespace, indexname FROM pg_indexes WHERE (schemaname, tablename) = (in_schema, in_table) LOOP - -- RAISE INFO 'DEBUG6: indexname=%', v_indexrec.indexname; + -- RAISE NOTICE 'DEBUG6: indexname=% indexdef=%', v_indexrec.indexname, v_indexrec.indexdef; -- loop through constraints and skip ones already defined bSkip = False; FOREACH constraintelement IN ARRAY constraintarr LOOP IF constraintelement = v_indexrec.indexname THEN - -- RAISE INFO 'DEBUG7: skipping index, %', v_indexrec.indexname; + -- RAISE NOTICE 'DEBUG7: skipping index, %', v_indexrec.indexname; bSkip = True; EXIT; END IF; @@ -492,48 +623,47 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify( -- Add IF NOT EXISTS clause so partition index additions will not be created if declarative partition in effect and index already created on parent v_indexrec.indexdef := REPLACE(v_indexrec.indexdef, 'CREATE INDEX', 'CREATE INDEX IF NOT EXISTS'); - -- RAISE INFO 'DEBUG8: adding index, %', v_indexrec.indexname; + -- Fix Issue#26: do it for unique/primary key indexes as well + v_indexrec.indexdef := REPLACE(v_indexrec.indexdef, 'CREATE UNIQUE INDEX', 'CREATE UNIQUE INDEX IF NOT EXISTS'); + -- RAISE NOTICE 'DEBUG8: adding index, %', v_indexrec.indexname; -- NOTE: cannot specify default tablespace for partitioned relations IF v_partition_key IS NOT NULL AND v_partition_key <> '' THEN v_table_ddl := v_table_ddl || v_indexrec.indexdef || ';' || E'\\n'; ELSE - v_table_ddl := v_table_ddl || v_indexrec.indexdef || ' TABLESPACE ' || v_indexrec.tablespace || ';' || E'\\n'; + -- Issue#25: see if partial index or not + select CASE WHEN i.indpred IS NOT NULL THEN True ELSE False END INTO v_partial + FROM pg_index i JOIN pg_class c1 ON (i.indexrelid = c1.oid) JOIN pg_class c2 ON (i.indrelid = c2.oid) + WHERE c1.relnamespace::regnamespace::text = in_schema AND c2.relnamespace::regnamespace::text = in_schema AND c2.relname = in_table AND c1.relname = v_indexrec.indexname; + IF v_partial THEN + -- Put tablespace def before WHERE CLAUSE + v_temp = v_indexrec.indexdef; + v_pos = POSITION(' WHERE ' IN v_temp); + v_temp2 = SUBSTRING(v_temp, v_pos); + v_temp = SUBSTRING(v_temp, 1, v_pos); + v_table_ddl := v_table_ddl || v_temp || ' TABLESPACE ' || v_indexrec.tablespace || v_temp2 || ';' || E'\\n'; + ELSE + v_table_ddl := v_table_ddl || v_indexrec.indexdef || ' TABLESPACE ' || v_indexrec.tablespace || ';' || E'\\n'; + END IF; END IF; END LOOP; - IF bVerbose THEN RAISE INFO '(7)tabledef so far: %', v_table_ddl; END IF; - - -- Handle external foreign key defs here if applicable. - IF fktype = 'FKEYS_EXTERNAL' THEN - -- Issue#13 fix here too for conparentid = 0. and had to change to a loop to handle multiple return set, not a select into variable syntax. - -- Also had to account for PG V10 where there is no conparentid - IF v_pgversion < 110000 THEN - FOR v_constraintrec IN - SELECT 'ALTER TABLE ONLY ' || n.nspname || '.' || c2.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' as fkeydef - FROM pg_constraint r, pg_class c1, pg_namespace n, pg_class c2 where r.conrelid = c1.oid and r.contype = 'f' and n.nspname = in_schema and n.oid = r.connamespace and r.conrelid = c2.oid and c2.relname = in_table + IF bVerbose THEN RAISE NOTICE '(7)tabledef so far: %', v_table_ddl; END IF; + + -- Issue#20: added logic for table and column comments + IF cmtcnt > 0 THEN + FOR v_rec IN + SELECT c.relname, 'COMMENT ON ' || CASE WHEN c.relkind in ('r','p') AND a.attname IS NULL THEN 'TABLE ' WHEN c.relkind in ('r','p') AND a.attname IS NOT NULL THEN 'COLUMN ' WHEN c.relkind = 'f' THEN 'FOREIGN TABLE ' + WHEN c.relkind = 'm' THEN 'MATERIALIZED VIEW ' WHEN c.relkind = 'v' THEN 'VIEW ' WHEN c.relkind = 'i' THEN 'INDEX ' WHEN c.relkind = 'S' THEN 'SEQUENCE ' ELSE 'XX' END || n.nspname || '.' || + CASE WHEN c.relkind in ('r','p') AND a.attname IS NOT NULL THEN quote_ident(c.relname) || '.' || a.attname ELSE quote_ident(c.relname) END || ' IS ' || quote_literal(d.description) || ';' as ddl + FROM pg_class c JOIN pg_namespace n ON (n.oid = c.relnamespace) LEFT JOIN pg_description d ON (c.oid = d.objoid) LEFT JOIN pg_attribute a ON (c.oid = a.attrelid AND a.attnum > 0 and a.attnum = d.objsubid) + WHERE d.description IS NOT NULL AND n.nspname = in_schema AND c.relname = in_table ORDER BY 2 desc, ddl LOOP - v_table_ddl := v_table_ddl || v_constraintrec.fkeydef || ';' || E'\\n'; - IF bVerbose THEN RAISE INFO 'keydef = %', v_constraintrec.fkeydef; END IF; - END LOOP; - ELSE - FOR v_constraintrec IN - SELECT 'ALTER TABLE ONLY ' || n.nspname || '.' || c2.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' as fkeydef - FROM pg_constraint r, pg_class c1, pg_namespace n, pg_class c2 where r.conrelid = c1.oid and r.contype = 'f' and n.nspname = in_schema and n.oid = r.connamespace and r.conrelid = c2.oid and c2.relname = in_table and r.conparentid = 0 - LOOP - v_table_ddl := v_table_ddl || v_constraintrec.fkeydef || E'\\n'; - IF bVerbose THEN RAISE INFO 'keydef = %', v_constraintrec.fkeydef; END IF; - END LOOP; - END IF; - - ELSIF fktype = 'FKEYS_COMMENTED' THEN - SELECT '-- ALTER TABLE ONLY ' || n.nspname || '.' || c2.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' into v_fkey_defs - FROM pg_constraint r, pg_class c1, pg_namespace n, pg_class c2 where r.conrelid = c1.oid and r.contype = 'f' and n.nspname = in_schema and n.oid = r.connamespace and r.conrelid = c2.oid and c2.relname = in_table; - IF v_fkey_defs IS NOT NULL THEN - v_table_ddl := v_table_ddl || v_fkey_defs; - END IF; + --RAISE NOTICE 'comments:%', v_rec.ddl; + v_table_ddl = v_table_ddl || v_rec.ddl || E'\\n'; + END LOOP; END IF; - IF bVerbose THEN RAISE INFO '(8)tabledef so far: %', v_table_ddl; END IF; + IF bVerbose THEN RAISE NOTICE '(8)tabledef so far: %', v_table_ddl; END IF; IF trigtype = 'INCLUDE_TRIGGERS' THEN -- Issue#14: handle multiple triggers for a table @@ -543,12 +673,21 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify( LOOP v_table_ddl := v_table_ddl || v_trigrec.triggerdef; v_table_ddl := v_table_ddl || E'\\n'; - IF bVerbose THEN RAISE INFO 'triggerdef = %', v_trigrec.triggerdef; END IF; + IF bVerbose THEN RAISE NOTICE 'triggerdef = %', v_trigrec.triggerdef; END IF; END LOOP; END IF; + IF bVerbose THEN RAISE NOTICE '(9)tabledef so far: %', v_table_ddl; END IF; -- add empty line v_table_ddl := v_table_ddl || E'\\n'; + IF bVerbose THEN RAISE NOTICE '(10)tabledef so far: %', v_table_ddl; END IF; + + -- reset search_path back to what it was + IF search_path_old = '' THEN + SELECT set_config('search_path', '', false) into v_temp; + ELSE + EXECUTE 'SET search_path = ' || search_path_old; + END IF; RETURN v_table_ddl; diff --git a/playwright-tests/tests/snapshot/spec/table-editor.spec.ts b/playwright-tests/tests/snapshot/spec/table-editor.spec.ts index 994784a3b0e..046e4b7f1a4 100644 --- a/playwright-tests/tests/snapshot/spec/table-editor.spec.ts +++ b/playwright-tests/tests/snapshot/spec/table-editor.spec.ts @@ -49,7 +49,7 @@ test.describe('Table Editor page', () => { // view its definition await page.getByText('definition').click() await expect(page.locator('div.view-lines')).toContainText( - `CREATE TABLE public.${tableName} ( id bigint GENERATED BY DEFAULT AS IDENTITY , created_at timestamp with time zone NULL DEFAULT now(), \"defaultValueColumn\" smallint NULL DEFAULT '2'::smallint, CONSTRAINT ${tableName}_pkey PRIMARY KEY (id)) TABLESPACE pg_default;` + `CREATE TABLE public.${tableName} ( id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, created_at timestamp with time zone NULL DEFAULT now(), \"defaultValueColumn\" smallint NULL DEFAULT '2'::smallint, CONSTRAINT ${tableName}_pkey PRIMARY KEY (id)) TABLESPACE pg_default;` ) // add a new row