Skip to content

Commit ea6d1c8

Browse files
authored
feat: adds route for indexes (#700)
1 parent f0f5237 commit ea6d1c8

File tree

11 files changed

+334
-0
lines changed

11 files changed

+334
-0
lines changed

CONTRIBUTING.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Contributing
2+
3+
### Install deps
4+
5+
- docker
6+
- `npm install`
7+
8+
### Start services
9+
10+
1. Run `docker compose up` in `/test/db`
11+
2. Run the tests: `npm run test:run`
12+
3. Make changes in code (`/src`) and tests (`/test/lib` and `/test/server`)
13+
4. Run the tests again: `npm run test:run`
14+
5. Commit + PR

src/lib/PostgresMeta.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import PostgresMetaConfig from './PostgresMetaConfig.js'
66
import PostgresMetaExtensions from './PostgresMetaExtensions.js'
77
import PostgresMetaForeignTables from './PostgresMetaForeignTables.js'
88
import PostgresMetaFunctions from './PostgresMetaFunctions.js'
9+
import PostgresMetaIndexes from './PostgresMetaIndexes.js'
910
import PostgresMetaMaterializedViews from './PostgresMetaMaterializedViews.js'
1011
import PostgresMetaPolicies from './PostgresMetaPolicies.js'
1112
import PostgresMetaPublications from './PostgresMetaPublications.js'
@@ -30,6 +31,7 @@ export default class PostgresMeta {
3031
extensions: PostgresMetaExtensions
3132
foreignTables: PostgresMetaForeignTables
3233
functions: PostgresMetaFunctions
34+
indexes: PostgresMetaIndexes
3335
materializedViews: PostgresMetaMaterializedViews
3436
policies: PostgresMetaPolicies
3537
publications: PostgresMetaPublications
@@ -57,6 +59,7 @@ export default class PostgresMeta {
5759
this.extensions = new PostgresMetaExtensions(this.query)
5860
this.foreignTables = new PostgresMetaForeignTables(this.query)
5961
this.functions = new PostgresMetaFunctions(this.query)
62+
this.indexes = new PostgresMetaIndexes(this.query)
6063
this.materializedViews = new PostgresMetaMaterializedViews(this.query)
6164
this.policies = new PostgresMetaPolicies(this.query)
6265
this.publications = new PostgresMetaPublications(this.query)

src/lib/PostgresMetaIndexes.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { ident, literal } from 'pg-format'
2+
import { DEFAULT_SYSTEM_SCHEMAS } from './constants.js'
3+
import { filterByList } from './helpers.js'
4+
import { indexesSql } from './sql/index.js'
5+
import { PostgresMetaResult, PostgresIndex } from './types.js'
6+
7+
export default class PostgresMetaFunctions {
8+
query: (sql: string) => Promise<PostgresMetaResult<any>>
9+
10+
constructor(query: (sql: string) => Promise<PostgresMetaResult<any>>) {
11+
this.query = query
12+
}
13+
14+
async list({
15+
includeSystemSchemas = false,
16+
includedSchemas,
17+
excludedSchemas,
18+
limit,
19+
offset,
20+
}: {
21+
includeSystemSchemas?: boolean
22+
includedSchemas?: string[]
23+
excludedSchemas?: string[]
24+
limit?: number
25+
offset?: number
26+
} = {}): Promise<PostgresMetaResult<PostgresIndex[]>> {
27+
let sql = enrichedSql
28+
const filter = filterByList(
29+
includedSchemas,
30+
excludedSchemas,
31+
!includeSystemSchemas ? DEFAULT_SYSTEM_SCHEMAS : undefined
32+
)
33+
if (filter) {
34+
sql += ` WHERE schema ${filter}`
35+
}
36+
if (limit) {
37+
sql = `${sql} LIMIT ${limit}`
38+
}
39+
if (offset) {
40+
sql = `${sql} OFFSET ${offset}`
41+
}
42+
return await this.query(sql)
43+
}
44+
45+
async retrieve({ id }: { id: number }): Promise<PostgresMetaResult<PostgresIndex>>
46+
async retrieve({
47+
name,
48+
schema,
49+
args,
50+
}: {
51+
name: string
52+
schema: string
53+
args: string[]
54+
}): Promise<PostgresMetaResult<PostgresIndex>>
55+
async retrieve({
56+
id,
57+
args = [],
58+
}: {
59+
id?: number
60+
args?: string[]
61+
}): Promise<PostgresMetaResult<PostgresIndex>> {
62+
if (id) {
63+
const sql = `${enrichedSql} WHERE id = ${literal(id)};`
64+
const { data, error } = await this.query(sql)
65+
if (error) {
66+
return { data, error }
67+
} else if (data.length === 0) {
68+
return { data: null, error: { message: `Cannot find a index with ID ${id}` } }
69+
} else {
70+
return { data: data[0], error }
71+
}
72+
} else {
73+
return { data: null, error: { message: 'Invalid parameters on function retrieve' } }
74+
}
75+
}
76+
}
77+
78+
const enrichedSql = `
79+
WITH x AS (
80+
${indexesSql}
81+
)
82+
SELECT
83+
x.*
84+
FROM x
85+
`

src/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export {
88
PostgresExtension,
99
PostgresFunction,
1010
PostgresFunctionCreate,
11+
PostgresIndex,
1112
PostgresMaterializedView,
1213
PostgresPolicy,
1314
PostgresPrimaryKey,

src/lib/sql/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const configSql = await readFile(join(__dirname, 'config.sql'), 'utf-8')
99
export const extensionsSql = await readFile(join(__dirname, 'extensions.sql'), 'utf-8')
1010
export const foreignTablesSql = await readFile(join(__dirname, 'foreign_tables.sql'), 'utf-8')
1111
export const functionsSql = await readFile(join(__dirname, 'functions.sql'), 'utf-8')
12+
export const indexesSql = await readFile(join(__dirname, 'indexes.sql'), 'utf-8')
1213
export const materializedViewsSql = await readFile(
1314
join(__dirname, 'materialized_views.sql'),
1415
'utf-8'

src/lib/sql/indexes.sql

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
SELECT
2+
idx.indexrelid::int8 AS id,
3+
idx.indrelid::int8 AS table_id,
4+
n.nspname AS schema,
5+
idx.indnatts AS number_of_attributes,
6+
idx.indnkeyatts AS number_of_key_attributes,
7+
idx.indisunique AS is_unique,
8+
idx.indisprimary AS is_primary,
9+
idx.indisexclusion AS is_exclusion,
10+
idx.indimmediate AS is_immediate,
11+
idx.indisclustered AS is_clustered,
12+
idx.indisvalid AS is_valid,
13+
idx.indcheckxmin AS check_xmin,
14+
idx.indisready AS is_ready,
15+
idx.indislive AS is_live,
16+
idx.indisreplident AS is_replica_identity,
17+
idx.indkey AS key_attributes,
18+
idx.indcollation AS collation,
19+
idx.indclass AS class,
20+
idx.indoption AS options,
21+
idx.indpred AS index_predicate,
22+
obj_description(idx.indexrelid, 'pg_class') AS comment,
23+
ix.indexdef as index_definition,
24+
am.amname AS access_method,
25+
jsonb_agg(
26+
jsonb_build_object(
27+
'attribute_number', a.attnum,
28+
'attribute_name', a.attname,
29+
'data_type', format_type(a.atttypid, a.atttypmod)
30+
)
31+
ORDER BY a.attnum
32+
) AS index_attributes
33+
FROM
34+
pg_index idx
35+
JOIN pg_class c ON c.oid = idx.indexrelid
36+
JOIN pg_namespace n ON c.relnamespace = n.oid
37+
JOIN pg_am am ON c.relam = am.oid
38+
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = ANY(idx.indkey)
39+
JOIN pg_indexes ix ON c.relname = ix.indexname
40+
GROUP BY
41+
idx.indexrelid, idx.indrelid, n.nspname, idx.indnatts, idx.indnkeyatts, idx.indisunique, idx.indisprimary, idx.indisexclusion, idx.indimmediate, idx.indisclustered, idx.indisvalid, idx.indcheckxmin, idx.indisready, idx.indislive, idx.indisreplident, idx.indkey, idx.indcollation, idx.indclass, idx.indoption, idx.indexprs, idx.indpred, ix.indexdef, am.amname

src/lib/types.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,40 @@ export const postgresFunctionCreateFunction = Type.Object({
175175
})
176176
export type PostgresFunctionCreate = Static<typeof postgresFunctionCreateFunction>
177177

178+
const postgresIndexSchema = Type.Object({
179+
id: Type.Integer(),
180+
table_id: Type.Integer(),
181+
schema: Type.String(),
182+
number_of_attributes: Type.Integer(),
183+
number_of_key_attributes: Type.Integer(),
184+
is_unique: Type.Boolean(),
185+
is_primary: Type.Boolean(),
186+
is_exclusion: Type.Boolean(),
187+
is_immediate: Type.Boolean(),
188+
is_clustered: Type.Boolean(),
189+
is_valid: Type.Boolean(),
190+
check_xmin: Type.Boolean(),
191+
is_ready: Type.Boolean(),
192+
is_live: Type.Boolean(),
193+
is_replica_identity: Type.Boolean(),
194+
key_attributes: Type.Array(Type.Number()),
195+
collation: Type.Array(Type.Number()),
196+
class: Type.Array(Type.Number()),
197+
options: Type.Array(Type.Number()),
198+
index_predicate: Type.Union([Type.String(), Type.Null()]),
199+
comment: Type.Union([Type.String(), Type.Null()]),
200+
index_definition: Type.String(),
201+
access_method: Type.String(),
202+
index_attributes: Type.Array(
203+
Type.Object({
204+
attribute_number: Type.Number(),
205+
attribute_name: Type.String(),
206+
data_type: Type.String(),
207+
})
208+
),
209+
})
210+
export type PostgresIndex = Static<typeof postgresIndexSchema>
211+
178212
export const postgresPolicySchema = Type.Object({
179213
id: Type.Integer(),
180214
schema: Type.String(),

src/server/routes/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import ConfigRoute from './config.js'
66
import ExtensionsRoute from './extensions.js'
77
import ForeignTablesRoute from './foreign-tables.js'
88
import FunctionsRoute from './functions.js'
9+
import IndexesRoute from './indexes.js'
910
import MaterializedViewsRoute from './materialized-views.js'
1011
import PoliciesRoute from './policies.js'
1112
import PublicationsRoute from './publications.js'
@@ -49,6 +50,7 @@ export default async (fastify: FastifyInstance) => {
4950
fastify.register(ExtensionsRoute, { prefix: '/extensions' })
5051
fastify.register(ForeignTablesRoute, { prefix: '/foreign-tables' })
5152
fastify.register(FunctionsRoute, { prefix: '/functions' })
53+
fastify.register(IndexesRoute, { prefix: '/indexes' })
5254
fastify.register(MaterializedViewsRoute, { prefix: '/materialized-views' })
5355
fastify.register(PoliciesRoute, { prefix: '/policies' })
5456
fastify.register(PublicationsRoute, { prefix: '/publications' })

src/server/routes/indexes.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { FastifyInstance } from 'fastify'
2+
import { PostgresMeta } from '../../lib/index.js'
3+
import { DEFAULT_POOL_CONFIG } from '../constants.js'
4+
import { extractRequestForLogging } from '../utils.js'
5+
6+
export default async (fastify: FastifyInstance) => {
7+
fastify.get<{
8+
Headers: { pg: string }
9+
Querystring: {
10+
include_system_schemas?: string
11+
// Note: this only supports comma separated values (e.g., ".../functions?included_schemas=public,core")
12+
included_schemas?: string
13+
excluded_schemas?: string
14+
limit?: number
15+
offset?: number
16+
}
17+
}>('/', async (request, reply) => {
18+
const connectionString = request.headers.pg
19+
const includeSystemSchemas = request.query.include_system_schemas === 'true'
20+
const includedSchemas = request.query.included_schemas?.split(',')
21+
const excludedSchemas = request.query.excluded_schemas?.split(',')
22+
const limit = request.query.limit
23+
const offset = request.query.offset
24+
25+
const pgMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString })
26+
const { data, error } = await pgMeta.indexes.list({
27+
includeSystemSchemas,
28+
includedSchemas,
29+
excludedSchemas,
30+
limit,
31+
offset,
32+
})
33+
await pgMeta.end()
34+
if (error) {
35+
request.log.error({ error, request: extractRequestForLogging(request) })
36+
reply.code(500)
37+
return { error: error.message }
38+
}
39+
40+
return data
41+
})
42+
43+
fastify.get<{
44+
Headers: { pg: string }
45+
Params: {
46+
id: string
47+
}
48+
}>('/:id(\\d+)', async (request, reply) => {
49+
const connectionString = request.headers.pg
50+
const id = Number(request.params.id)
51+
52+
const pgMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString })
53+
const { data, error } = await pgMeta.indexes.retrieve({ id })
54+
await pgMeta.end()
55+
if (error) {
56+
request.log.error({ error, request: extractRequestForLogging(request) })
57+
reply.code(404)
58+
return { error: error.message }
59+
}
60+
61+
return data
62+
})
63+
}

test/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import './lib/types'
1616
import './lib/version'
1717
import './lib/views'
1818
import './server/column-privileges'
19+
import './server/indexes'
1920
import './server/materialized-views'
2021
import './server/query'
2122
import './server/ssl'

0 commit comments

Comments
 (0)