Skip to content

Commit 4275a8d

Browse files
committed
wip: batch endpoint for column creation
1 parent 9da6ae6 commit 4275a8d

File tree

3 files changed

+171
-92
lines changed

3 files changed

+171
-92
lines changed

src/lib/PostgresMetaColumns.ts

Lines changed: 128 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,26 @@ import { DEFAULT_SYSTEM_SCHEMAS } from './constants'
44
import { columnsSql } from './sql'
55
import { PostgresMetaResult, PostgresColumn } from './types'
66

7+
interface ColumnCreateRequest {
8+
name: string
9+
type: string
10+
default_value?: any
11+
default_value_format?: 'expression' | 'literal'
12+
is_identity?: boolean
13+
identity_generation?: 'BY DEFAULT' | 'ALWAYS'
14+
is_nullable?: boolean
15+
is_primary_key?: boolean
16+
is_unique?: boolean
17+
comment?: string
18+
check?: string
19+
}
20+
21+
interface ColumnInfoRequest {
22+
id?: string
23+
name?: string
24+
table?: string
25+
schema?: string
26+
}
727
// TODO: Fix handling of `type` in `create()` and `update()`.
828
// `type` on its own is not enough, e.g. `1::my type` should be `1::"my type"`.
929
// `ident(type)` is not enough, e.g. `"int2[]"` should be `"int2"[]`.
@@ -38,6 +58,8 @@ export default class PostgresMetaColumns {
3858
return await this.query(sql)
3959
}
4060

