Skip to content

Commit 2fd95fc

Browse files
committed
feat: batch endpoints for column creation and retrieval
1 parent eaf321f commit 2fd95fc

File tree

2 files changed

+134
-65
lines changed

2 files changed

+134
-65
lines changed

src/lib/PostgresMetaColumns.ts

Lines changed: 133 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,28 @@ import { DEFAULT_SYSTEM_SCHEMAS } from './constants'
44
import { columnsSql } from './sql'
55
import { 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+
729
export 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;
293362
END
294363
$$;
@@ -302,8 +371,8 @@ $$;
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

test/lib/columns.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ test('retrieve, create, update, delete', async () => {
170170
expect(res).toMatchObject({
171171
data: null,
172172
error: {
173-
message: expect.stringMatching(/^Cannot find a column with ID \d+.1$/),
173+
message: expect.stringMatching(/^Cannot find some of the requested columns.$/),
174174
},
175175
})
176176

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy