Skip to content

adds route for indexes #700

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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