61+
async retrieve(columns: ColumnInfoRequest[]): Promise<PostgresMetaResult<PostgresColumn>> { }
62+
4163
async retrieve({ id }: { id: string }): Promise<PostgresMetaResult<PostgresColumn>>
4264
async retrieve({
4365
name,
@@ -53,12 +75,7 @@ export default class PostgresMetaColumns {
5375
name,
5476
table,
5577
schema = 'public',
56-
}: {
57-
id?: string
58-
name?: string
59-
table?: string
60-
schema?: string
61-
}): Promise<PostgresMetaResult<PostgresColumn>> {
78+
}: ColumnInfoRequest): Promise<PostgresMetaResult<PostgresColumn>> {
6279
if (id) {
6380
const regexp = /^(\d+)\.(\d+)$/
6481
if (!regexp.test(id)) {
@@ -95,49 +112,29 @@ export default class PostgresMetaColumns {
95112
}
96113
}
97114

98-
async create({
99-
table_id,
100-
name,
101-
type,
102-
default_value,
103-
default_value_format = 'literal',
104-
is_identity = false,
105-
identity_generation = 'BY DEFAULT',
106-
// Can't pick a value as default since regular columns are nullable by default but PK columns aren't
107-
is_nullable,
108-
is_primary_key = false,
109-
is_unique = false,
110-
comment,
111-
check,
112-
}: {
113-
table_id: number
114-
name: string
115-
type: string
116-
default_value?: any
117-
default_value_format?: 'expression' | 'literal'
118-
is_identity?: boolean
119-
identity_generation?: 'BY DEFAULT' | 'ALWAYS'
120-
is_nullable?: boolean
121-
is_primary_key?: boolean
122-
is_unique?: boolean
123-
comment?: string
124-
check?: string
125-
}): Promise<PostgresMetaResult<PostgresColumn>> {
126-
const { data, error } = await this.metaTables.retrieve({ id: table_id })
127-
if (error) {
128-
return { data: null, error }
129-
}
130-
const { name: table, schema } = data!
131-
115+
generateColumnSql(
116+
{
117+
name,
118+
type,
119+
default_value,
120+
default_value_format = 'literal',
121+
is_identity = false,
122+
identity_generation = 'BY DEFAULT',
123+
// Can't pick a value as default since regular columns are nullable by default but PK columns aren't
124+
is_nullable,
125+
is_primary_key = false,
126+
is_unique = false,
127+
comment,
128+
check,
129+
}: ColumnCreateRequest,
130+
schema: string,
131+
table: string
132+
): string {
132133
let defaultValueClause = ''
133134
if (is_identity) {
134135
if (default_value !== undefined) {
135-
return {
136-
data: null,
137-
error: { message: 'Columns cannot both be identity and have a default value' },
138-
}
136+
throw new Error(`Column ${name} cannot both be identity and have a default value`)
139137
}
140-
141138
defaultValueClause = `GENERATED ${identity_generation} AS IDENTITY`
142139
} else {
143140
if (default_value === undefined) {
@@ -161,25 +158,65 @@ export default class PostgresMetaColumns {
161158
? ''
162159
: `COMMENT ON COLUMN ${ident(schema)}.${ident(table)}.${ident(name)} IS ${literal(comment)}`
163160

164-
const sql = `
165-
BEGIN;
161+
return `
166162
ALTER TABLE ${ident(schema)}.${ident(table)} ADD COLUMN ${ident(name)} ${type}
167163
${defaultValueClause}
168164
${isNullableClause}
169165
${isPrimaryKeyClause}
170166
${isUniqueClause}
171167
${checkSql};
172-
${commentSql};
173-
COMMIT;`
168+
${commentSql};`
169+
}
170+
171+
async createBatch({
172+
table_id,
173+
columns,
174+
}: {
175+
table_id: number
176+
columns: ColumnCreateRequest[]
177+
}): Promise<PostgresMetaResult<PostgresColumn>> {
178+
const { data, error } = await this.metaTables.retrieve({ id: table_id })
179+
if (error) {
180+
return { data: null, error }
181+
}
182+
const { name: table, schema } = data!
183+
let columnsSql: string
184+
try {
185+
columnsSql = columns.map((column) => this.generateColumnSql(column, schema, table)).join('\n')
186+
} catch (e: any) {
187+
return { data: null, error: { message: e.toString() } }
188+
}
189+
174190
{
175-
const { error } = await this.query(sql)
191+
const { error } = await this.query(`
192+
BEGIN;
193+
${columnsSql}
194+
COMMIT;
195+
`)
176196
if (error) {
177197
return { data: null, error }
178198
}
179199
}
180200
return await this.retrieve({ name, table, schema })
181201
}
182202

203+
async create(body: {
204+
table_id: number
205+
name: string
206+
type: string
207+
default_value?: any
208+
default_value_format?: 'expression' | 'literal'
209+
is_identity?: boolean
210+
identity_generation?: 'BY DEFAULT' | 'ALWAYS'
211+
is_nullable?: boolean
212+
is_primary_key?: boolean
213+
is_unique?: boolean
214+
comment?: string
215+
check?: string
216+
}): Promise<PostgresMetaResult<PostgresColumn>> {
217+
return this.createBatch(body.table_id, [body])
218+
}
219+
183220
async update(
184221
id: string,
185222
{
@@ -215,29 +252,29 @@ COMMIT;`
215252
name === undefined || name === old!.name
216253
? ''
217254
: `ALTER TABLE ${ident(old!.schema)}.${ident(old!.table)} RENAME COLUMN ${ident(
218-
old!.name
219-
)} TO ${ident(name)};`
255+
old!.name
256+
)} TO ${ident(name)}; `
220257
// We use USING to allow implicit conversion of incompatible types (e.g. int4 -> text).
221258
const typeSql =
222259
type === undefined
223260
? ''
224261
: `ALTER TABLE ${ident(old!.schema)}.${ident(old!.table)} ALTER COLUMN ${ident(
225-
old!.name
226-
)} SET DATA TYPE ${ident(type)} USING ${ident(old!.name)}::${ident(type)};`
262+
old!.name
263+
)} SET DATA TYPE ${ident(type)} USING ${ident(old!.name)}:: ${ident(type)}; `
227264

228265
let defaultValueSql: string
229266
if (drop_default) {
230267
defaultValueSql = `ALTER TABLE ${ident(old!.schema)}.${ident(
231268
old!.table
232-
)} ALTER COLUMN ${ident(old!.name)} DROP DEFAULT;`
269+
)} ALTER COLUMN ${ident(old!.name)} DROP DEFAULT; `
233270
} else if (default_value === undefined) {
234271
defaultValueSql = ''
235272
} else {
236273
const defaultValue =
237274
default_value_format === 'expression' ? default_value : literal(default_value)
238275
defaultValueSql = `ALTER TABLE ${ident(old!.schema)}.${ident(
239276
old!.table
240-
)} ALTER COLUMN ${ident(old!.name)} SET DEFAULT ${defaultValue};`
277+
)} ALTER COLUMN ${ident(old!.name)} SET DEFAULT ${defaultValue}; `
241278
}
242279
// What identitySql does vary depending on the old and new values of
243280
// is_identity and identity_generation.
@@ -249,64 +286,64 @@ COMMIT;`
249286
// | false | - | add identity | drop if exists |
250287
let identitySql = `ALTER TABLE ${ident(old!.schema)}.${ident(old!.table)} ALTER COLUMN ${ident(
251288
old!.name
252-
)}`
289+
)} `
253290
if (is_identity === false) {
254291
identitySql += ' DROP IDENTITY IF EXISTS;'
255292
} else if (old!.is_identity === true) {
256293
if (identity_generation === undefined) {
257294
identitySql = ''
258295
} else {
259-
identitySql += ` SET GENERATED ${identity_generation};`
296+
identitySql += ` SET GENERATED ${identity_generation}; `
260297
}
261298
} else if (is_identity === undefined) {
262299
identitySql = ''
263300
} else {
264-
identitySql += ` ADD GENERATED ${identity_generation} AS IDENTITY;`
301+
identitySql += ` ADD GENERATED ${identity_generation} AS IDENTITY; `
265302
}
266303
let isNullableSql: string
267304
if (is_nullable === undefined) {
268305
isNullableSql = ''
269306
} else {
270307
isNullableSql = is_nullable
271308
? `ALTER TABLE ${ident(old!.schema)}.${ident(old!.table)} ALTER COLUMN ${ident(
272-
old!.name
273-
)} DROP NOT NULL;`
309+
old!.name
310+
)} DROP NOT NULL; `
274311
: `ALTER TABLE ${ident(old!.schema)}.${ident(old!.table)} ALTER COLUMN ${ident(
275-
old!.name
276-
)} SET NOT NULL;`
312+
old!.name
313+
)} SET NOT NULL; `
277314
}
278315
let isUniqueSql = ''
279316
if (old!.is_unique === true && is_unique === false) {
280317
isUniqueSql = `
281-
DO $$
282-
DECLARE
283-
r record;
284-
BEGIN
285-
FOR r IN
318+
DO $$
319+
DECLARE
320+
r record;
321+
BEGIN
322+
FOR r IN
286323
SELECT conname FROM pg_constraint WHERE
287-
contype = 'u'
288-
AND cardinality(conkey) = 1
289-
AND conrelid = ${literal(old!.table_id)}
290-
AND conkey[1] = ${literal(old!.ordinal_position)}
291-
LOOP
324+
contype = 'u'
325+
AND cardinality(conkey) = 1
326+
AND conrelid = ${literal(old!.table_id)}
327+
AND conkey[1] = ${literal(old!.ordinal_position)}
328+
LOOP
292329
EXECUTE ${literal(
293-
`ALTER TABLE ${ident(old!.schema)}.${ident(old!.table)} DROP CONSTRAINT `
294-
)} || quote_ident(r.conname);
295-
END LOOP;
296-
END
297-
$$;
298-
`
330+
`ALTER TABLE ${ident(old!.schema)}.${ident(old!.table)} DROP CONSTRAINT `
331+
)} || quote_ident(r.conname);
332+
END LOOP;
333+
END
334+
$$;
335+
`
299336
} else if (old!.is_unique === false && is_unique === true) {
300-
isUniqueSql = `ALTER TABLE ${ident(old!.schema)}.${ident(old!.table)} ADD UNIQUE (${ident(
337+
isUniqueSql = `ALTER TABLE ${ident(old!.schema)}.${ident(old!.table)} ADD UNIQUE(${ident(
301338
old!.name
302339
)});`
303340
}
304341
const commentSql =
305342
comment === undefined
306343
? ''
307344
: `COMMENT ON COLUMN ${ident(old!.schema)}.${ident(old!.table)}.${ident(
308-
old!.name
309-
)} IS ${literal(comment)};`
345+
old!.name
346+
)} IS ${literal(comment)}; `
310347

311348
// TODO: Can't set default if column is previously identity even if
312349
// is_identity: false. Must do two separate PATCHes (once to drop identity
@@ -315,14 +352,14 @@ $$;
315352
// identitySql must be after isNullableSql.
316353
const sql = `
317354
BEGIN;
318-
${isNullableSql}
319-
${typeSql}
320-
${defaultValueSql}
321-
${identitySql}
322-
${isUniqueSql}
323-
${commentSql}
324-
${nameSql}
325-
COMMIT;`
355+
${isNullableSql}
356+
${typeSql}
357+
${defaultValueSql}
358+
${identitySql}
359+
${isUniqueSql}
360+
${commentSql}
361+
${nameSql}
362+
COMMIT; `
326363
{
327364
const { error } = await this.query(sql)
328365
if (error) {
@@ -339,7 +376,7 @@ COMMIT;`
339376
}
340377
const sql = `ALTER TABLE ${ident(column!.schema)}.${ident(column!.table)} DROP COLUMN ${ident(
341378
column!.name
342-
)};`
379+
)}; `
343380
{
344381
const { error } = await this.query(sql)
345382
if (error) {

src/server/routes/columns.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ export default async (fastify: FastifyInstance) => {
6161
const connectionString = request.headers.pg
6262

6363
const pgMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString })
64-
const { data, error } = await pgMeta.columns.create(request.body)
64+
const method = 'columns' in request.body ? pgMeta.columns.createBatch : pgMeta.columns.create
65+
const { data, error } = await method(request.body)
6566
await pgMeta.end()
6667
if (error) {
6768
request.log.error({ error, request: extractRequestForLogging(request) })

0 commit comments

Comments
 (0)