Skip to content

Commit 9fdb9a7

Browse files
committed
Fix Oracle connector: database infos, indexes and types
1 parent e5d150c commit 9fdb9a7

File tree

7 files changed

+66
-55
lines changed

7 files changed

+66
-55
lines changed

cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "azimutt",
33
"description": "Export database schema from relational or document databases. Import it to https://azimutt.app",
4-
"version": "0.1.36",
4+
"version": "0.1.37",
55
"license": "MIT",
66
"homepage": "https://azimutt.app",
77
"keywords": [

cli/src/version.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export const version = '0.1.36' // FIXME: `process.env.npm_package_version` is not available :/
1+
export const version = '0.1.37' // FIXME: `process.env.npm_package_version` is not available :/

gateway/package-lock.json

Lines changed: 9 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gateway/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@azimutt/gateway",
33
"description": "A Gateway to proxy database access for Azimutt frontend",
4-
"version": "0.1.23",
4+
"version": "0.1.24",
55
"license": "MIT",
66
"homepage": "https://azimutt.app",
77
"keywords": [
@@ -32,7 +32,7 @@
3232
"@azimutt/connector-mariadb": "^0.1.9",
3333
"@azimutt/connector-mongodb": "^0.1.4",
3434
"@azimutt/connector-mysql": "^0.1.5",
35-
"@azimutt/connector-oracle": "^0.1.3",
35+
"@azimutt/connector-oracle": "^0.1.4",
3636
"@azimutt/connector-postgres": "^0.1.11",
3737
"@azimutt/connector-snowflake": "^0.1.2",
3838
"@azimutt/connector-sqlserver": "^0.1.4",

libs/connector-oracle/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@azimutt/connector-oracle",
33
"description": "Connect to Oracle, extract schema, run analysis and queries",
4-
"version": "0.1.3",
4+
"version": "0.1.4",
55
"license": "MIT",
66
"homepage": "https://azimutt.app",
77
"keywords": [],

libs/connector-oracle/src/oracle.ts

Lines changed: 47 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -112,21 +112,22 @@ const toEntityId = <T extends { TABLE_OWNER: string; TABLE_NAME: string }>(value
112112
const groupByEntity = <T extends { TABLE_OWNER: string; TABLE_NAME: string }>(values: T[]): Record<EntityId, T[]> => groupBy(values, toEntityId)
113113

114114
export type RawDatabase = {
115-
DATABASE: string
116-
VERSION: string
117-
BYTES: number
115+
DATABASE: string | undefined
116+
VERSION: string | undefined
117+
BYTES: number | undefined
118118
}
119119

120120
export const getDatabase = (opts: ConnectorSchemaOpts) => async (conn: Conn): Promise<RawDatabase> => {
121121
// https://docs.oracle.com/en/database/oracle/oracle-database/23/refrn/V-DATABASE.html
122122
// https://docs.oracle.com/en/database/oracle/oracle-database/23/refrn/V-VERSION.html
123123
// https://docs.oracle.com/en/database/oracle/oracle-database/23/refrn/DBA_DATA_FILES.html
124-
const db: RawDatabase = {DATABASE: '', VERSION: '', BYTES: 0}
125-
return conn.query<RawDatabase>(`
126-
SELECT (SELECT NAME FROM V$DATABASE) AS DATABASE
127-
, (SELECT BANNER FROM V$VERSION) AS VERSION
128-
, (SELECT SUM(BYTES) FROM DBA_DATA_FILES) AS BYTES`, [], 'getDatabase'
129-
).then(res => res[0] || db).catch(handleError(`Failed to get database infos`, db, opts))
124+
const DATABASE: string | undefined = await conn.query<{NAME: string}>(`SELECT NAME FROM V$DATABASE`, [], 'getDatabaseName')
125+
.then(res => res[0]?.NAME, handleError(`Failed to get database name`, undefined, {...opts, ignoreErrors: true}))
126+
const VERSION: string | undefined = await conn.query<{VERSION: string}>(`SELECT BANNER AS VERSION FROM V$VERSION FETCH NEXT 1 ROW ONLY`, [], 'getDatabaseVersion')
127+
.then(res => res[0]?.VERSION, handleError(`Failed to get database version`, undefined, {...opts, ignoreErrors: true}))
128+
const BYTES: number | undefined = await conn.query<{BYTES: number}>(`SELECT SUM(BYTES) AS BYTES FROM DBA_DATA_FILES`, [], 'getDatabaseSize')
129+
.then(res => res[0]?.BYTES, handleError(`Failed to get database size`, undefined, {...opts, ignoreErrors: true}))
130+
return {DATABASE, VERSION, BYTES}
130131
}
131132

132133
export type RawBlockSizes = { TABLESPACE_NAME: string, BLOCK_SIZE: number }
@@ -139,6 +140,7 @@ export const getBlockSizes = (opts: ConnectorSchemaOpts) => async (conn: Conn):
139140
.catch(handleError(`Failed to get block sizes`, {}, opts))
140141
}
141142

143+
// used to ignore objects owned by Oracle users (see scopeWhere schemaFilter)
142144
export const getOracleUsers = (opts: ConnectorSchemaOpts) => async (conn: Conn): Promise<string[]> => {
143145
// https://docs.oracle.com/en/database/oracle/oracle-database/23/refrn/ALL_USERS.html
144146
return conn.query<{USERNAME: string}>(`SELECT USERNAME FROM ALL_USERS WHERE ORACLE_MAINTAINED='Y'`, [], 'getOracleUsers')
@@ -430,31 +432,33 @@ export const getIndexes = (opts: ScopeOpts) => async (conn: Conn): Promise<RawIn
430432
// https://docs.oracle.com/en/database/oracle/oracle-database/23/refrn/ALL_INDEXES.html
431433
// https://docs.oracle.com/en/database/oracle/oracle-database/23/refrn/ALL_IND_COLUMNS.html
432434
// `i.INDEX_NAME NOT IN`: ignore indexes from primary keys
433-
return conn.query<RawIndex>(`
434-
SELECT i.TABLESPACE_NAME AS INDEX_TABLESPACE
435-
, i.INDEX_NAME AS INDEX_NAME
436-
, i.INDEX_TYPE AS INDEX_TYPE
437-
, i.TABLE_OWNER AS TABLE_OWNER
438-
, i.TABLE_NAME AS TABLE_NAME
439-
, i.TABLE_TYPE AS TABLE_TYPE
440-
, LISTAGG(c.COLUMN_NAME, ',') WITHIN GROUP (ORDER BY c.COLUMN_POSITION) AS COLUMN_NAMES
441-
, JSON_OBJECTAGG(KEY c.COLUMN_NAME VALUE t.DATA_DEFAULT_VC) AS COLUMN_VALUES
442-
, MIN(i.UNIQUENESS) AS IS_UNIQUE
443-
, MIN(i.DISTINCT_KEYS) AS CARDINALITY
444-
, MIN(i.NUM_ROWS) AS INDEX_ROWS
445-
, MIN(i.LAST_ANALYZED) AS ANALYZED_LAST
446-
, MIN(i.GENERATED) AS GENERATED
447-
, MIN(i.PARTITIONED) AS PARTITIONED
448-
, MIN(i.CONSTRAINT_INDEX) AS IS_CONSTRAINT
449-
, MIN(i.VISIBILITY) AS VISIBILITY
450-
FROM ALL_INDEXES i
451-
JOIN ALL_IND_COLUMNS c ON c.INDEX_OWNER = i.OWNER AND c.INDEX_NAME = i.INDEX_NAME
452-
LEFT JOIN ALL_TAB_COLS t ON t.OWNER = i.OWNER AND t.TABLE_NAME = i.TABLE_NAME AND t.COLUMN_NAME = c.COLUMN_NAME
453-
WHERE i.DROPPED != 'YES'
454-
AND ${scopeWhere({schema: 'i.TABLE_OWNER', entity: 'i.TABLE_NAME'}, opts)}
455-
AND i.INDEX_NAME NOT IN (SELECT co.CONSTRAINT_NAME FROM ALL_CONSTRAINTS co WHERE co.CONSTRAINT_TYPE = 'P' AND ${scopeWhere({schema: 'co.OWNER', entity: 'co.TABLE_NAME'}, opts)})
456-
GROUP BY i.TABLESPACE_NAME, i.INDEX_NAME, i.INDEX_TYPE, i.TABLE_OWNER, i.TABLE_NAME, i.TABLE_TYPE`, [], 'getIndexes'
457-
).catch(handleError(`Failed to get indexes`, [], opts))
435+
const cCols = await getTableColumns('SYS', 'ALL_TAB_COLS', opts)(conn) // check column presence to include them or not
436+
const values = cCols.includes('DATA_DEFAULT_VC') ? 'JSON_OBJECTAGG(KEY c.COLUMN_NAME VALUE t.DATA_DEFAULT_VC)' : "'{}' "
437+
const query =
438+
`SELECT i.TABLESPACE_NAME AS INDEX_TABLESPACE
439+
, i.INDEX_NAME AS INDEX_NAME
440+
, i.INDEX_TYPE AS INDEX_TYPE
441+
, i.TABLE_OWNER AS TABLE_OWNER
442+
, i.TABLE_NAME AS TABLE_NAME
443+
, i.TABLE_TYPE AS TABLE_TYPE
444+
, LISTAGG(c.COLUMN_NAME, ',') WITHIN GROUP (ORDER BY c.COLUMN_POSITION) AS COLUMN_NAMES
445+
, ${values} AS COLUMN_VALUES
446+
, MIN(i.UNIQUENESS) AS IS_UNIQUE
447+
, MIN(i.DISTINCT_KEYS) AS CARDINALITY
448+
, MIN(i.NUM_ROWS) AS INDEX_ROWS
449+
, MIN(i.LAST_ANALYZED) AS ANALYZED_LAST
450+
, MIN(i.GENERATED) AS GENERATED
451+
, MIN(i.PARTITIONED) AS PARTITIONED
452+
, MIN(i.CONSTRAINT_INDEX) AS IS_CONSTRAINT
453+
, MIN(i.VISIBILITY) AS VISIBILITY
454+
FROM ALL_INDEXES i
455+
JOIN ALL_IND_COLUMNS c ON c.INDEX_OWNER = i.OWNER AND c.INDEX_NAME = i.INDEX_NAME
456+
LEFT JOIN ALL_TAB_COLS t ON t.OWNER = i.OWNER AND t.TABLE_NAME = i.TABLE_NAME AND t.COLUMN_NAME = c.COLUMN_NAME
457+
WHERE i.DROPPED != 'YES'
458+
AND ${scopeWhere({schema: 'i.TABLE_OWNER', entity: 'i.TABLE_NAME'}, opts)}
459+
AND i.INDEX_NAME NOT IN (SELECT co.CONSTRAINT_NAME FROM ALL_CONSTRAINTS co WHERE co.CONSTRAINT_TYPE = 'P' AND ${scopeWhere({schema: 'co.OWNER', entity: 'co.TABLE_NAME'}, opts)})
460+
GROUP BY i.TABLESPACE_NAME, i.INDEX_NAME, i.INDEX_TYPE, i.TABLE_OWNER, i.TABLE_NAME, i.TABLE_TYPE`
461+
return conn.query<RawIndex>(query, [], 'getIndexes').catch(handleError(`Failed to get indexes`, [], opts))
458462
}
459463

460464
function buildIndex(blockSize: number, index: RawIndex): Index {
@@ -566,6 +570,7 @@ export const getTypes = (opts: ScopeOpts) => async (conn: Conn): Promise<RawType
566570
// https://docs.oracle.com/en/database/oracle/oracle-database/23/refrn/ALL_TYPES.html
567571
// https://docs.oracle.com/en/database/oracle/oracle-database/23/refrn/ALL_TYPE_ATTRS.html
568572
// https://docs.oracle.com/en/database/oracle/oracle-database/23/refrn/ALL_SOURCE.html
573+
// /!\ LISTAGG functions can product "ORA-01489: result of string concatenation is too long", mostly on DEFINITION column
569574
return conn.query<RawType>(`
570575
SELECT t.OWNER AS TYPE_OWNER
571576
, t.TYPE_NAME AS TYPE_NAME
@@ -580,7 +585,7 @@ export const getTypes = (opts: ScopeOpts) => async (conn: Conn): Promise<RawType
580585
LEFT JOIN ALL_TYPE_ATTRS a ON a.OWNER = t.OWNER AND a.TYPE_NAME = t.TYPE_NAME
581586
WHERE ${scopeWhere({schema: 't.OWNER'}, opts)}
582587
GROUP BY t.OWNER, t.TYPE_NAME, t.TYPECODE, t.ATTRIBUTES`, [], 'getTypes'
583-
).catch(handleError(`Failed to get types`, [], opts))
588+
).catch(handleError(`Failed to get types`, [], {...opts, ignoreErrors: true}))
584589
}
585590

586591
function buildType(t: RawType): Type {
@@ -649,6 +654,11 @@ export const getDistinctValues = (ref: EntityRef, attribute: AttributePath, opts
649654
FROM ${sqlTable}
650655
WHERE ${sqlColumn} IS NOT NULL
651656
ORDER BY value FETCH FIRST ${sampleSize} ROWS ONLY`, [], 'getDistinctValues'
652-
).then(rows => rows.map(row => row.VALUE))
653-
.catch(handleError(`Failed to get distinct values for '${attributeRefToId({...ref, attribute})}'`, [], opts))
657+
).then(rows => rows.map(row => row.VALUE), handleError(`Failed to get distinct values for '${attributeRefToId({...ref, attribute})}'`, [], opts))
658+
}
659+
660+
const getTableColumns = (schema: string | undefined, table: string, opts: ConnectorSchemaOpts) => async (conn: Conn): Promise<string[]> => {
661+
const query = `SELECT COLUMN_NAME AS ATTR FROM ALL_TAB_COLS WHERE TABLE_NAME = :0${schema ? ` AND OWNER = :1` : ''};`
662+
return conn.query<{ ATTR: string }>(query, schema ? [table, schema] : [table], 'getTableColumns')
663+
.then(res => res.map(r => r.ATTR), handleError(`Failed to get table columns`, [], opts))
654664
}

pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)