Skip to content

Commit

Permalink
adds route for indexes
Browse files Browse the repository at this point in the history
  • Loading branch information
kiwicopple committed Jan 26, 2024
1 parent 352715a commit e93c9f8
Show file tree
Hide file tree
Showing 11 changed files with 334 additions and 0 deletions.
14 changes: 14 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Contributing

### Install deps

- docker
- `npm install`

### Start services

1. Run `docker compose up` in `/test/db`
2. Run the tests: `npm run test:run`
3. Make changes in code (`/src`) and tests (`/test/lib` and `/test/server`)
4. Run the tests again: `npm run test:run`
5. Commit + PR
3 changes: 3 additions & 0 deletions src/lib/PostgresMeta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import PostgresMetaConfig from './PostgresMetaConfig.js'
import PostgresMetaExtensions from './PostgresMetaExtensions.js'
import PostgresMetaForeignTables from './PostgresMetaForeignTables.js'
import PostgresMetaFunctions from './PostgresMetaFunctions.js'
import PostgresMetaIndexes from './PostgresMetaIndexes.js'
import PostgresMetaMaterializedViews from './PostgresMetaMaterializedViews.js'
import PostgresMetaPolicies from './PostgresMetaPolicies.js'
import PostgresMetaPublications from './PostgresMetaPublications.js'
Expand All @@ -30,6 +31,7 @@ export default class PostgresMeta {
extensions: PostgresMetaExtensions
foreignTables: PostgresMetaForeignTables
functions: PostgresMetaFunctions
indexes: PostgresMetaIndexes
materializedViews: PostgresMetaMaterializedViews
policies: PostgresMetaPolicies
publications: PostgresMetaPublications
Expand Down Expand Up @@ -57,6 +59,7 @@ export default class PostgresMeta {
this.extensions = new PostgresMetaExtensions(this.query)
this.foreignTables = new PostgresMetaForeignTables(this.query)
this.functions = new PostgresMetaFunctions(this.query)
this.indexes = new PostgresMetaIndexes(this.query)
this.materializedViews = new PostgresMetaMaterializedViews(this.query)
this.policies = new PostgresMetaPolicies(this.query)
this.publications = new PostgresMetaPublications(this.query)
Expand Down
85 changes: 85 additions & 0 deletions src/lib/PostgresMetaIndexes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { ident, literal } from 'pg-format'
import { DEFAULT_SYSTEM_SCHEMAS } from './constants.js'
import { filterByList } from './helpers.js'
import { indexesSql } from './sql/index.js'
import { PostgresMetaResult, PostgresIndex } from './types.js'

export default class PostgresMetaFunctions {
query: (sql: string) => Promise<PostgresMetaResult<any>>

constructor(query: (sql: string) => Promise<PostgresMetaResult<any>>) {
this.query = query
}

async list({
includeSystemSchemas = false,
includedSchemas,
excludedSchemas,
limit,
offset,
}: {
includeSystemSchemas?: boolean
includedSchemas?: string[]
excludedSchemas?: string[]
limit?: number
offset?: number
} = {}): Promise<PostgresMetaResult<PostgresIndex[]>> {
let sql = enrichedSql
const filter = filterByList(
includedSchemas,
excludedSchemas,
!includeSystemSchemas ? DEFAULT_SYSTEM_SCHEMAS : undefined
)
if (filter) {
sql += ` WHERE schema ${filter}`
}
if (limit) {
sql = `${sql} LIMIT ${limit}`
}
if (offset) {
sql = `${sql} OFFSET ${offset}`
}
return await this.query(sql)
}

async retrieve({ id }: { id: number }): Promise<PostgresMetaResult<PostgresIndex>>
async retrieve({
name,
schema,
args,
}: {
name: string
schema: string
args: string[]
}): Promise<PostgresMetaResult<PostgresIndex>>
async retrieve({
id,
args = [],
}: {
id?: number
args?: string[]
}): Promise<PostgresMetaResult<PostgresIndex>> {
if (id) {
const sql = `${enrichedSql} WHERE id = ${literal(id)};`
const { data, error } = await this.query(sql)
if (error) {
return { data, error }
} else if (data.length === 0) {
return { data: null, error: { message: `Cannot find a index with ID ${id}` } }
} else {
return { data: data[0], error }
}
} else {
return { data: null, error: { message: 'Invalid parameters on function retrieve' } }
}
}
}

const enrichedSql = `
WITH x AS (
${indexesSql}
)
SELECT
x.*
FROM x
`
1 change: 1 addition & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export {
PostgresExtension,
PostgresFunction,
PostgresFunctionCreate,
PostgresIndex,
PostgresMaterializedView,
PostgresPolicy,
PostgresPrimaryKey,
Expand Down
1 change: 1 addition & 0 deletions src/lib/sql/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const configSql = await readFile(join(__dirname, 'config.sql'), 'utf-8')
export const extensionsSql = await readFile(join(__dirname, 'extensions.sql'), 'utf-8')
export const foreignTablesSql = await readFile(join(__dirname, 'foreign_tables.sql'), 'utf-8')
export const functionsSql = await readFile(join(__dirname, 'functions.sql'), 'utf-8')
export const indexesSql = await readFile(join(__dirname, 'indexes.sql'), 'utf-8')
export const materializedViewsSql = await readFile(
join(__dirname, 'materialized_views.sql'),
'utf-8'
Expand Down
41 changes: 41 additions & 0 deletions src/lib/sql/indexes.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
SELECT
idx.indexrelid::int8 AS id,
idx.indrelid::int8 AS table_id,
n.nspname AS schema,
idx.indnatts AS number_of_attributes,
idx.indnkeyatts AS number_of_key_attributes,
idx.indisunique AS is_unique,
idx.indisprimary AS is_primary,
idx.indisexclusion AS is_exclusion,
idx.indimmediate AS is_immediate,
idx.indisclustered AS is_clustered,
idx.indisvalid AS is_valid,
idx.indcheckxmin AS check_xmin,
idx.indisready AS is_ready,
idx.indislive AS is_live,
idx.indisreplident AS is_replica_identity,
idx.indkey AS key_attributes,
idx.indcollation AS collation,
idx.indclass AS class,
idx.indoption AS options,
idx.indpred AS index_predicate,
obj_description(idx.indexrelid, 'pg_class') AS comment,
ix.indexdef as index_definition,
am.amname AS access_method,
jsonb_agg(
jsonb_build_object(
'attribute_number', a.attnum,
'attribute_name', a.attname,
'data_type', format_type(a.atttypid, a.atttypmod)
)
ORDER BY a.attnum
) AS index_attributes
FROM
pg_index idx
JOIN pg_class c ON c.oid = idx.indexrelid
JOIN pg_namespace n ON c.relnamespace = n.oid
JOIN pg_am am ON c.relam = am.oid
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = ANY(idx.indkey)
JOIN pg_indexes ix ON c.relname = ix.indexname
GROUP BY
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
34 changes: 34 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,40 @@ export const postgresFunctionCreateFunction = Type.Object({
})
export type PostgresFunctionCreate = Static<typeof postgresFunctionCreateFunction>

const postgresIndexSchema = Type.Object({
id: Type.Integer(),
table_id: Type.Integer(),
schema: Type.String(),
number_of_attributes: Type.Integer(),
number_of_key_attributes: Type.Integer(),
is_unique: Type.Boolean(),
is_primary: Type.Boolean(),
is_exclusion: Type.Boolean(),
is_immediate: Type.Boolean(),
is_clustered: Type.Boolean(),
is_valid: Type.Boolean(),
check_xmin: Type.Boolean(),
is_ready: Type.Boolean(),
is_live: Type.Boolean(),
is_replica_identity: Type.Boolean(),
key_attributes: Type.Array(Type.Number()),
collation: Type.Array(Type.Number()),
class: Type.Array(Type.Number()),
options: Type.Array(Type.Number()),
index_predicate: Type.Union([Type.String(), Type.Null()]),
comment: Type.Union([Type.String(), Type.Null()]),
index_definition: Type.String(),
access_method: Type.String(),
index_attributes: Type.Array(
Type.Object({
attribute_number: Type.Number(),
attribute_name: Type.String(),
data_type: Type.String(),
})
),
})
export type PostgresIndex = Static<typeof postgresIndexSchema>

export const postgresPolicySchema = Type.Object({
id: Type.Integer(),
schema: Type.String(),
Expand Down
2 changes: 2 additions & 0 deletions src/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import ConfigRoute from './config.js'
import ExtensionsRoute from './extensions.js'
import ForeignTablesRoute from './foreign-tables.js'
import FunctionsRoute from './functions.js'
import IndexesRoute from './indexes.js'
import MaterializedViewsRoute from './materialized-views.js'
import PoliciesRoute from './policies.js'
import PublicationsRoute from './publications.js'
Expand Down Expand Up @@ -49,6 +50,7 @@ export default async (fastify: FastifyInstance) => {
fastify.register(ExtensionsRoute, { prefix: '/extensions' })
fastify.register(ForeignTablesRoute, { prefix: '/foreign-tables' })
fastify.register(FunctionsRoute, { prefix: '/functions' })
fastify.register(IndexesRoute, { prefix: '/indexes' })
fastify.register(MaterializedViewsRoute, { prefix: '/materialized-views' })
fastify.register(PoliciesRoute, { prefix: '/policies' })
fastify.register(PublicationsRoute, { prefix: '/publications' })
Expand Down
63 changes: 63 additions & 0 deletions src/server/routes/indexes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { FastifyInstance } from 'fastify'
import { PostgresMeta } from '../../lib/index.js'
import { DEFAULT_POOL_CONFIG } from '../constants.js'
import { extractRequestForLogging } from '../utils.js'

export default async (fastify: FastifyInstance) => {
fastify.get<{
Headers: { pg: string }
Querystring: {
include_system_schemas?: string
// Note: this only supports comma separated values (e.g., ".../functions?included_schemas=public,core")
included_schemas?: string
excluded_schemas?: string
limit?: number
offset?: number
}
}>('/', async (request, reply) => {
const connectionString = request.headers.pg
const includeSystemSchemas = request.query.include_system_schemas === 'true'
const includedSchemas = request.query.included_schemas?.split(',')
const excludedSchemas = request.query.excluded_schemas?.split(',')
const limit = request.query.limit
const offset = request.query.offset

const pgMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString })
const { data, error } = await pgMeta.indexes.list({
includeSystemSchemas,
includedSchemas,
excludedSchemas,
limit,
offset,
})
await pgMeta.end()
if (error) {
request.log.error({ error, request: extractRequestForLogging(request) })
reply.code(500)
return { error: error.message }
}

return data
})

fastify.get<{
Headers: { pg: string }
Params: {
id: string
}
}>('/:id(\\d+)', async (request, reply) => {
const connectionString = request.headers.pg
const id = Number(request.params.id)

const pgMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString })
const { data, error } = await pgMeta.indexes.retrieve({ id })
await pgMeta.end()
if (error) {
request.log.error({ error, request: extractRequestForLogging(request) })
reply.code(404)
return { error: error.message }
}

return data
})
}
1 change: 1 addition & 0 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import './lib/types'
import './lib/version'
import './lib/views'
import './server/column-privileges'
import './server/indexes'
import './server/materialized-views'
import './server/query'
import './server/ssl'
Expand Down
Loading

0 comments on commit e93c9f8

Please sign in to comment.