@@ -4,6 +4,28 @@ import { DEFAULT_SYSTEM_SCHEMAS } from './constants'
44import { columnsSql } from './sql'
55import { PostgresMetaResult , PostgresColumn } from './types'
66
7+ interface ColumnCreationRequest {
8+ table_id : number
9+ name : string
10+ type : string
11+ default_value ?: any
12+ default_value_format ?: 'expression' | 'literal'
13+ is_identity ?: boolean
14+ identity_generation ?: 'BY DEFAULT' | 'ALWAYS'
15+ is_nullable ?: boolean
16+ is_primary_key ?: boolean
17+ is_unique ?: boolean
18+ comment ?: string
19+ check ?: string
20+ }
21+
22+ interface ColumnBatchInfoRequest {
23+ ids ?: string [ ]
24+ names ?: string [ ]
25+ table ?: string
26+ schema ?: string
27+ }
28+
729export default class PostgresMetaColumns {
830 query : ( sql : string ) => Promise < PostgresMetaResult < any > >
931 metaTables : PostgresMetaTables
@@ -57,75 +79,130 @@ export default class PostgresMetaColumns {
5779 schema ?: string
5880 } ) : Promise < PostgresMetaResult < PostgresColumn > > {
5981 if ( id ) {
60- const regexp = / ^ ( \d + ) \. ( \d + ) $ /
61- if ( ! regexp . test ( id ) ) {
62- return { data : null , error : { message : 'Invalid format for column ID' } }
82+ const { data, error } = await this . batchRetrieve ( { ids : [ id ] } )
83+ if ( data ) {
84+ return { data : data [ 0 ] , error : null }
85+ } else if ( error ) {
86+ return { data : null , error : error }
87+ }
88+ }
89+ if ( name && table ) {
90+ const { data, error } = await this . batchRetrieve ( { names : [ name ] , table, schema } )
91+ if ( data ) {
92+ return { data : data [ 0 ] , error : null }
93+ } else if ( error ) {
94+ return { data : null , error : error }
6395 }
64- const matches = id . match ( regexp ) as RegExpMatchArray
65- const [ tableId , ordinalPos ] = matches . slice ( 1 ) . map ( Number )
66- const sql = `${ columnsSql } AND c.oid = ${ tableId } AND a.attnum = ${ ordinalPos } ;`
96+ }
97+ return { data : null , error : { message : 'Invalid parameters on column retrieve' } }
98+ }
99+
100+ async batchRetrieve ( {
101+ ids,
102+ names,
103+ table,
104+ schema = 'public' ,
105+ } : ColumnBatchInfoRequest ) : Promise < PostgresMetaResult < PostgresColumn [ ] > > {
106+ if ( ids && ids . length > 0 ) {
107+ const regexp = / ^ ( \d + ) \. ( \d + ) $ /
108+ const filteringClauses = ids
109+ . map ( ( id ) => {
110+ if ( ! regexp . test ( id ) ) {
111+ return { data : null , error : { message : 'Invalid format for column ID' } }
112+ }
113+ const matches = id . match ( regexp ) as RegExpMatchArray
114+ const [ tableId , ordinalPos ] = matches . slice ( 1 ) . map ( Number )
115+ return `(c.oid = ${ tableId } AND a.attnum = ${ ordinalPos } )`
116+ } )
117+ . join ( ' OR ' )
118+ const sql = `${ columnsSql } AND (${ filteringClauses } );`
67119 const { data, error } = await this . query ( sql )
68120 if ( error ) {
69121 return { data, error }
70- } else if ( data . length === 0 ) {
71- return { data : null , error : { message : `Cannot find a column with ID ${ id } ` } }
122+ } else if ( data . length < ids . length ) {
123+ return { data : null , error : { message : `Cannot find some of the requested columns. ` } }
72124 } else {
73- return { data : data [ 0 ] , error }
125+ return { data, error }
74126 }
75- } else if ( name && table ) {
76- const sql = `${ columnsSql } AND a.attname = ${ literal ( name ) } AND c.relname = ${ literal (
127+ } else if ( names && names . length > 0 && table ) {
128+ const filteringClauses = names . map ( ( name ) => `a.attname = ${ literal ( name ) } ` ) . join ( ' OR ' )
129+ const sql = `${ columnsSql } AND (${ filteringClauses } ) AND c.relname = ${ literal (
77130 table
78131 ) } AND nc.nspname = ${ literal ( schema ) } ;`
79132 const { data, error } = await this . query ( sql )
80133 if ( error ) {
81134 return { data, error }
82- } else if ( data . length === 0 ) {
135+ } else if ( data . length < names . length ) {
83136 return {
84137 data : null ,
85- error : { message : `Cannot find a column named ${ name } in table ${ schema } . ${ table } ` } ,
138+ error : { message : `Cannot find some of the requested columns. ` } ,
86139 }
87140 } else {
88- return { data : data [ 0 ] , error }
141+ return { data, error }
89142 }
90143 } else {
91144 return { data : null , error : { message : 'Invalid parameters on column retrieve' } }
92145 }
93146 }
94147
95- async create ( {
96- table_id,
97- name,
98- type,
99- default_value,
100- default_value_format = 'literal' ,
101- is_identity = false ,
102- identity_generation = 'BY DEFAULT' ,
103- // Can't pick a value as default since regular columns are nullable by default but PK columns aren't
104- is_nullable,
105- is_primary_key = false ,
106- is_unique = false ,
107- comment,
108- check,
109- } : {
110- table_id : number
111- name : string
112- type : string
113- default_value ?: any
114- default_value_format ?: 'expression' | 'literal'
115- is_identity ?: boolean
116- identity_generation ?: 'BY DEFAULT' | 'ALWAYS'
117- is_nullable ?: boolean
118- is_primary_key ?: boolean
119- is_unique ?: boolean
120- comment ?: string
121- check ?: string
122- } ) : Promise < PostgresMetaResult < PostgresColumn > > {
148+ async create ( col : ColumnCreationRequest ) : Promise < PostgresMetaResult < PostgresColumn > > {
149+ const { data, error } = await this . batchCreate ( [ col ] )
150+ if ( data ) {
151+ return { data : data [ 0 ] , error : null }
152+ } else if ( error ) {
153+ return { data : null , error : error }
154+ }
155+ return { data : null , error : { message : 'Invalid params' } }
156+ }
157+
158+ async batchCreate ( cols : ColumnCreationRequest [ ] ) : Promise < PostgresMetaResult < PostgresColumn [ ] > > {
159+ if ( cols . length < 1 ) {
160+ throw new Error ( 'no columns provided for creation' )
161+ }
162+ if ( [ ...new Set ( cols . map ( ( col ) => col . table_id ) ) ] . length > 1 ) {
163+ throw new Error ( 'all columns in a single request must share the same table' )
164+ }
165+ const { table_id } = cols [ 0 ]
123166 const { data, error } = await this . metaTables . retrieve ( { id : table_id } )
124167 if ( error ) {
125168 return { data : null , error }
126169 }
127170 const { name : table , schema } = data !
128171
172+ const sqlStrings = cols . map ( ( col ) => this . generateColumnCreationSql ( col , schema , table ) )
173+
174+ const sql = `BEGIN;
175+ ${ sqlStrings . join ( '\n' ) }
176+ COMMIT;
177+ `
178+ {
179+ const { error } = await this . query ( sql )
180+ if ( error ) {
181+ return { data : null , error }
182+ }
183+ }
184+ const names = cols . map ( ( col ) => col . name )
185+ return await this . batchRetrieve ( { names, table, schema } )
186+ }
187+
188+ generateColumnCreationSql (
189+ {
190+ name,
191+ type,
192+ default_value,
193+ default_value_format = 'literal' ,
194+ is_identity = false ,
195+ identity_generation = 'BY DEFAULT' ,
196+ // Can't pick a value as default since regular columns are nullable by default but PK columns aren't
197+ is_nullable,
198+ is_primary_key = false ,
199+ is_unique = false ,
200+ comment,
201+ check,
202+ } : ColumnCreationRequest ,
203+ schema : string ,
204+ table : string
205+ ) {
129206 let defaultValueClause = ''
130207 if ( is_identity ) {
131208 if ( default_value !== undefined ) {
@@ -159,22 +236,14 @@ export default class PostgresMetaColumns {
159236 : `COMMENT ON COLUMN ${ ident ( schema ) } .${ ident ( table ) } .${ ident ( name ) } IS ${ literal ( comment ) } `
160237
161238 const sql = `
162- BEGIN;
163239 ALTER TABLE ${ ident ( schema ) } .${ ident ( table ) } ADD COLUMN ${ ident ( name ) } ${ typeIdent ( type ) }
164240 ${ defaultValueClause }
165241 ${ isNullableClause }
166242 ${ isPrimaryKeyClause }
167243 ${ isUniqueClause }
168244 ${ checkSql } ;
169- ${ commentSql } ;
170- COMMIT;`
171- {
172- const { error } = await this . query ( sql )
173- if ( error ) {
174- return { data : null , error }
175- }
176- }
177- return await this . retrieve ( { name, table, schema } )
245+ ${ commentSql } ;`
246+ return sql
178247 }
179248
180249 async update (
@@ -212,15 +281,15 @@ COMMIT;`
212281 name === undefined || name === old ! . name
213282 ? ''
214283 : `ALTER TABLE ${ ident ( old ! . schema ) } .${ ident ( old ! . table ) } RENAME COLUMN ${ ident (
215- old ! . name
216- ) } TO ${ ident ( name ) } ;`
284+ old ! . name
285+ ) } TO ${ ident ( name ) } ;`
217286 // We use USING to allow implicit conversion of incompatible types (e.g. int4 -> text).
218287 const typeSql =
219288 type === undefined
220289 ? ''
221290 : `ALTER TABLE ${ ident ( old ! . schema ) } .${ ident ( old ! . table ) } ALTER COLUMN ${ ident (
222- old ! . name
223- ) } SET DATA TYPE ${ typeIdent ( type ) } USING ${ ident ( old ! . name ) } ::${ typeIdent ( type ) } ;`
291+ old ! . name
292+ ) } SET DATA TYPE ${ typeIdent ( type ) } USING ${ ident ( old ! . name ) } ::${ typeIdent ( type ) } ;`
224293
225294 let defaultValueSql : string
226295 if ( drop_default ) {
@@ -266,11 +335,11 @@ COMMIT;`
266335 } else {
267336 isNullableSql = is_nullable
268337 ? `ALTER TABLE ${ ident ( old ! . schema ) } .${ ident ( old ! . table ) } ALTER COLUMN ${ ident (
269- old ! . name
270- ) } DROP NOT NULL;`
338+ old ! . name
339+ ) } DROP NOT NULL;`
271340 : `ALTER TABLE ${ ident ( old ! . schema ) } .${ ident ( old ! . table ) } ALTER COLUMN ${ ident (
272- old ! . name
273- ) } SET NOT NULL;`
341+ old ! . name
342+ ) } SET NOT NULL;`
274343 }
275344 let isUniqueSql = ''
276345 if ( old ! . is_unique === true && is_unique === false ) {
@@ -287,8 +356,8 @@ BEGIN
287356 AND conkey[1] = ${ literal ( old ! . ordinal_position ) }
288357 LOOP
289358 EXECUTE ${ literal (
290- `ALTER TABLE ${ ident ( old ! . schema ) } .${ ident ( old ! . table ) } DROP CONSTRAINT `
291- ) } || quote_ident(r.conname);
359+ `ALTER TABLE ${ ident ( old ! . schema ) } .${ ident ( old ! . table ) } DROP CONSTRAINT `
360+ ) } || quote_ident(r.conname);
292361 END LOOP;
293362END
294363$$;
302371 comment === undefined
303372 ? ''
304373 : `COMMENT ON COLUMN ${ ident ( old ! . schema ) } .${ ident ( old ! . table ) } .${ ident (
305- old ! . name
306- ) } IS ${ literal ( comment ) } ;`
374+ old ! . name
375+ ) } IS ${ literal ( comment ) } ;`
307376
308377 // TODO: Can't set default if column is previously identity even if
309378 // is_identity: false. Must do two separate PATCHes (once to drop identity
0 commit comments