diff --git a/docker-compose.yml b/docker-compose.yml index faff0b09e994d3ab78cbd3d6fe6308e7a2e0b62e..96c93899716bf576615ebf191d1436c9e73800f1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -130,6 +130,7 @@ services: ORACLE_PASSWORD: N0tSecret APP_USER: sakila APP_USER_PASSWORD: N0tSecret + DALIBO_SERVICE_NAME: FREEPDB1 ORACLE_SERVICE: SAKILA ORACLE_PFILE: "${ORACLE_PFILE-/pg_migrate/test/docker/oracle-init23.ora}" entrypoint: /pg_migrate/test/docker/oracle-entrypoint.sh @@ -146,10 +147,26 @@ services: ORACLE_PASSWORD: N0tSecret APP_USER: sakila APP_USER_PASSWORD: N0tSecret + DALIBO_SERVICE_NAME: XEPDB1 ORACLE_SERVICE: SAKILA ORACLE_PFILE: "${ORACLE_PFILE-/pg_migrate/test/docker/oracle-init18.ora}" entrypoint: /pg_migrate/test/docker/oracle-entrypoint.sh + oracle11: + image: gvenzl/oracle-xe:11-slim + ports: ["1521:1521"] + working_dir: /pg_migrate/ + volumes: + - .:/pg_migrate/ + - ./test/fixtures/oracle/:/container-entrypoint-initdb.d/ + environment: + ORACLE_PASSWORD: N0tSecret + APP_USER: sakila + APP_USER_PASSWORD: N0tSecret + DALIBO_SERVICE_NAME: XE + ORACLE_SERVICE: SAKILA + entrypoint: /pg_migrate/test/docker/oracle-entrypoint.sh + postgres: image: postgres:17-alpine ports: ["5432:5432"] diff --git a/docs/references/features.md b/docs/references/features.md index 95ff0f3ef1b5d28c430c0837b5d3d655deca06ee..61462eec34f2c5d7e5f0f24bb0999d59e0a2bf2c 100644 --- a/docs/references/features.md +++ b/docs/references/features.md @@ -23,7 +23,7 @@ The following tables and matrices provide information about the progress of each | - | Not applicable | -## Features +## Migration Here are implemented and planified features. @@ -46,6 +46,46 @@ Here are implemented and planified features. [transqlate]: https://gitlab.com/dalibo/transqlate +## Versions + +Here are the implementattion status of the different systems and versions. + + +=== "Oracle" + + | System | Support | + |----------------------------|------------| + | Oracle 18c to 23ai | ๐ŸŸข `FI` | + | Oracle 11g and Oracle 12c | ๐ŸŸ  `PI` | + | Oracle 10g | โšช `NP` | + | Oracle 9i and below | `-` `WONT` | + +=== "MySQL" + + | System | Support | + |----------------------------|------------| + | MySQL 8.4 | ๐ŸŸข `FI` | + | MariaDB | โšช `NP` | + +=== "Other" + + | System | Support | + |----------------------------|------------| + | Microsoft SQL Server | โšช `NP` | + | Sybaseโ„ข SQL Anywhere | โšช `NP` | + | IBM Db2 | โšช `NP` | + +=== "PostgreSQL" + + | System | Support | + |----------------------------|------------| + | PostgreSQL 13 to 18 | ๐ŸŸข `FI` | + + PostgreSQL is supported as a target system. + PostgreSQL is the only target system supported. + Other system are only supported as source of migration. + + ## Compatibility System-specific features will be implemented one system at a time. @@ -61,33 +101,45 @@ PostgreSQL Migrator fully implements tables conversion for limited systems, as it **inspects** the source catalog, **converts** columns data types, and **copies** data to target tables. -| | Oracleโ„ข Database | MySQL | Microsoft SQLโ€ฏServerยฎ | Sybaseโ„ข SQLโ€ฏAnywhere | IBMโ€ฏDb2 | -|-------------|:----------------:|:-----:|:---------------------:|:--------------------:|:-------:| -| Roles | ๐ŸŸ  `PI` | ๐Ÿ”ด `NI` | โšช `NP` | โšช `NP` | โšช `NP` | -| Privileges | ๐Ÿ”ด `NI` | ๐Ÿ”ด `NI` | โšช `NP` | โšช `NP` | โšช `NP` | -| Schemas | ๐ŸŸข `FI` | ๐ŸŸข `FI` | โšช `NP` | โšช `NP` | โšช `NP` | -| Sequences | ๐ŸŸข `FI` | - | โšช `NP` | โšช `NP` | โšช `NP` | -| Tables | ๐ŸŸข `FI` | ๐ŸŸข `FI` | โšช `NP` | โšช `NP` | โšช `NP` | -| Partitions | ๐ŸŸ  `PI` | ๐ŸŸข `FI` | โšช `NP` | โšช `NP` | โšช `NP` | -| Constraints | ๐ŸŸ  `PI` | ๐ŸŸ  `PI` | โšช `NP` | โšช `NP` | โšช `NP` | -| Indexes | ๐ŸŸ  `PI` | ๐ŸŸ  `PI` | โšช `NP` | โšช `NP` | โšช `NP` | -| Views | ๐ŸŸ  `PI` | ๐ŸŸ  `PI` | โšช `NP` | โšช `NP` | โšช `NP` | -| Routines | ๐ŸŸ  `PI` | ๐ŸŸ  `PI` | โšช `NP` | โšช `NP` | โšช `NP` | -| Triggers | ๐ŸŸ  `PI` | ๐ŸŸ  `PI` | โšช `NP` | โšช `NP` | โšช `NP` | - -The following table describes specific SQL extension support. - -| System | Features | Status | -|------------------|--------------------|--------| -| Oracleโ„ข Database | Synonyms | ๐ŸŸ  `PI` | -| | Packages | ๐ŸŸ  `PI` | -| | Types | ๐ŸŸ  `PI` | -| | Database Links | ๐ŸŸ  `PI` | -| | Materialized Views | ๐ŸŸ  `PI` | -| | Schedules | ๐ŸŸ  `PI` | -| MySQL | Autoincrement | ๐ŸŸ  `PI` | -| | Federated Tables | ๐ŸŸ  `PI` | -| | Events | ๐ŸŸ  `PI` | +=== "Oracle" + + | Features | Status | + |--------------------|--------| + | Roles | ๐ŸŸ  `PI` | + | Privileges | ๐Ÿ”ด `NI` | + | Schemas | ๐ŸŸข `FI` | + | Sequences | ๐ŸŸข `FI` | + | Tables | ๐ŸŸข `FI` | + | Partitions | ๐ŸŸ  `PI` | + | Constraints | ๐ŸŸ  `PI` | + | Indexes | ๐ŸŸ  `PI` | + | Views | ๐ŸŸ  `PI` | + | Routines | ๐ŸŸ  `PI` | + | Triggers | ๐ŸŸ  `PI` | + | Synonyms | ๐ŸŸ  `PI` | + | Packages | ๐ŸŸ  `PI` | + | Types | ๐ŸŸ  `PI` | + | Database Links | ๐ŸŸ  `PI` | + | Materialized Views | ๐ŸŸ  `PI` | + | Schedules | ๐ŸŸ  `PI` | + +=== "MySQL" + + | Features | Status | + |--------------------|--------| + | Roles | ๐Ÿ”ด `NI` | + | Privileges | ๐Ÿ”ด `NI` | + | Schemas | ๐ŸŸข `FI` | + | Tables | ๐ŸŸข `FI` | + | Partitions | ๐ŸŸข `FI` | + | Constraints | ๐ŸŸ  `PI` | + | Indexes | ๐ŸŸ  `PI` | + | Views | ๐ŸŸ  `PI` | + | Routines | ๐ŸŸ  `PI` | + | Triggers | ๐ŸŸ  `PI` | + | Autoincrement | ๐ŸŸ  `PI` | + | Federated Tables | ๐ŸŸ  `PI` | + | Events | ๐ŸŸ  `PI` | ## Contribute diff --git a/docs/references/requirements.md b/docs/references/requirements.md index f26f0495c9eea3768af165efd0fcad421a430354..8deb0130c1464ddba9be147d5d0956d2c9875b30 100644 --- a/docs/references/requirements.md +++ b/docs/references/requirements.md @@ -17,23 +17,7 @@ PostgreSQL Migrator refuses to allocate more than 1GB of RAM. ## Versions -=== "Oracle" - - From version 18c to 23ai. - - We plan to support Oracle 12R2. - - Oracle Database 8i is not supported and won't be. - -=== "MySQL" - - Version 8.4. - - MariaDB is not supported yet. - -=== "PostgreSQL" - - From version 13 ot 18. +See [Features](features.md#versions) page for supported versions of source system and PostgreSQL target. ## Privileges diff --git a/internal/catalog/model.go b/internal/catalog/model.go index d6be9493d76ffcbb390e95a024f77797114284df..c5368d4e0c43f219543e6548c26594d166aa3b16 100644 --- a/internal/catalog/model.go +++ b/internal/catalog/model.go @@ -26,6 +26,14 @@ type Model struct { Annotable } +func (mdl Model) After(v string) bool { + return mdl.Metadata.Version > v +} + +func (mdl Model) Before(v string) bool { + return mdl.Metadata.Version < v +} + // CreateSchemas generates CREATE SCHEMA tasks func (mdl Model) CreateSchemas(p dispatch.TaskAdder) { for _, schema := range mdl.Schemas { diff --git a/internal/oracle/metadata.go b/internal/oracle/metadata.go index abe91545f4bc16a9b2f9c6eb572938d36559eb12..fbc8d5be51decd081076aae701264914a055dd51 100644 --- a/internal/oracle/metadata.go +++ b/internal/oracle/metadata.go @@ -24,7 +24,8 @@ func (mdl *model) Version(ctx context.Context) (software, version string, err er return } conn := database.Connection(ctx, database.Source) - err = conn.QueryRowContext(ctx, `SELECT PRODUCT, VERSION_FULL FROM PRODUCT_COMPONENT_VERSION`).Scan(&software, &version) + sql := fetch.Render("version", nil) + err = conn.QueryRowContext(ctx, sql).Scan(&software, &version) if err != nil { err = fmt.Errorf("oracle: %w", err) } diff --git a/internal/oracle/sql/checks.sql b/internal/oracle/sql/checks.sql index d8bc91627bce7f38ef448413040f686cd4e6769a..d72f1bcd9ceb654569b1d0fd60d844c28295323c 100644 --- a/internal/oracle/sql/checks.sql +++ b/internal/oracle/sql/checks.sql @@ -5,7 +5,10 @@ SELECT CON.OWNER, FROM ALL_CONSTRAINTS CON JOIN ALL_TABLES TAB ON TAB.OWNER = CON.OWNER AND TAB.TABLE_NAME = CON.TABLE_NAME - WHERE CON.SEARCH_CONDITION_VC NOT LIKE '"%" IS NOT NULL' + LEFT OUTER JOIN ALL_MVIEWS MV + ON MV.OWNER = TAB.OWNER AND MV.MVIEW_NAME = TAB.TABLE_NAME + WHERE CON.OWNER IN ({{ template "stringarray.sql" .Schemas }}) + AND MV.OWNER IS NULL AND TAB.TEMPORARY = 'N' AND TAB.SECONDARY = 'N' AND TAB.NESTED = 'NO' @@ -14,5 +17,7 @@ SELECT CON.OWNER, AND CON.STATUS = 'ENABLED' AND CON.VALIDATED = 'VALIDATED' AND CON.INVALID IS NULL - AND CON.OWNER IN ({{ template "stringarray.sql" .Schemas }}) +{{- if .After "12" }} + AND CON.SEARCH_CONDITION_VC NOT LIKE '"%" IS NOT NULL' +{{- end }} ORDER BY 1, 2, 3 diff --git a/internal/oracle/sql/columns.sql b/internal/oracle/sql/columns.sql index 354edcc533a966b3bb4b007e28866154a9d93f4b..773abed38423891bc90b6ff42bb9a6651adaf4fe 100644 --- a/internal/oracle/sql/columns.sql +++ b/internal/oracle/sql/columns.sql @@ -30,8 +30,10 @@ SELECT COL.OWNER, COL.DATA_SCALE AS SCALE, CASE WHEN COL.NULLABLE = 'Y' THEN 1 ELSE 0 END AS NULLABLE, COL.DATA_DEFAULT, - CASE WHEN COL.IDENTITY_COLUMN = 'YES' THEN IC.GENERATION_TYPE || ' AS IDENTITY' - WHEN COL.VIRTUAL_COLUMN = 'YES' THEN 'VIRTUAL' + CASE WHEN COL.VIRTUAL_COLUMN = 'YES' THEN 'VIRTUAL' +{{- if .After "12" }} + WHEN COL.IDENTITY_COLUMN = 'YES' THEN IC.GENERATION_TYPE || ' AS IDENTITY' +{{- end }} ELSE 'NEVER' END AS "GENERATED" FROM ALL_TAB_COLS COL @@ -42,10 +44,12 @@ SELECT COL.OWNER, ON COL.OWNER = CC.OWNER AND COL.TABLE_NAME = CC.TABLE_NAME AND COL.COLUMN_NAME = CC.COLUMN_NAME +{{- if .After "12" }} LEFT OUTER JOIN ALL_TAB_IDENTITY_COLS IC ON IC.OWNER = COL.OWNER AND IC.TABLE_NAME = COL.TABLE_NAME AND IC.COLUMN_NAME = COL.COLUMN_NAME +{{- end }} WHERE COL.HIDDEN_COLUMN = 'NO' -- skip system temporary table for materialized views log AND COL.TABLE_NAME NOT LIKE 'RUPD$_%' diff --git a/internal/oracle/sql/dblinks.sql b/internal/oracle/sql/dblinks.sql index b4a52b9d50bd636f34a44cb3286b333639c7b386..a21143ab8b173e0d5a88318e21b460f923e2db79 100644 --- a/internal/oracle/sql/dblinks.sql +++ b/internal/oracle/sql/dblinks.sql @@ -1,5 +1,5 @@ SELECT OWNER, DB_LINK, USERNAME, HOST, - CASE VALID WHEN 'YES' THEN 1 ELSE 0 END AS VALID + {{ if .After "18" }}CASE VALID WHEN 'YES' THEN 1 ELSE 0 END{{ else }}1{{ end }} AS VALID FROM ALL_DB_LINKS WHERE OWNER IN ( 'PUBLIC', diff --git a/internal/oracle/sql/keys.sql b/internal/oracle/sql/keys.sql index f3f4e9b012e0f5c6d2d7c0207c9638474b8885aa..f89619df2f7be193ae868b814edfdcecdc2027bc 100644 --- a/internal/oracle/sql/keys.sql +++ b/internal/oracle/sql/keys.sql @@ -1,26 +1,22 @@ -WITH VIEWS AS ( - SELECT DISTINCT OWNER, MVIEW_NAME AS TABLE_NAME - FROM ALL_MVIEWS - - UNION - - SELECT DISTINCT LOG_OWNER AS OWNER, LOG_TABLE AS TABLE_NAME - FROM ALL_MVIEW_LOGS -), -TABLES AS ( +WITH TABLES AS ( SELECT TAB.OWNER, TAB.TABLE_NAME FROM ALL_TABLES TAB - LEFT JOIN VIEWS ON VIEWS.OWNER = TAB.OWNER AND VIEWS.TABLE_NAME = TAB.TABLE_NAME - WHERE VIEWS.OWNER IS NULL + LEFT OUTER JOIN ALL_MVIEWS MV + ON MV.OWNER = TAB.OWNER AND MV.MVIEW_NAME = TAB.TABLE_NAME + LEFT OUTER JOIN ALL_MVIEW_LOGS MVL + ON MVL.LOG_OWNER = TAB.OWNER AND MVL.LOG_TABLE = TAB.TABLE_NAME + WHERE TAB.OWNER IN ({{ template "stringarray.sql" .Schemas }}) AND TAB.SECONDARY = 'N' AND TAB.NESTED = 'NO' AND TAB.DROPPED = 'NO' + AND MV.OWNER IS NULL + AND MVL.LOG_OWNER IS NULL ) SELECT CON.OWNER, CON.TABLE_NAME, CON.CONSTRAINT_NAME, '' AS "COMMENT", - LISTAGG(COL.COLUMN_NAME, ',') AS "COLUMNS", + LISTAGG(COL.COLUMN_NAME, ',') WITHIN GROUP (ORDER BY COL.POSITION) AS "COLUMNS", CASE WHEN CON.CONSTRAINT_TYPE = 'P' THEN 1 ELSE 0 END AS IS_PRIMARY, CASE WHEN CON.CONSTRAINT_TYPE = 'U' THEN 1 ELSE 0 END AS IS_UNIQUE FROM TABLES TAB @@ -28,8 +24,7 @@ SELECT CON.OWNER, ON TAB.OWNER = CON.OWNER AND TAB.TABLE_NAME = CON.TABLE_NAME JOIN ALL_CONS_COLUMNS COL ON CON.OWNER = COL.OWNER AND CON.TABLE_NAME = COL.TABLE_NAME AND CON.CONSTRAINT_NAME = COL.CONSTRAINT_NAME - WHERE CON.OWNER IN ({{ template "stringarray.sql" .Schemas }}) - AND CON.CONSTRAINT_TYPE IN ('P', 'U') + WHERE CON.CONSTRAINT_TYPE IN ('P', 'U') AND CON.STATUS = 'ENABLED' AND CON.VALIDATED = 'VALIDATED' GROUP BY CON.OWNER, CON.TABLE_NAME, CON.CONSTRAINT_NAME, CON.CONSTRAINT_TYPE diff --git a/internal/oracle/sql/materialized-views-indexes-columns.sql b/internal/oracle/sql/materialized-views-indexes-columns.sql index d267033f996c226c610569802c60f40f07c3765b..2c27c1914f0deed602a76d756ed67e4e2daf056d 100644 --- a/internal/oracle/sql/materialized-views-indexes-columns.sql +++ b/internal/oracle/sql/materialized-views-indexes-columns.sql @@ -2,7 +2,7 @@ SELECT I.OWNER, I.TABLE_NAME, I.INDEX_NAME, C.COLUMN_NAME, E.COLUMN_EXPRESSION AS EXPRESSION, CASE WHEN C.DESCEND = 'DESC' THEN 1 ELSE 0 END AS DESCEND, - COL.COLLATION AS COLLATION + {{ if .After "18" }}COL.COLLATION{{ else }}''{{ end }} AS COLLATION FROM ALL_MVIEWS MV JOIN ALL_INDEXES I ON I.OWNER = MV.OWNER diff --git a/internal/oracle/sql/materialized-views.sql b/internal/oracle/sql/materialized-views.sql index 82f69bc7f3d70c712ecfdbaa9cec98d00def1ca3..9015e5b6997534045d20f94dc55634bbf1878fb4 100644 --- a/internal/oracle/sql/materialized-views.sql +++ b/internal/oracle/sql/materialized-views.sql @@ -12,7 +12,12 @@ WITH MVIEWS AS ( ) SELECT V.OWNER, V.MVIEW_NAME, - REFRESH_MODE, REFRESH_METHOD, J.REPEAT_INTERVAL, + REFRESH_MODE, REFRESH_METHOD, +{{- if .Before "18" }} + J.INTERVAL, +{{- else }} + J.REPEAT_INTERVAL, +{{- end }} MVLONG.QUERY, V.COLUMNS, COM.COMMENTS @@ -23,11 +28,16 @@ SELECT V.OWNER, LEFT OUTER JOIN DBA_RGROUP R ON R.OWNER = V.OWNER AND R.NAME = V.MVIEW_NAME +{{- if .Before "18" }} + LEFT OUTER JOIN ALL_JOBS J + ON J.JOB = R.JOB +{{- else }} LEFT OUTER JOIN ALL_SCHEDULER_JOBS J ON J.OWNER = R.OWNER AND J.JOB_NAME = R.JOB_NAME AND J.ENABLED = 'TRUE' AND J.SYSTEM = 'FALSE' +{{- end }} LEFT OUTER JOIN ALL_MVIEW_COMMENTS COM ON V.OWNER = COM.OWNER AND V.MVIEW_NAME = COM.MVIEW_NAME diff --git a/internal/oracle/sql/roles.sql b/internal/oracle/sql/roles.sql index f9ed9e8940beabef6d610955ac7973e918fdecba..438b4223eea82fd12c80915df295721b6c84afc1 100644 --- a/internal/oracle/sql/roles.sql +++ b/internal/oracle/sql/roles.sql @@ -1,21 +1,37 @@ WITH ROLES AS ( - SELECT ROLE, R.COMMON, AUTHENTICATION_TYPE + SELECT ROLE, +{{- if .After "12" }} + R.COMMON, +{{- end }} + AUTHENTICATION_TYPE FROM DBA_ROLES R +{{- if .After "12" }} WHERE R.ORACLE_MAINTAINED = 'N' +{{- end }} UNION - SELECT USERNAME, U.COMMON, AUTHENTICATION_TYPE + SELECT USERNAME, +{{- if .After "12" }} + U.COMMON, +{{- end }} + AUTHENTICATION_TYPE FROM DBA_USERS U - WHERE U.ORACLE_MAINTAINED = 'N' - AND U.ACCOUNT_STATUS = 'OPEN' + WHERE U.ACCOUNT_STATUS = 'OPEN' +{{- if .After "12" }} + AND U.ORACLE_MAINTAINED = 'N' +{{- end }} ORDER BY 1 ) SELECT R.ROLE AS "ROLE", CASE WHEN LOGINP.GRANTEE IS NOT NULL THEN 1 ELSE 0 END AS "LOGIN", CASE WHEN SUPER.GRANTEE IS NOT NULL THEN 1 ELSE 0 END AS "SUPER", - CASE R."COMMON" WHEN 'YES' THEN 1 ELSE 0 END AS "COMMON", +{{- if .After "12" }} + CASE R."COMMON" WHEN 'YES' THEN 1 ELSE 0 END +{{- else }} + 0 +{{- end }} AS "COMMON", AUTHENTICATION_TYPE FROM ROLES R LEFT OUTER JOIN DBA_SYS_PRIVS LOGINP @@ -24,4 +40,92 @@ SELECT R.ROLE AS "ROLE", LEFT OUTER JOIN DBA_ROLE_PRIVS SUPER ON R.ROLE = SUPER.GRANTEE AND SUPER.GRANTED_ROLE = 'DBA' +{{- if .Before "18" }} + -- poor ORACLE_MAINTAINED substitution. + WHERE ROLE NOT LIKE 'APEX_%' + AND ROLE NOT LIKE 'AQ_%_ROLE' + AND ROLE NOT LIKE 'FLOWS_%' + AND ROLE NOT LIKE 'HS_ADMIN_%' + AND ROLE NOT LIKE 'OEM_%' + AND ROLE NOT LIKE 'XDB_%' + AND ROLE NOT LIKE '%P_FULL_DATABASE' + AND ROLE NOT LIKE '%_CATALOG_ROLE' + AND ROLE NOT IN ( + 'ADM_PARALLEL_EXECUTE_TASK', + 'ANONYMOUS', + 'APPQOSSYS', + 'AUDSYS', + 'AURORA$JIS$UTILITY$', + 'AURORA$ORB$UNAUTHENTICATED', + 'AUTHENTICATEDUSER', + 'CONNECT', + 'CSMIG', + 'CTXSYS', + 'DBA', + 'DBFS_ROLE', + 'DBMS_PRIVILEGE_CAPTURE', + 'DBSFWUSER', + 'DBSNMP', + 'DGPDB_INT', + 'DIP', + 'DMSYS', + 'DVSYS', + 'DVF', + 'EXFSYS', + 'GATHER_SYSTEM_STATISTICS', + 'GSMADMIN_INTERNAL', + 'GSMCATUSER', + 'GSMUSER', + 'GGSHAREDCAP', + 'GGSYS', + 'LBACSYS', + 'LOGSTDBY_ADMINISTRATOR', + 'MDDATA', + 'MDSYS', + 'MGMT_VIEW', + 'MGDSYS', + 'ODM', + 'ODM_MTR', + 'OLAPSYS', + 'ORDDATA', + 'ORDPLUGINS', + 'ORDSYS', + 'ORACLE_OCM', + 'OSE$HTTP$ADMIN', + 'OUTLN', + 'OWBSYS', + 'OWBSYS_AUDIT', + 'OJVMSYS', + 'PERFSTAT', + 'PDBADMIN', + 'PLUSTRACE', + 'RECOVERY_CATALOG_OWNER', + 'REMOTE_SCHEDULER_AGENT', + 'RESOURCE', + 'SCHEDULER_ADMIN', + 'SI_INFORMTN_SCHEMA', + 'SDE', + 'SPATIAL_CSW_ADMIN_USR', + 'SPATIAL_WFS_ADMIN_USR', + 'SQLTXPLAIN', + 'SYSTEM', + 'SYSMAN', + 'SYSRAC', + 'SYS', + 'SYSBACKUP', + 'SYSDG', + 'SYSKM', + 'SYS$UMF', + 'TSMSYS', + 'TRACESRV', + 'VECSYS', + 'WKSYS', + 'WK_PROXY', + 'WK_TEST', + 'WKPROXY', + 'WMSYS', + 'XDB', + 'XS$NULL' + ) +{{- end }} ORDER BY 1 diff --git a/internal/oracle/sql/scheduler.sql b/internal/oracle/sql/scheduler.sql index db5ade28cdfb88005e4f86ea7ed372374816a4b2..1f37737dafce2592552f487bb3d4492e499885e2 100644 --- a/internal/oracle/sql/scheduler.sql +++ b/internal/oracle/sql/scheduler.sql @@ -3,11 +3,15 @@ SELECT OWNER, J.JOB_NAME, JOB_SUBNAME, LAST_START_DATE, REPEAT_INTERVAL, END_DATE FROM ALL_SCHEDULER_JOBS J +{{- if .After "18" }} LEFT OUTER JOIN ALL_REFRESH R ON J.OWNER = R.ROWNER AND J.JOB_NAME = R.JOB_NAME +{{- end }} WHERE OWNER IN ({{ template "stringarray.sql" .Schemas }}) AND SYSTEM = 'FALSE' +{{- if .After "18" }} AND R.RNAME IS NULL +{{- end }} AND J.JOB_NAME NOT LIKE 'DBMS_JOB$_%%' ORDER BY 1, 2 diff --git a/internal/oracle/sql/sequences.sql b/internal/oracle/sql/sequences.sql index 87fd1c8d073bc726ec745f020b2e62758671b47b..ef536abcd719dcbdb9ec92b3116826d5652a3134 100644 --- a/internal/oracle/sql/sequences.sql +++ b/internal/oracle/sql/sequences.sql @@ -7,9 +7,13 @@ SELECT S.SEQUENCE_OWNER, CASE WHEN CYCLE_FLAG = 'Y' THEN 1 ELSE 0 END AS CYCLE_FLAG, CACHE_SIZE FROM ALL_SEQUENCES S +{{- if .After "12" }} LEFT OUTER JOIN ALL_TAB_IDENTITY_COLS IC ON S.SEQUENCE_OWNER = IC.OWNER AND S.SEQUENCE_NAME = IC.SEQUENCE_NAME - WHERE IC.COLUMN_NAME IS NULL - AND S.SEQUENCE_OWNER IN ({{ template "stringarray.sql" .Schemas }}) +{{- end }} + WHERE S.SEQUENCE_OWNER IN ({{ template "stringarray.sql" .Schemas }}) + {{- if .After "12" }} + AND IC.COLUMN_NAME IS NULL + {{- end }} ORDER BY 1, 2 diff --git a/internal/oracle/sql/tables-indexes-columns.sql b/internal/oracle/sql/tables-indexes-columns.sql index 866523e96ba088b99af633e4cdda1c7e75eb8535..d2e82c7af54165f6aab3893246c950f2005f29d4 100644 --- a/internal/oracle/sql/tables-indexes-columns.sql +++ b/internal/oracle/sql/tables-indexes-columns.sql @@ -1,8 +1,12 @@ SELECT I.OWNER, I.TABLE_NAME, I.INDEX_NAME, NVL2(E.COLUMN_EXPRESSION, '', C.COLUMN_NAME), E.COLUMN_EXPRESSION AS EXPRESSION, CASE WHEN C.DESCEND = 'DESC' THEN 1 ELSE 0 END AS DESCEND, - COL.COLLATION AS COLLATION + {{ if .After "18" }}COL.COLLATION{{ else }}''{{ end }} AS COLLATION FROM ALL_INDEXES I + LEFT OUTER JOIN ALL_MVIEWS MV + ON MV.OWNER = I.TABLE_OWNER AND MV.MVIEW_NAME = I.TABLE_NAME + LEFT OUTER JOIN ALL_MVIEW_LOGS MVL + ON MVL.LOG_OWNER = I.TABLE_OWNER AND MVL.LOG_TABLE = I.TABLE_NAME JOIN ALL_IND_COLUMNS C ON I.OWNER = C.INDEX_OWNER AND I.INDEX_NAME = C.INDEX_NAME @@ -23,8 +27,8 @@ SELECT I.OWNER, I.TABLE_NAME, I.INDEX_NAME, NVL2(E.COLUMN_EXPRESSION, '', C.COLU ON E.INDEX_OWNER = I.OWNER AND E.INDEX_NAME = I.INDEX_NAME WHERE I.OWNER IN ({{ template "stringarray.sql" .Schemas }}) - AND (I.OWNER, I.TABLE_NAME) NOT IN (SELECT OWNER, MVIEW_NAME FROM ALL_MVIEWS) - AND (I.OWNER, I.TABLE_NAME) NOT IN (SELECT LOG_OWNER, LOG_TABLE FROM ALL_MVIEW_LOGS) + AND MV.OWNER IS NULL + AND MVL.LOG_OWNER IS NULL AND I.TABLE_NAME NOT LIKE 'RUPD$_%' AND I.TABLE_NAME NOT LIKE 'DR$_%' AND NVL(CONS.CONSTRAINT_TYPE, 'X') NOT IN ('P', 'U') diff --git a/internal/oracle/sql/tables-indexes.sql b/internal/oracle/sql/tables-indexes.sql index 756b05e4e5399a11bcadceac06814537a6784f63..26a6c87d7da014ddda3afe00ccd7558e12cbc98b 100644 --- a/internal/oracle/sql/tables-indexes.sql +++ b/internal/oracle/sql/tables-indexes.sql @@ -4,6 +4,10 @@ SELECT DISTINCT I.TABLE_OWNER, I.TABLE_NAME, I.INDEX_NAME, '' AS "COMMENT", CASE WHEN I.VISIBILITY = 'INVISIBLE' THEN 1 ELSE 0 END AS "INVISIBLE" FROM ALL_INDEXES I + LEFT OUTER JOIN ALL_MVIEWS MV + ON MV.OWNER = I.TABLE_OWNER AND MV.MVIEW_NAME = I.TABLE_NAME + LEFT OUTER JOIN ALL_MVIEW_LOGS MVL + ON MVL.LOG_OWNER = I.TABLE_OWNER AND MVL.LOG_TABLE = I.TABLE_NAME LEFT JOIN ALL_CONSTRAINTS CONS -- search constraint using this index ON CONS.OWNER = I.OWNER AND CONS.TABLE_NAME = I.TABLE_NAME @@ -18,8 +22,8 @@ SELECT DISTINCT I.TABLE_OWNER, I.TABLE_NAME, I.INDEX_NAME, AND ICOL.TABLE_NAME = CCOL.TABLE_NAME AND ICOL.COLUMN_NAME = CCOL.COLUMN_NAME WHERE I.OWNER IN ({{ template "stringarray.sql" .Schemas }}) - AND (I.OWNER, I.TABLE_NAME) NOT IN (SELECT OWNER, MVIEW_NAME FROM ALL_MVIEWS) - AND (I.OWNER, I.TABLE_NAME) NOT IN (SELECT LOG_OWNER, LOG_TABLE FROM ALL_MVIEW_LOGS) + AND MV.OWNER IS NULL + AND MVL.LOG_OWNER IS NULL AND I.TABLE_NAME NOT LIKE 'RUPD$_%' AND I.TABLE_NAME NOT LIKE 'DR$_%' AND NVL(CONS.CONSTRAINT_TYPE, 'X') NOT IN ('P', 'U') diff --git a/internal/oracle/sql/version.sql b/internal/oracle/sql/version.sql new file mode 100644 index 0000000000000000000000000000000000000000..41219801b5204926d62dd9c7a90a410585940b2a --- /dev/null +++ b/internal/oracle/sql/version.sql @@ -0,0 +1,3 @@ +SELECT PRODUCT, VERSION + FROM PRODUCT_COMPONENT_VERSION + WHERE PRODUCT LIKE 'Oracle%' diff --git a/internal/oracle/tables.go b/internal/oracle/tables.go index 2e447cea35d49be50269def35c38aacc576d8f3f..fc53176fdc88dc6e61253b8e100ef0bc59bbf3c1 100644 --- a/internal/oracle/tables.go +++ b/internal/oracle/tables.go @@ -79,13 +79,15 @@ func (mdl *model) inspectTables(ctx context.Context) error { errs.Appendf("columns: %w", err) progress.Update(ctx, progress.Increment(1)) - mdl.Tables, _, err = fetch.CollectAndAssociateContext( - tctx, - "identities", mdl, - mdl.Tables, - catalog.RowToIdentity, - ) - errs.Appendf("identities: %w", err) + if mdl.After("12") { + mdl.Tables, _, err = fetch.CollectAndAssociateContext( + tctx, + "identities", mdl, + mdl.Tables, + catalog.RowToIdentity, + ) + errs.Appendf("identities: %w", err) + } progress.Update(ctx, progress.Increment(1)) }, func(ctx context.Context, _ chan error) { diff --git a/test/cli/oracle.bats b/test/cli/oracle.bats index b98b5e56e094dfe2d6c65e548f64c5aa78414ed8..baf222e67cd66c100638c9d0fea2121a366c861f 100644 --- a/test/cli/oracle.bats +++ b/test/cli/oracle.bats @@ -179,7 +179,7 @@ setup_file() { @test "convert" { run jq -er ".Tables | length" "$target" - assert_output "27" + assert_output "28" run jq -er '.["sakila"]' "$PGMDIRECTORY/.pg_migrate/Map.json" assert_output "SAKILA" @@ -197,8 +197,8 @@ setup_file() { # Checking boolean conversion configured in init test. run psql -tA -c "SELECT first_name FROM sakila.customer WHERE is_active ORDER BY customer_id LIMIT 1;" assert_output "MARY" - run psql -tA -c "SELECT last_value FROM xtra.tab_id_seq" - assert_output "1358" + run psql -tA -c "SELECT last_value FROM xtra.tab_id_id_seq" + assert_output "1338" run psql -tA -c "SELECT last_value FROM sakila.film_sequence" assert_output "22" } diff --git a/test/docker/oracle-entrypoint.sh b/test/docker/oracle-entrypoint.sh index 972c716d309177ad51793b895622456afe0c0314..4548328fc900965cc4076cb56fa19e32f9abcccc 100755 --- a/test/docker/oracle-entrypoint.sh +++ b/test/docker/oracle-entrypoint.sh @@ -6,5 +6,5 @@ if [ -n "${ORACLE_PFILE-}" ] ; then sed -i "s/startup;/startup pfile=\${ORACLE_PFILE};/" /opt/oracle/container-entrypoint.sh fi -cd /opt/oracle +cd "${ORACLE_BASE}" exec ./container-entrypoint.sh diff --git a/test/fixtures/oracle/00-add-service-name.sh b/test/fixtures/oracle/00-add-service-name.sh index c70459230f26d100a719cfd89197ccbb79878261..ef7f97cf683c52114c7ae43ea4b9ff71ca5bf4b0 100644 --- a/test/fixtures/oracle/00-add-service-name.sh +++ b/test/fixtures/oracle/00-add-service-name.sh @@ -8,13 +8,18 @@ fi echo -e "\nCONTAINER: Creating ${ORACLE_SERVICE} service." sqlplus -s /nolog << EOF - -- Exit on any errors WHENEVER SQLERROR EXIT SQL.SQLCODE - CONNECT sys/${ORACLE_PASSWORD}@localhost:1521/FREEPDB1 AS SYSDBA + CONNECT sys/${ORACLE_PASSWORD}@localhost:1521/${DALIBO_SERVICE_NAME} AS SYSDBA EXEC dbms_service.create_service(service_name => '${ORACLE_SERVICE}', network_name => '${ORACLE_SERVICE}'); EXEC dbms_service.start_service(service_name => '${ORACLE_SERVICE}'); - ALTER PLUGGABLE DATABASE SAVE STATE; - exit; + + CREATE OR REPLACE TRIGGER START_SERVICE_${ORACLE_SERVICE} + AFTER STARTUP ON DATABASE + BEGIN + DBMS_SERVICE.START_SERVICE(SERVICE_NAME => '${ORACLE_SERVICE}'); + END; + / + EXIT EOF echo "CONTAINER: DONE: Creating ${ORACLE_SERVICE} service." diff --git a/test/fixtures/oracle/02-sakila-load-data.sh b/test/fixtures/oracle/02-sakila-load-data.sh old mode 100755 new mode 100644 index c4ee501f713578c0d98d23d67d821f4533454d94..52d0a9229d47e05c34ccf2e13f8b39dbbc480ec6 --- a/test/fixtures/oracle/02-sakila-load-data.sh +++ b/test/fixtures/oracle/02-sakila-load-data.sh @@ -7,7 +7,7 @@ ORACLE_HOST="localhost" ORACLE_PORT="1521" INIT_DIR="/container-entrypoint-initdb.d/files" -ORA_DIR="/opt/oracle" +ORA_DIR="${ORACLE_BASE}" export NLS_DATE_FORMAT='yyyy-mm-dd hh24:mi:ss ' diff --git a/test/fixtures/oracle/90-roles.sql b/test/fixtures/oracle/90-roles.sql index 0d363ac609cedf504b351436da29e96f2a243628..5d64919a43b934b91e65ae18c3540403ac767581 100644 --- a/test/fixtures/oracle/90-roles.sql +++ b/test/fixtures/oracle/90-roles.sql @@ -1,9 +1,11 @@ WHENEVER SQLERROR EXIT SQL.SQLCODE CONNECT sys/N0tSecret@localhost:1521/sakila AS SYSDBA +START '/container-entrypoint-initdb.d/exit11.sql' + CREATE ROLE "ALICE" IDENTIFIED BY "N0tSecret" / -CREATE USER SEBASTIEN +CREATE USER SEBASTIEN IDENTIFIED BY "N0tSecret" / GRANT CREATE SESSION TO SEBASTIEN / diff --git a/test/fixtures/oracle/91-extra-relations.sql b/test/fixtures/oracle/91-extra-relations.sql index a8c43f691ac3b29ab85e1d65cf5bab6f2f1cb6fc..2f7f2e8639ba2c7334abab9cd63843f3daba5144 100644 --- a/test/fixtures/oracle/91-extra-relations.sql +++ b/test/fixtures/oracle/91-extra-relations.sql @@ -8,7 +8,7 @@ CREATE PUBLIC SYNONYM SYN_SAKILA_ACTOR_PUB FOR SAKILA.ACTOR / CREATE TABLE TAB ( - ID NUMBER(8, 2) GENERATED BY DEFAULT ON NULL AS IDENTITY (START WITH 1337) NOT NULL PRIMARY KEY, + ID NUMBER(8, 2) NOT NULL PRIMARY KEY, CREATED DATE NOT NULL, FMT VARCHAR2(10) DEFAULT 'YYYY', YEAR_GENERATED AS (EXTRACT(YEAR FROM CREATED)) VIRTUAL, @@ -17,7 +17,7 @@ CREATE TABLE TAB ( ) / -INSERT INTO TAB(CREATED, TXT) VALUES (SYSDATE, 'Nร”T-UTF-8') +INSERT INTO TAB(ID, CREATED, TXT) VALUES (1, SYSDATE, 'Nร”T-UTF-8') / CREATE TABLE TAB_PRECISION_MISMATCH ( @@ -46,7 +46,6 @@ CREATE TABLE TAB_NUMBER ( / CREATE TABLE TAB_DATES ( - ID NUMBER GENERATED AS IDENTITY NOT NULL PRIMARY KEY, T TIMESTAMP(6), TZ TIMESTAMP WITH TIME ZONE, TZ_LOCAL TIMESTAMP WITH LOCAL TIME ZONE, @@ -76,7 +75,6 @@ END; / CREATE TABLE TAB_RAW ( - ID NUMBER GENERATED AS IDENTITY NOT NULL PRIMARY KEY, RAW16 RAW(16), RAW32 RAW(32), VLONGR LONG RAW, @@ -90,51 +88,6 @@ INSERT INTO TAB_RAW(VLONGR) VALUES (NULL) -- NULL row INSERT INTO TAB_RAW(VLONGR) VALUES (UTL_RAW.CAST_TO_RAW('raw data')) / -CREATE TABLE TAB_PART_RANGE ( - ID NUMBER(10) NOT NULL PRIMARY KEY, - YEAR NUMBER, - MONTH NUMBER -) -PARTITION BY RANGE (YEAR, MONTH) -( - PARTITION P_PAST VALUES LESS THAN (2019, 1), - PARTITION P_2019 VALUES LESS THAN (2020, 1), - PARTITION P_2020 VALUES LESS THAN (2021, 1), - PARTITION P_CURRENT VALUES LESS THAN (MAXVALUE, MAXVALUE) -) -/ - -INSERT INTO TAB_PART_RANGE (ID, YEAR, MONTH) VALUES (1, 2019, 1) -/ - -CREATE TABLE TAB_PART_REF ( - ID NUMBER NOT NULL PRIMARY KEY, - CONSTRAINT ID_FK FOREIGN KEY (ID) REFERENCES TAB_PART_RANGE(ID) -) -PARTITION BY REFERENCE (ID_FK) -/ - -CREATE TABLE TAB_PART_SYSTEM (ID NUMBER) -PARTITION BY SYSTEM -PARTITIONS 4 -/ - -CREATE TABLE TAB_PART_LIST_SUB ( - ID NUMBER(10) NOT NULL, - REGION VARCHAR2(20) NOT NULL -) -PARTITION BY LIST (REGION) -SUBPARTITION BY HASH (ID) ( - PARTITION P_EAST VALUES ('EAST', 'WEST') SUBPARTITIONS 4, - PARTITION P_NORTH VALUES ('NORTH') SUBPARTITIONS 2, - PARTITION P_SOUTH VALUES ('SOUTH') SUBPARTITIONS 2, - PARTITION P_DEFAULT VALUES (DEFAULT) SUBPARTITIONS 1 -) -/ - -INSERT INTO TAB_PART_LIST_SUB (ID, REGION) VALUES (1, 'EAST') -/ - CREATE GLOBAL TEMPORARY TABLE TAB_TMP ( ID NUMBER(10) NOT NULL ) @@ -153,19 +106,12 @@ ALTER TABLE "TAB" / WHENEVER SQLERROR EXIT SQL.SQLCODE -CREATE INDEX IDX_INVISIBLE ON TAB(ID) INVISIBLE +CREATE INDEX IDX_INVISIBLE ON TAB(TXT) INVISIBLE / CREATE INDEX IDX_FUNCTIONNAL ON TAB( TRUNC(CREATED, FMT) ) / --- Create an unsupported bitmap index. -CREATE BITMAP INDEX IDX_TAB_BITMAP_YESNO ON TAB("YESNO") -/ - -CREATE MATERIALIZED VIEW LOG ON TAB -/ - CREATE VIEW "VMixedCase" AS SELECT 1 AS "PRIMARY" FROM DUAL / @@ -185,11 +131,6 @@ CREATE MATERIALIZED VIEW MV_NEVER AS SELECT TRUNC(SYSDATE, 'YY'||'YY') AS Y FROM DUAL / -CREATE MATERIALIZED VIEW MV_FAST - REFRESH FAST ON COMMIT - AS SELECT ID FROM TAB -/ - CREATE MATERIALIZED VIEW MV_NEXT REFRESH COMPLETE WITH ROWID @@ -206,7 +147,3 @@ CONNECT sys/N0tSecret@localhost:1521/sakila AS SYSDBA EXEC DBMS_STATS.GATHER_TABLE_STATS('XTRA', 'TAB', CASCADE => TRUE); EXEC DBMS_STATS.GATHER_TABLE_STATS('XTRA', 'TAB_PRECISION_MISMATCH'); EXEC DBMS_STATS.GATHER_TABLE_STATS('XTRA', 'TAB_NUMBER'); -EXEC DBMS_STATS.GATHER_TABLE_STATS('XTRA', 'TAB_PART_RANGE'); -EXEC DBMS_STATS.GATHER_TABLE_STATS('XTRA', 'TAB_PART_REF'); -EXEC DBMS_STATS.GATHER_TABLE_STATS('XTRA', 'TAB_PART_LIST_SUB'); -EXEC DBMS_STATS.GATHER_TABLE_STATS('XTRA', 'TAB_PART_SYSTEM'); diff --git a/test/fixtures/oracle/92-extra-relations-ora18.sql b/test/fixtures/oracle/92-extra-relations-ora18.sql new file mode 100644 index 0000000000000000000000000000000000000000..3919109c9e2651b78b676f1367c35083bd16c297 --- /dev/null +++ b/test/fixtures/oracle/92-extra-relations-ora18.sql @@ -0,0 +1,75 @@ +WHENEVER SQLERROR EXIT SQL.SQLCODE +CONNECT xtra/N0tSecret@localhost:1521/sakila + +START '/container-entrypoint-initdb.d/exit11.sql' + +CREATE TABLE TAB_ID ( + ID NUMBER GENERATED BY DEFAULT AS IDENTITY (START WITH 1337) PRIMARY KEY +) +/ + +-- Oracle 11g XE docker image has partionning disabled +CREATE TABLE TAB_PART_RANGE ( + ID NUMBER(10) NOT NULL PRIMARY KEY, + YEAR NUMBER, + MONTH NUMBER +) +PARTITION BY RANGE (YEAR, MONTH) +( + PARTITION P_PAST VALUES LESS THAN (2019, 1), + PARTITION P_2019 VALUES LESS THAN (2020, 1), + PARTITION P_2020 VALUES LESS THAN (2021, 1), + PARTITION P_CURRENT VALUES LESS THAN (MAXVALUE, MAXVALUE) +) +/ + +INSERT INTO TAB_PART_RANGE (ID, YEAR, MONTH) VALUES (1, 2019, 1) +/ + +CREATE TABLE TAB_PART_REF ( + ID NUMBER NOT NULL PRIMARY KEY, + CONSTRAINT ID_FK FOREIGN KEY (ID) REFERENCES TAB_PART_RANGE(ID) +) +PARTITION BY REFERENCE (ID_FK) +/ + +CREATE TABLE TAB_PART_SYSTEM (ID NUMBER) +PARTITION BY SYSTEM +PARTITIONS 4 +/ + +CREATE TABLE TAB_PART_LIST_SUB ( + ID NUMBER(10) NOT NULL, + REGION VARCHAR2(20) NOT NULL +) +PARTITION BY LIST (REGION) +SUBPARTITION BY HASH (ID) ( + PARTITION P_EAST VALUES ('EAST', 'WEST') SUBPARTITIONS 4, + PARTITION P_NORTH VALUES ('NORTH') SUBPARTITIONS 2, + PARTITION P_SOUTH VALUES ('SOUTH') SUBPARTITIONS 2, + PARTITION P_DEFAULT VALUES (DEFAULT) SUBPARTITIONS 1 +) +/ + +INSERT INTO TAB_PART_LIST_SUB (ID, REGION) VALUES (1, 'EAST') +/ + +-- Create an unsupported bitmap index. +CREATE BITMAP INDEX IDX_BITMAP ON TAB_TEXT(V2) +/ + +CREATE MATERIALIZED VIEW LOG ON TAB +/ + +CREATE MATERIALIZED VIEW MV_FAST + REFRESH FAST ON COMMIT + AS SELECT ID FROM TAB +/ + +CONNECT sys/N0tSecret@localhost:1521/sakila AS SYSDBA + +EXEC DBMS_STATS.GATHER_TABLE_STATS('XTRA', 'TAB_ID'); +EXEC DBMS_STATS.GATHER_TABLE_STATS('XTRA', 'TAB_PART_RANGE'); +EXEC DBMS_STATS.GATHER_TABLE_STATS('XTRA', 'TAB_PART_REF'); +EXEC DBMS_STATS.GATHER_TABLE_STATS('XTRA', 'TAB_PART_LIST_SUB'); +EXEC DBMS_STATS.GATHER_TABLE_STATS('XTRA', 'TAB_PART_SYSTEM'); diff --git a/test/fixtures/oracle/exit11.sql b/test/fixtures/oracle/exit11.sql new file mode 100644 index 0000000000000000000000000000000000000000..a6d531b063d608de97fe630fb06f2c1b99e51b69 --- /dev/null +++ b/test/fixtures/oracle/exit11.sql @@ -0,0 +1,13 @@ +-- EXIT SQLplus for Oracle 11 +SET TERMOUT OFF + +-- Use a SELECT CASE to set a file to execute. +COLUMN exit_file NEW_VALUE exit_file NOPRINT +SELECT CASE WHEN &_O_RELEASE LIKE '11%' THEN '/container-entrypoint-initdb.d/exitsqlplus.sqllib' + ELSE '/dev/null' + END AS exit_file FROM DUAL; +/ + +-- Unconditionnaly execute the file. +START &exit_file +SET TERMOUT ON diff --git a/test/fixtures/oracle/exitsqlplus.sqllib b/test/fixtures/oracle/exitsqlplus.sqllib new file mode 100644 index 0000000000000000000000000000000000000000..961e4659e7da9c1cf0192d8dc2be74c1973bef66 --- /dev/null +++ b/test/fixtures/oracle/exitsqlplus.sqllib @@ -0,0 +1 @@ +EXIT 0