SQLite 提取 Oracle 表的工具、方法和陷阱

sqlite

简介

SQLite 数据库是一个非常成功且无处不在的软件包,但对更广泛的 IT 社区来说大多是未知的。SQLite 由 Richard Hipp 博士设计和编码,其第三个主要版本为 许多用户 提供服务,这些用户所在的市场领域对软件质量有着严格的要求,而 SQLite 通过符合 DO-178B 航空电子标准满足了这些要求。除了在航空航天和汽车领域的强大影响力外,大多数主要操作系统供应商(包括 Oracle、Microsoft、Apple、Google 和 RedHat)都将 SQLite 作为核心操作系统组件包含在内。

SQLite 有一些怪癖,可能会让来自其他 RDBMS 环境的用户感到困惑。SQLite 被称为“灵活类型”数据库,这与严格执行列数据类型的 Oracle 不同;字符值可以插入到声明为整数的 SQLite 列中而不会出错(尽管如果需要,检查约束 可以加强 SQLite 类型刚性)。虽然允许许多并发进程从 SQLite 数据库读取数据,但任何时候只允许一个进程拥有写入权限(需要并发写入器的应用程序应谨慎对待 SQLite)。没有网络接口,所有连接都通过文件系统建立;SQLite 不实现客户端-服务器模型。没有“时间点恢复”,备份操作基本上类似于 Oracle 7 风格的 ALTER DATAFILE BEGIN BACKUP,它创建整个数据库的事务一致性副本。GRANTREVOKE 在 SQLite 中未实现,它使用文件系统权限进行所有访问控制。没有后台进程,新连接的客户端可能会发现自己被延迟,并负责事务恢复、统计信息收集或其他管理功能,这些功能在“零管理数据库”中会在后台静默执行。有关 SQLite 的一些历史和架构,请参阅 Richard Hipp 博士讨论的 音频视频 记录。

尽管存在这些怪癖,但 SQLite 很可能是一种优于 CSV、XML 甚至 JSON 的数据交换格式,因为它可以包含索引,使接收者能够执行高速 SQL92 查询,而无需任何预处理、许可或激活。SQLite 保守的编码风格和注释旨在造福“尚未出生的未来程序员”,并且磁盘数据库 格式 已被 美国国会图书馆 定义为 长期存储 标准。

本文将介绍三种将 Oracle 数据库表复制到 SQLite 的方法:CSV 交换、ODBC 数据库链接和 PHP 脚本。CSV 交换可以使用现有工具和 POSIX shell 完成,无需任何管理干预,只需一个具有目标数据的可访问帐户即可。unixODBC 方法需要管理员编译、安装和配置 SQLite ODBC 数据源,然后使用 Oracle 的 dg4odbc(Oracle 服务器 ODBC 接口)连接到它们。PHP 脚本方法允许移动 LOB 类型,但可能需要编译器和开发环境来准备它。

将 Oracle 数据库内容提取到 SQLite 在某种程度上是有问题的,因为基本数据类型并不完全对齐。在这里,我将介绍强制对齐并使用适当转换进行提取的工具。

假设用户有权访问 Oracle 数据库和 Oracle 的 sqlplus 命令行工具,并了解其用法(也可以使用 SQLcl)。

SQLite 安全性

SQLite 专注于将数据库源代码编译到自己的应用程序中的开发人员,而系统级工具似乎受到了一些忽视。用于管理数据库的命令行实用程序 sqlite3 在工具集合中为 Linux 提供。最新版本提供了引人注目的新功能,但编译后的二进制文件存在足够多的问题,我建议不要使用它。

  • 该实用程序仅针对 32 位 Linux x86 环境编译,并且依赖于共享库,这带来了两个问题
    • 许多现代 AMD64/x86_64 Linux 发行版默认不包含 32 位 libc 或其他依赖库,需要管理员权限才能安装。
    • 32 位 Linux 允许程序最多使用 3 GB 的内存,当有更多的物理 RAM 可用且可以在 64 位模式下使用时,这可能会不合理地限制排序和缓存。
  • 工具集合中分发的 sqlite3 没有启用任何编译器安全功能,正如 hardening-check 实用程序所报告的那样,该实用程序可以在 EPEL 中找到。

以下是 SQLite 分发的 Linux 工具实用程序的 hardening check 结果

$ hardening-check sqlite3; file sqlite3
sqlite3:
 Position Independent Executable: no, normal executable!
 Stack protected: no, not found!
 Fortify Source functions: no, only unprotected functions found!
 Read-only relocations: no, not found!
 Immediate binding: no, not found!
sqlite3: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 4.3.0, stripped

我建议获取源代码下载(目前为 sqlite-autoconf-3360000.tar.gz),并在具有当前编译器安全控制的现代操作系统上进行准备

CFLAGS='-O3 -D_FORTIFY_SOURCE=2 -fstack-protector-strong -fpic -pie' \
LDFLAGS='-Wl,-z,relro,-z,now -Wl,-z,now' ./configure

假设这样做,所有编译器安全控制都将被启用

$ hardening-check sqlite3; file sqlite3
sqlite3:
 Position Independent Executable: yes
 Stack protected: yes
 Fortify Source functions: yes (some protected functions found)
 Read-only relocations: yes
 Immediate binding: yes
sqlite3: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=34ffa395a5f985b16f6ac2e6c7c3ad5126e05bed, not stripped

SQLite 是基础设施,虽然我们希望它是 坚不可摧的,但它应该遵守现代编译器安全。任何可以利用这些选项的编译器都应该这样做。

DDL

SQLite 允许为表的列定义以下基本 数据亲缘性。组成列的行可以是 NULL 或以下任何类型,但 NUMERIC 除外

  • TEXT - 文本字符串,使用数据库编码(UTF-8、UTF-16BE 或 UTF-16LE)存储。
  • BLOB - 数据 blob,按原样存储。
  • NUMERIC - 通用数字,尝试退化为整数或实数。
  • INTEGER - 有符号整数,根据值的大小以 1、2、3、4、6 或 8 字节存储。
  • REAL - 浮点值,以 8 字节 IEEE-754 格式存储。

这些类型与 Oracle 并不完全匹配。

Oracle 的 NUMBER 类型用于保存整数和实数,具体取决于“数据比例”的使用。Oracle 的 CHARVARCHAR2 映射到 TEXT 类型。DATE 类型在 SQLite 中不受直接支持,我们暂时将其视为 INTEGER

在检查源代码后发现,SQLITE numeric 亲缘性实际上并不存在。仅检查列数据类型的前几个字符;如果无法识别,则将其分配为 numeric 类型。为了演示

$ sqlite3
SQLite version 3.36.0 2021-06-18 18:36:39
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.

sqlite> CREATE TABLE foo(bar razzamataz,ch text,fl real,whl int,mess blob);

sqlite> .dump foo
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE foo(bar razzamataz,ch text,fl real,whl int,mess blob);
COMMIT;

sqlite> create table bar as select * from foo where 1=0;

sqlite> .dump bar
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE bar(
  bar NUM,
  ch TEXT,
  fl REAL,
  whl INT,
  mess
);
COMMIT;

上面的 CREATE TABLE AS SELECT (CTAS) 操作调用了 C 函数 createTableStmt,该函数提取列亲缘性。未知类型“razzamataz”被静默分配为 numeric 类型,而 blob 类型被完全省略。bar 表中的 NUM 关键字也无法识别。这是我在 SQLite 中找到的确定列真实数据亲缘性的最直接方法。

源代码中定义的实际 SQLite 列亲缘性是

** Substring     | Affinity
** --------------------------------
** 'INT'         | SQLITE_AFF_INTEGER
** 'CHAR'        | SQLITE_AFF_TEXT
** 'CLOB'        | SQLITE_AFF_TEXT
** 'TEXT'        | SQLITE_AFF_TEXT
** 'BLOB'        | SQLITE_AFF_BLOB
** 'REAL'        | SQLITE_AFF_REAL
** 'FLOA'        | SQLITE_AFF_REAL
** 'DOUB'        | SQLITE_AFF_REAL
** If none of the substrings in the above table are found,
** SQLITE_AFF_NUMERIC is returned.

Oracle 的二进制类型(LONG、BLOB、RAW 等)只能使用更强大的工具(如 PHP)移动,本文档稍后将对此进行讨论。任何其他奇异的 Oracle 类型都留给读者自行练习。

以下是一个表 DDL(数据定义语言)提取器脚本,用于 Oracle sqlplus,可用于在 SQLite 中创建兼容表

$ cat textract_ddl.sql
set pages 50000 lin 32767 verify off heading off feedback off newpage none
variable own varchar2(128);
variable nam varchar2(128);
execute :own := upper('&1');
execute :nam := upper('&2');
select null, 'create table ' || :nam || ' (' from dual;
select sep, substr(col,1,cl) col, dtype, num,
case when lag(num) over (order by num) = num - 1 then null else '*' end chk from
(
  select case when COLUMN_ID = 1 then null else ',' end sep, COLUMN_NAME col,
                                        'TEXT --' dtype,        COLUMN_ID num,
  (select max(length(COLUMN_NAME)) from ALL_TAB_COLUMNS
    where OWNER = :own and TABLE_NAME = :nam) cl
  from   ALL_TAB_COLUMNS
  where  OWNER      = :own
  and    TABLE_NAME = :nam
  and    DATA_TYPE IN ('CHAR', 'VARCHAR', 'VARCHAR2') --'CLOB', 'NCLOB' ...
  union all
  select case when COLUMN_ID = 1 then null else ',' end sep, COLUMN_NAME col,
         case when data_scale  = 0 then 'INT --'
              when data_scale <> 0 then 'REAL --'
                                   else 'NUM --' end dtype, COLUMN_ID num,
  (select max(length(COLUMN_NAME)) from ALL_TAB_COLUMNS
    where OWNER = :own and TABLE_NAME = :nam) cl
  from   ALL_TAB_COLUMNS
  where  OWNER      = :own
  and    TABLE_NAME = :nam
  and    DATA_TYPE  = 'NUMBER'
  union all
  select case when COLUMN_ID = 1 then null else ',' end sep, COLUMN_NAME col,
                                        'INT --date' dtype,     COLUMN_ID num,
  (select max(length(COLUMN_NAME)) from ALL_TAB_COLUMNS
    where OWNER = :own and TABLE_NAME = :nam) cl
  from   ALL_TAB_COLUMNS
  where  OWNER      = :own
  and    TABLE_NAME = :nam
  and    DATA_TYPE  = 'DATE'
  order by num
)
union all select null, ');', null, null, null from dual;

为了演示使用此脚本时的失败情况,我们可以针对 ALL_VIEWS 视图使用它,该视图通常可以被任何连接的用户看到。此视图包含一个 LONG 列,需要更强大的客户端(例如 PHP)。

尝试在 sqlplusSQLcl 中运行该脚本会导致标记(和跳过)列

SQL> start textract_ddl sys all_views

  create table all_views (
  OWNER 			 TEXT --	     1 *
, VIEW_NAME			 TEXT --	     2
, TEXT_LENGTH			 NUM --		     3
, TYPE_TEXT_LENGTH		 NUM --		     5 *
, TYPE_TEXT			 TEXT --	     6
, OID_TEXT_LENGTH		 NUM --		     7
, OID_TEXT			 TEXT --	     8
, VIEW_TYPE_OWNER		 TEXT --	     9
, VIEW_TYPE			 TEXT --	    10
, SUPERVIEW_NAME		 TEXT --	    11
, EDITIONING_VIEW		 TEXT --	    12
, READ_ONLY			 TEXT --	    13
  );

上面标记的列(编号 1 和 5)指示 LAG 窗口函数在该行上失败。列 1 在任何运行中都将始终被标记,但任何其他标记都指示未处理的数据类型(在本例中为列 4 中的 LONG)。

一个更微妙的挑战是整数值缺少指定的 data_scale。针对以下 ALL_OBJECTS 视图的运行展示了这个问题

SQL> start textract_ddl sys all_objects

  create table all_objects (
  OWNER 			 TEXT --	     1 *
, OBJECT_NAME			 TEXT --	     2
, SUBOBJECT_NAME		 TEXT --	     3
, OBJECT_ID			 NUM --		     4
, DATA_OBJECT_ID		 NUM --		     5
, OBJECT_TYPE			 TEXT --	     6
, CREATED			 INT --date	     7
, LAST_DDL_TIME 		 INT --date	     8
, TIMESTAMP			 TEXT --	     9
, STATUS			 TEXT --	    10
, TEMPORARY			 TEXT --	    11
, GENERATED			 TEXT --	    12
, SECONDARY			 TEXT --	    13
  );

上面的两个 NUM 列实际上包含整数,而不是浮点数。当列在 Oracle 中定义为 NUMBER 但没有精度和小数位数时,通常会出现此问题。查看各种 Oracle 数字类型的一些示例可能会有所帮助

SQL> create table testab(n1 number(5,3), n2 number (5,0), n3 number(*,0), n4 number(5), n5 number);

Table created.

SQL> select column_name, data_type, data_precision, data_scale from user_tab_columns where table_name='TESTAB';

COLUMN_NAME DATA_TYPE DATA_PRECISION DATA_SCALE
----------- --------- -------------- ----------
N1          NUMBER    5              3
N2          NUMBER    5              0
N3          NUMBER                   0
N4          NUMBER    5              0
N5          NUMBER

SQL> insert into testab values(3.141592653589, 2.718281828, 1.414, 22/7, (1+sqrt(5))/2);

1 row created.

SQL> select * from testab;

        N1         N2         N3         N4         N5
---------- ---------- ---------- ---------- ----------
     3.142          3          1          3 1.61803399

SQL> start textract_ddl fishecj testab

  create table testab (
  N1				  REAL --	      1 *
, N2				  INT --	      2
, N3				  INT --	      3
, N4				  INT --	      4
, N5				  NUM --	      5
  );

如果您确定 Oracle number 列包含整数,即使未指定小数位数,也请手动更改列类型。

我们可以使用 SQLite 命令行 shell 创建一个数据库,并按原样添加 ALL_OBJECTS 表,包括 NUM 列(请注意,shell 工具不返回确认消息)

$ sqlite3 orasys.db3
SQLite version 3.36.0 2021-06-18 18:36:39
Enter ".help" for usage hints.
sqlite>   create table all_objects (
   ...>   OWNER          TEXT --     1 *
   ...> , OBJECT_NAME    TEXT --     2
   ...> , SUBOBJECT_NAME TEXT --     3
   ...> , OBJECT_ID      NUM --      4
   ...> , DATA_OBJECT_ID NUM --      5
   ...> , OBJECT_TYPE    TEXT --     6
   ...> , CREATED        INT --date  7
   ...> , LAST_DDL_TIME  INT --date  8
   ...> , TIMESTAMP      TEXT --     9
   ...> , STATUS         TEXT --    10
   ...> , TEMPORARY      TEXT --    11
   ...> , GENERATED      TEXT --    12
   ...> , SECONDARY      TEXT --    13
   ...>   );

对于非常大的数据库,最大化 SQLite 页面大小很有帮助。这将略微减少存储开销,并为其他相同的表产生更小的文件大小

sqlite> pragma page_size=65536;
sqlite> vacuum;

SQLite 还实现了 预写式日志,它具有许多优点,但也引入了限制。如果需要 WAL 模式,请使用 PRAGMA journal_mode=WAL; 命令启用它。

此时,我们假设我们的 SQLite 表现在已在我们的数据库文件中定义,并且已准备好接收从 Oracle 提取的数据。

sqlite> .quit

DML

本节的主要问题是日期和时间的表达。

正如之前 数据亲缘性 讨论中提到的,SQLite 中没有原生日期或时间格式。转换函数 存在于以下 SQLite 数据类型中表示的日期

  • TEXT 作为 ISO8601 字符串 ("YYYY-MM-DD HH:MM:SS.SSS")。
  • REAL 作为儒略日数字,自公元前 4714 年 11 月 24 日格林威治时间中午以来的天数,根据前摄格里高利历。
  • INTEGER 作为 UNIX 纪元时间,自 1970-01-01 00:00:00 UTC 以来的秒数。

虽然上面的 REAL 格式可能最接近 Oracle 的内部表示,但 UNIX 纪元格式更小,并且在 Oracle 中具有更好的描述的转换函数。我们可以使用一个小的 gawk 脚本来确定 32 位 UNIX 纪元时间的限制

$ cat strftime.gawk
#!/bin/gawk -f
BEGIN { for(i=1;i<ARGC;i++) print strftime("%Y/%m/%d %H:%M:%S",ARGV[i]) }

32 位平台上的旧版本 gawk 允许负纪元时间戳,可以追溯到 1901 年 12 月(请注意,这些时间是相对于本地时区表示的,在本例中为 CST)

$ ./strftime.gawk -2147483648 0 2147483647
1901/12/13 14:45:52
1969/12/31 18:00:00
2038/01/18 21:14:07

较新版本的 gawk 不允许负纪元时间戳(奇怪的是,现代 Busybox awk 没有这种限制)

$ ./strftime.gawk -2147483648
gawk: ./strftime.gawk:2: fatal: strftime: second argument less than 0 or too big for time_t

gawk 可以表达并轻松传输到 64 位系统上的 Oracle 的最大时间似乎是

$ ./strftime.gawk 0 253402322399 67767976233554395
1969/12/31 18:00:00
9999/12/31 23:59:59
2147483647/12/31 23:59:52

这种情况主要记录在 2038 年问题 wiki 中。

Oracle 有一个网站,其中包含用于将 Oracle DATE 类型转换为 UNIX 纪元时间的 转换函数。请注意,我已经调整了最小/最大时间,下限调整为上述结果,上限调整为 9999 年的最后一秒,因为这在 64 位 UNIX 纪元时间中是安全的。我在下面提供了一个修改后的转换函数版本,它在溢出时返回最小/最大值,而不是错误

CREATE OR REPLACE
FUNCTION date_to_unixts(oracle_date IN DATE) RETURN NUMBER IS
  unix_epoch DATE := TO_DATE('19700101000000','YYYYMMDDHH24MISS');
  max_date   DATE := TO_DATE('99991231235959','YYYYMMDDHH24MISS');
  min_date   DATE := TO_DATE('19011213194552','YYYYMMDDHH24MISS');
  unix_ts    NUMBER(38,0);
BEGIN
  IF    oracle_date > max_date THEN return(253402322399);
  ELSIF oracle_date < min_date THEN return(-2147483648);
  ELSE  unix_ts := (oracle_date - unix_epoch) / (1/86400);
  END IF;
  RETURN (unix_ts);
END;
/

一旦此函数到位,我们就准备好构建一个 SELECT,它将从 Oracle 表中提取所有目标列,但专门将日期类型转换为 UNIX 纪元格式。为此,需要一个显式调用日期列转换器的 SELECT

$ cat textract_dml.sql
set pages 50000 lin 32767 verify off heading off feedback off newpage none
variable own varchar2(128);
variable nam varchar2(128);
execute :own := upper('&1');
execute :nam := upper('&2');
select null, 'select' from dual;
select sep, substr(col,1,cl) col, com, num from
(
  select decode(COLUMN_ID, 1, '', ',') sep,
         COLUMN_NAME col,
        '--' com, COLUMN_ID num,
  (select max(length(COLUMN_NAME))+16 from ALL_TAB_COLUMNS
    where OWNER = :own and TABLE_NAME = :nam) cl
  from   ALL_TAB_COLUMNS
  where  OWNER      = :own 
  and    TABLE_NAME = :nam
  and    DATA_TYPE IN ('CHAR', 'VARCHAR2', 'NUMBER')
  union all
  select decode(COLUMN_ID, 1, '', ',') sep,
         'date_to_unixts(' || COLUMN_NAME || ')' col,
         '--' com, COLUMN_ID num,
  (select max(length(COLUMN_NAME))+16 from ALL_TAB_COLUMNS
    where OWNER = :own and TABLE_NAME = :nam) cl
  from   ALL_TAB_COLUMNS
  where  OWNER      = :own
  and    TABLE_NAME = :nam
  and    DATA_TYPE  = 'DATE'
  order by num
);
select null, 'from '||:own||'.'||:nam||';', null from dual;

导出 ALL_OBJECTS 的查询如下。

SQL> spool all_objects-dump.sql

SQL> start textract_dml sys all_objects

  select
  OWNER                         --  1
, OBJECT_NAME                   --  2
, SUBOBJECT_NAME                --  3
, OBJECT_ID                     --  4
, DATA_OBJECT_ID                --  5
, OBJECT_TYPE                   --  6
, date_to_unixts(CREATED)       --  7
, date_to_unixts(LAST_DDL_TIME) --  8
, TIMESTAMP                     --  9
, STATUS                        -- 10
, TEMPORARY                     -- 11
, GENERATED                     -- 12
, SECONDARY                     -- 13
  from sys.all_objects;
SQL> spool off

仔细检查 all_objects-dump.sql 文件,并将其调整为精确的 SQLite 导出查询 - 不多也不少。

可能有理由对脚本生成的 SQL 进行调整

  • 如果要将多个表导出到 SQLite,可能希望将它们全部导出为最近某个特定时间点的外观,以便整个表集是“读取一致的”。这可以通过对 SELECT 语句使用 AS OF 子句来完成,并且用户必须对每个表具有“flashback on <table>”权限才能查看历史版本。Oracle 的 UNDO 表空间也必须具有足够的保留期才能重建恢复映像。
  • 任何额外的 WHERE 子句内容以限制行可见性都应在此阶段添加(而不是在传输后从 SQLite 中删除行)。
  • 只有 3.25 之后的 SQLite 版本才具有窗口函数。如果需要窗口函数的结果,并且目标 SQLite 实现上不可用此功能,请在传输之前在 Oracle 中添加它。
  • 数据的任何转换(例如,子字符串函数以修剪 char/varchar 字段中不相关或不需要的数据,特定于目标的数字调整等)最好作为传输的一部分执行,而不是在传输后清理。

编排 - CSV

定义了兼容的 SQLite 数据库,并且查询将正确过滤提取的数据后,现在可以进行数据传输了。

 

本节的方法将需要在 Oracle sqlplus 和 SQLite 命令行 shell 中都设置 CSV 导出/导入模式。此传输将通过“命名管道”(fifo)进行;CSV 文件实际上永远不会存在于磁盘上。此技术通常与 Oracle 的导出/导入工具一起使用。

下面提供了一个 POSIX shell 脚本来执行此传输,该脚本已使用 Debian 的 dash 进行测试

$ cat textract.sh
#!/bin/sh

set -eu             # http://redsymbol.net/articles/unofficial-bash-strict-mode/
#set -euo pipefail  # http://redsymbol.net/articles/unofficial-bash-strict-mode/
IFS=$'\n\t' head='SET HEADING OFF FEEDBACK OFF MARKUP CSV ON' #TNS='/ as sysdba'

#TNS="(description=          \
#       (address=            \
#         (protocol=tcp)     \
#         (host=db.myco.com) \
#         (port=1521))       \
#       (connect_data=       \
#         (sid=orcl)))"

[ -z "${1:-}" ] && { echo "
$(basename "$0" ".sh") - table extractor

-f Destination sqlite file
-n Source oracle database (TNS descriptor)
-s Oracle extraction SQL file
-t Destination sqlite table
-u Oracle username
"; exit; }

while getopts f:n:p:s:t:u: arg
do case "$arg" in
        f) fILE="$OPTARG" ;;
        n)  TNS="$OPTARG" ;;
#       p) pASS="$OPTARG" ;;
        s) sqlX="$OPTARG" ;;
        t) tABL="$OPTARG" ;;
        u) uSER="$OPTARG" ;;
   esac
done

getv () { getVAR="$1"; getVAL="$(eval printf %s '"${'"$getVAR"':-}"')"
  [ -z "$getVAL" ] && { shift; echo "$@"; read getINP
    [ -z "$getINP" ] && return; eval "$getVAR"='"'$getINP'"'; }; return 1; }

getv sqlX "Oracle Extraction SQL filename: " && exit
getv TNS  -n "Source Oracle database: "      && exit
getv fILE -n "Destination sqlite file: "     && exit
getv tABL -n "Destination sqlite table: "    && exit
getv uSER -n "User (default $LOGNAME): "     && uSER="$LOGNAME"

echo "Logging in / executing as $uSER."

DIR="$(mktemp -d "/tmp/$(basename "$0" ".sh")XXXXX")"
FIFO="$DIR/fifo.lst" GET="$DIR/get.sql" PUT="$DIR/put.sql"
mkfifo "$FIFO"

trap 'stty echo; rm -fv "$FIFO" "$GET" "$PUT"; rmdir -v "$DIR"' EXIT

[ -z "${pASS:-}" ] &&
  { stty -echo; getv pASS -n "Password: " && : ; stty echo; echo ''; }

echo "$head
spool $FIFO" > "$GET"
cat "$sqlX" >> "$GET"
 
( sqlplus -silent "$uSER/$pASS@$TNS" "@${GET}" |
  sed -n '0~10000p' ) &

unset pASS
 
echo ".mode csv
.import ${FIFO} ${tABL}" > "$PUT"

sqlite3 "$fILE" ".read $PUT"

wait

该脚本必须具有多个组件才能执行表传输,如果未指定这些组件,则会提示您输入。特别值得注意的是 TNS(透明网络底层);有一些注释示例允许将脚本锁定到特定目标,可以作为本地 sysDBA 执行,也可以使用远程数据库的完整描述符执行 - 如果未设置这些并且缺少 -n 选项,则会提示您输入。

此脚本的某些变体可以在 Windows 上运行。Busybox Win32 POSIX shell 不实现 mkfifo;Cygwin 或 WSL 可能是更好的选择。

脚本的要求和行为最好通过示例导出运行来描述

$ time ./textract.sh -f orasys.db3 -u fishecj \
	-s all_objects-dump.sql -t all_objects
Logging in / executing as fishecj.
Password:
"SCOTT","EMPPK",,10775,20472,"INDEX",1366667998,1369409340,"2013-04-22:21:59:58","VALID","N","N","N"
"TIGER","DEPT",,29780,,"SYNONYM",1371564246,1371564246,"2013-06-18:14:04:06","VALID","N","N","N"
removed ‘/tmp/textractpH3qJ/fifo.lst’
removed ‘/tmp/textractpH3qJ/get.sql’
removed ‘/tmp/textractpH3qJ/put.sql’
rmdir: removing directory, ‘/tmp/textractpH3qJ’

real   0m7.885s
user   0m0.448s
sys    0m0.078s
  • 有一个注释掉的 -p 选项可以在命令行上指定 Oracle 帐户密码;不应使用此选项,因为它将被输入到命令历史记录中,并且可能作为 /proc/*/cmdline 中的参数出现。
  • 脚本运行后,sqlplussqlite 都将通过临时目录中创建的 fifo.lst 进行通信,并且应该在 top 报告中可见为占用 CPU。
  • 每处理 10,000 行 CSV 行,就会在标准输出上打印一行,以指示持续活动;如果不希望这样做,请删除 sed 并将 sqlplus 输出重定向到空设备。为了更彻底的静默,请删除退出 trap 中的 -v 选项。
  • 执行脚本时,date_to_unixts 函数必须可见,可以作为在本地帐户中创建的对象,也可以通过另一个所有者授予的同义词可见。
  • 如果在 bash 或 Korn 下运行,请使用完整的“redsymbol”严格模式。Debian 的 dash POSIX shell 不实现 set -o;如果您的 shell 实现,请启用它。
  • 在调试脚本时,注释掉 trap 触发器很有用。
  • 有一个 SQLite 库将提供等效于 外部表 的功能,作为 CSV 处理的替代方法。

ALL_OBJECTS 的传输应该相对较快(如上记录的 7 秒)。从 Oracle sqlplus 和 SQLite 确认行数和 DATEs

SQL> select count(*) from all_objects;
 
  COUNT(*)
----------
     23699

SQL> select created, last_ddl_time from all_objects where object_name='EMPPK';

CREATED             LAST_DDL_TIME
------------------- -------------------
2013-04-22 21:59:58 2013-05-24 15:29:00

如果我们仔细检查 DATEs,这些 Oracle 结果与 sqlite 并不完全一致

$ sqlite3 orasys.db3
SQLite version 3.36.0 2021-06-18 18:36:39
Enter ".help" for usage hints.

sqlite> select count(*) from all_objects;
23699

sqlite> select * from all_objects where object_name='EMPPK';
SCOTT|EMPPK||10775.0|20472.0|INDEX|1366667998|1369409340|2013-04-22:21:59:58|VALID|N|N|N

sqlite> select datetime(CREATED, 'unixepoch', 'localtime'), datetime(LAST_DDL_TIME, 'unixepoch', 'localtime') from all_objects where object_name='EMPPK';
2013-04-22 16:59:58|2013-05-24 10:29:00

sqlite> select datetime(CREATED, 'unixepoch'), datetime(LAST_DDL_TIME, 'unixepoch') from all_objects where object_name='EMPPK';
2013-04-22 21:59:58|2013-05-24 15:29:00

sqlite> .quit

除非删除 localtime 参数,否则 DATEs 因本地时区而异。

我们可以创建并填充一个小型的 Oracle 表,以确认我们可以超过 2038 年的限制

 

create table birthday (who varchar2(4000), theday date);
insert into birthday values ('joe',  to_date('2030/04/01','YYYY/MM/DD'));
insert into birthday values ('jane', to_date('2040/05/01','YYYY/MM/DD'));
insert into birthday values ('jed',  to_date('2050/06/01','YYYY/MM/DD'));
insert into birthday values ('jill', to_date('9999/12/31','YYYY/MM/DD'));
commit;

在上面的导出过程之后

sqlite> select * from birthday;
joe|1901232000
jane|2219443200
jed|2537654400
jill|253400000000

sqlite> select who, datetime(theday, 'unixepoch', 'localtime') from birthday;
joe|2030-03-31 19:00:00
jane|2040-04-30 18:00:00
jed|2050-05-31 18:00:00
jill|9999-12-05 02:53:20

sqlite> select who, datetime(theday, 'unixepoch') from birthday;
joe|2030-04-01 00:00:00
jane|2040-05-01 00:00:00
jed|2050-06-01 00:00:00
jill|9999-12-05 08:53:20

纪元时间并没有带我们完全到 9,999 年的年底,但已经足够接近了。

dg4odbc

如本节结果所示,上面的 CSV 传输模式比 dg4odbc 可靠得多。直接链接到 SQLite 的数据库链接很有吸引力,因为它们可以保留事务完整性,但该接口很脆弱且容易失败。

在没有有效的 Oracle 支持合同的情况下,不应依赖 dg4odbc 将其用于生产环境中的 SQLite。那些使用没有支持的 Oracle 免费 XE 数据库的人应尽可能避免使用 dg4odbc。ODBC 服务器软件包包含警告:“该驱动程序可用,但可能包含错误。风险自负。”

要继续进行 ODBC 安装,必须安装以下操作系统软件包

# yum install sqlite sqlite-devel unixODBC unixODBC-devel
Loaded plugins: langpacks, ulninfo
Package sqlite-3.7.17-8.el7_7.1.x86_64 already installed and latest version
Package unixODBC-2.3.1-14.0.1.el7.x86_64 already installed and latest version
Resolving Dependencies
--> Running transaction check
---> Package sqlite-devel.x86_64 0:3.7.17-8.el7_7.1 will be installed
---> Package unixODBC-devel.x86_64 0:2.3.1-14.0.1.el7 will be installed
--> Finished Dependency Resolution

Dependencies Resolved

================================================================================
 Package             Arch        Version                  Repository       Size
================================================================================
Installing:
 sqlite-devel        x86_64      3.7.17-8.el7_7.1         ol7_latest      104 k
 unixODBC-devel      x86_64      2.3.1-14.0.1.el7         ol7_latest       54 k

Transaction Summary
================================================================================
Install  2 Packages

Total download size: 158 k
Installed size: 552 k
Is this ok [y/d/N]: y
Downloading packages:
(1/2): unixODBC-devel-2.3.1-14.0.1.el7.x86_64.rpm          |  54 kB   00:00     
(2/2): sqlite-devel-3.7.17-8.el7_7.1.x86_64.rpm            | 104 kB   00:03     
--------------------------------------------------------------------------------
Total                                               47 kB/s | 158 kB  00:03     
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
  Installing : sqlite-devel-3.7.17-8.el7_7.1.x86_64                         1/2 
  Installing : unixODBC-devel-2.3.1-14.0.1.el7.x86_64                       2/2 
  Verifying  : unixODBC-devel-2.3.1-14.0.1.el7.x86_64                       1/2 
  Verifying  : sqlite-devel-3.7.17-8.el7_7.1.x86_64                         2/2 

Installed:
  sqlite-devel.x86_64 0:3.7.17-8.el7_7.1                                        
  unixODBC-devel.x86_64 0:2.3.1-14.0.1.el7                                      

Complete!

SQLite ODBC 包 以源代码形式分发。对于基于 RedHat 的系统,有一个 source RPM,可以轻松构建可安装的软件包。获取它,并使用以下命令构建二进制软件包(这将需要安装的 C 编译器)

rpmbuild --rebuild sqliteodbc-0.9998-1.src.rpm

构建完成后,编译后的软件包可以在以下位置找到

$ ll ~/rpmbuild/RPMS/x86_64/
total 408
-rw-rw-r--. 1 cfisher cfisher 120440 Jul 12 19:08 sqliteodbc-0.9998-1.x86_64.rpm
-rw-rw-r--. 1 cfisher cfisher 294704 Jul 12 19:08 sqliteodbc-debuginfo-0.9998-1.x86_64.rpm

在目标服务器上安装所需的软件包

# rpm -Uvh ~fishecj/rpmbuild/RPMS/x86_64/sqliteodbc-0.9998-1.x86_64.rpm
Preparing...                          ################################# [100%]
Updating / installing...
   1:sqliteodbc-0.9998-1              ################################# [100%]
odbcinst: Driver installed. Usage count increased to 1. 
    Target directory is /etc

查看 README

# rpm -ql sqliteodbc
/usr/lib64/libsqlite3_mod_blobtoxy-0.9998.so
/usr/lib64/libsqlite3_mod_blobtoxy.so
/usr/lib64/libsqlite3_mod_csvtable-0.9998.so
/usr/lib64/libsqlite3_mod_csvtable.so
/usr/lib64/libsqlite3_mod_impexp-0.9998.so
/usr/lib64/libsqlite3_mod_impexp.so
/usr/lib64/libsqlite3_mod_xpath-0.9998.so
/usr/lib64/libsqlite3_mod_xpath.so
/usr/lib64/libsqlite3_mod_zipfile-0.9998.so
/usr/lib64/libsqlite3_mod_zipfile.so
/usr/lib64/libsqlite3odbc-0.9998.so
/usr/lib64/libsqlite3odbc.so
/usr/share/doc/sqliteodbc-0.9998
/usr/share/doc/sqliteodbc-0.9998/ChangeLog
/usr/share/doc/sqliteodbc-0.9998/README
/usr/share/doc/sqliteodbc-0.9998/license.terms

请注意,已添加驱动程序规范

# cat /etc/odbcinst.ini
[ODBC Driver 11 for SQL Server]
Description=Microsoft ODBC Driver 11 for SQL Server
Driver=/opt/microsoft/msodbcsql/lib64/libmsodbcsql-11.0.so.2270.0
Threading=1
UsageCount=1

[SQLITE3]
Description=SQLite ODBC 3.X
Driver=/usr/lib64/libsqlite3odbc.so
Setup=/usr/lib64/libsqlite3odbc.so
Threading=2
FileUsage=1
UsageCount=1

还添加了一个虚假的全局句柄

# tail -2 /etc/odbc.ini
[SQLite3 Datasource]
Driver=SQLITE3

以 root 用户身份,为上一节中创建的特定 SQLite 数据库文件添加驱动程序(请注意,/etc/odbc.ini 对所有用户都是全局的,受文件系统权限限制;可以创建私有 odbc.ini 文件,但此处未涵盖)

echo '

[SQLitec1]
Description=textract PoC
Driver=/usr/lib64/libsqlite3odbc.so
Database=/home/oracle/orasys.db3
# optional lock timeout in milliseconds
Timeout=2000' >> /etc/odbc.ini

使用 ODBC SQL 工具测试驱动程序

# isql SQLitec1
+---------------------------------------+
| Connected!                            |
|                                       |
| sql-statement                         |
| help [tablename]                      |
| quit                                  |
|                                       |
+---------------------------------------+

SQL> select count(*) from all_objects;
+-----------+
| count(*)  |
+-----------+
| 23699     |
+-----------+
SQLRowCount returns 0
1 rows fetched

SQL> select datetime(CREATED, 'unixepoch'), datetime(LAST_DDL_TIME, 'unixepoch') from all_objects where object_name='EMPPK';
+--------------------------------+--------------------------------------+
| datetime(CREATED, 'unixepoch') | datetime(LAST_DDL_TIME, 'unixepoch') |
+--------------------------------+--------------------------------------+
| 2013-04-22 21:59:58            | 2013-05-24 15:29:00                  |
+--------------------------------+--------------------------------------+
SQLRowCount returns 0
1 rows fetched

SQL> quit

安装 ODBC 服务器组件后,我们就可以为特定于 Oracle dg4odbc 客户端的 init.ora 配置连接了。

首先,DBA 必须将 TNS 客户端条目添加到以下文件

$ORACLE_HOME/network/admin/tnsnames.ora

TNS 条目应类似于本地数据库的 SID。确保完全匹配其配置的主机

sqlitec1 = (DESCRIPTION=
  (ADDRESS=
    (PROTOCOL=tcp)
    (HOST=1.2.3.4)
    (PORT=1524))
  (CONNECT_DATA=
    (SID=SQLitec1))
  (HS=OK))

TNS 客户端条目到位后,必须配置数据库监听器以按需启动 ODBC 服务,方法是在以下文件中添加条目

$ORACLE_HOME/network/admin/listener.ora

可以将新服务添加到现有的 SID 列表中

    (SID_DESC=
      (SID_NAME=SQLitec1)
      (ORACLE_HOME=/home/oracle/Ora19)
      (PROGRAM=dg4odbc)
    )

此时,重新启动监听器

lsnrctl restart

希望您的监听器报告新的 ODBC SID

Service "SQLitec1" has 1 instance(s).
  Instance "SQLitec1", status UNKNOWN, has 1 handler(s) for this service...

最后,在以下位置创建一个新的 init.ora 文件,该文件以 dg4odbc 为目标

$ORACLE_HOME/hs/admin/initSQLitec1.ora

此文件应具有以下内容

HS_FDS_CONNECT_INFO = SQLitec1
HS_FDS_TRACE_LEVEL = 4

tnsping 实用程序可以确认监听器知道新的 ODBC 服务

$ $ORACLE_HOME/bin/tnsping sqlitec1

TNS Ping Utility for Linux: Version 19.0.0.0.0 - Production on 09-JUL-2021 12:18:01

Copyright (c) 1997, 2020, Oracle.  All rights reserved.

Attempting to contact (description=(address=(protocol=tcp)(host=1.2.3.4)(port=1521))(connect_data=(sid=SQLitec1)))
OK (0 msec)

DBA 现在可以为连接测试创建数据库链接

$ $ORACLE_HOME/bin/sqlplus '/ as sysdba'

SQL*Plus: Release 19.0.0.0.0 - Production on Thu Jul 8 13:45:19 2021
Version 19.3.0.0.0

Copyright (c) 1982, 2019, Oracle.  All rights reserved.


Connected to:
Oracle Database 19c Enterprise Edition Release 19.0.0.0.0 - Production
Version 19.3.0.0.0

SQL> create database link lite_c1 connect to nouser identified by nopass using 'sqlitec1';

Database link created.

SQL> select count(*) from all_objects@lite_c1;

COUNT(*)
--------
23699

注释

  • 上面虚假的 nouser/nopass 是伪造的凭据;SQLite 不使用基于密码的访问控制,但没有它们连接将失败。
  • 如果完整的 TNS 描述符(包含主机和端口)出现在 using 之后,则可以省略 tnsnames.ora 条目。
  • 如果数据库在 RedHat 8 上运行,则 SELECT 可能会失败,直到将以下行
    HS_FDS_SHAREABLE_NAME=/usr/lib64/libodbc.so
    添加到 hs/admin/initSQLitec1.ora 文件。也可能需要添加以下行
    SET EXTPROC_DLLS=/usr/lib64/libodbc.so:/usr/lib64/libsqlite3odbc.so
    hs/admin/extproc.ora 文件。

现在可以从 Oracle 中对 SQLite 数据库执行基本 DML

SQL> delete from all_objects@lite_c1;

23699 rows deleted.

SQL> commit;

Commit complete.

但是,尝试从 ALL_OBJECTS 复杂视图重新插入...

SQL> insert into all_objects@lite_c1
  select
  OWNER                         --  1
, OBJECT_NAME                   --  2
, SUBOBJECT_NAME                --  3
, OBJECT_ID                     --  4
, DATA_OBJECT_ID                --  5
, OBJECT_TYPE                   --  6
, date_to_unixts(CREATED)       --  7
, date_to_unixts(LAST_DDL_TIME) --  8
, TIMESTAMP                     --  9
, STATUS                        -- 10
, TEMPORARY                     -- 11
, GENERATED                     -- 12
, SECONDARY                     -- 13
  from sys.all_objects;

...即使将许多有问题的行替换为 NULL,也会反复失败

  *
ERROR at line 8:
ORA-02070: database LITE_C1 does not support DECODE in this context
  *
ERROR at line 9:
ORA-02070: database LITE_C1 does not support operator 169 in this context
  *
ERROR at line 10:
ORA-02070: database LITE_C1 does not support operator 169 in this context
  *
ERROR at line 11:
ORA-02070: database LITE_C1 does not support TO_CHAR in this context
  *
ERROR at line 12:
ORA-02070: database LITE_C1 does not support DECODE in this context
  *
ERROR at line 13:
ORA-02070: database LITE_C1 does not support DECODE in this context

从复杂视图插入似乎是数据库链接的真正问题;此活动更适合上一节中的 CSV 传输方法。

原始 DML 似乎可以正常工作

SQL> insert into all_objects@lite_c1 (OWNER,OBJECT_NAME) values ('foo','bar');

1 row created.

SQL> commit;

Commit complete.

Oracle 插入的数据确实出现在 SQLite 中

SQL> select * from all_objects@lite_c1;

OWNER OBJECT_NAME ...
----- ----------- ...
foo   bar         ...

Oracle 还可以报告远程 SQLite 版本(如果将其创建为视图)

sqlite> create view my_version as select sqlite_version();
sqlite> select * from my_version;
3.36.0

Oracle 能够选择此视图

SQL> select * from my_version@lite_c1;

sqlite_version()
----------------
3.7.17

SQL> select version from v$instance;

VERSION
-----------------
19.0.0.0.0

在 RedHat 8 上,shell file 命令能够报告写入数据库的最后一个 SQLite 的版本

$ file /home/oracle/orasys.db3

/home/oracle/orasys.db3: SQLite 3.x database, last written using SQLite version 3036000

ODBC 实例使用旧版本的 SQLite,并且在 ODBC 参与 DML 后,降级是可见的

$ file /home/oracle/orasys.db3

/home/oracle/orasys.db3: SQLite 3.x database, last written using SQLite version 3026000

ODBC 在某些 SQL 中似乎也因 SQLite ODBC 实现中的 回调链接 而失败,并出现错误

ORA-02025: all tables in the SQL statement must be at the remote database

实际上,此 ODBC 接口并不健壮。

PHP 来救援

尽管 ODBC 可能很脆弱,但 PHP 同样可靠。PHP 可以轻松处理 LONG 数据类型,任何时候只有一个客户端进程处于活动状态,并且除了作为 Web 服务器引擎的正常位置外,它还作为 shell 解释器运行。

让我们考虑一下之前包含 LONG 列的 ALL_VIEWS

SQL> desc all_views
 Name					   Null?    Type
 ----------------------------------------- -------- --------------
 OWNER					   NOT NULL VARCHAR2(30)
 VIEW_NAME				   NOT NULL VARCHAR2(30)
 TEXT_LENGTH					    NUMBER
 TEXT						    LONG
 TYPE_TEXT_LENGTH				    NUMBER
 TYPE_TEXT					    VARCHAR2(4000)
 OID_TEXT_LENGTH				    NUMBER
 OID_TEXT					    VARCHAR2(4000)
 VIEW_TYPE_OWNER				    VARCHAR2(30)
 VIEW_TYPE					    VARCHAR2(30)
 SUPERVIEW_NAME 				    VARCHAR2(30)
 EDITIONING_VIEW				    VARCHAR2(1)

我们可以为此表添加一个 TEXT 列来代替

$ sqlite3 orasys.db3 
SQLite version 3.36.0 2021-06-18 18:36:39
Enter ".help" for usage hints.
sqlite>   create table all_views (
   ...>   OWNER            TEXT --  1 *
   ...> , VIEW_NAME        TEXT --  2
   ...> , TEXT_LENGTH      NUM  --  3
   ...> , text text
   ...> , TYPE_TEXT_LENGTH NUM  --  5 *
   ...> , TYPE_TEXT        TEXT --  6
   ...> , OID_TEXT_LENGTH  NUM  --  7
   ...> , OID_TEXT         TEXT --  8
   ...> , VIEW_TYPE_OWNER  TEXT --  9
   ...> , VIEW_TYPE        TEXT -- 10
   ...> , SUPERVIEW_NAME   TEXT -- 11
   ...> , EDITIONING VIEW  TEXT -- 12
   ...>   );

然后,一个简单的脚本可以逐行复制表,使用绑定变量。我在下面提供了两个版本,一个使用原生 PHP SQLite 类,另一个使用 PHP 数据对象 (PDO) 通用抽象类。PDO 版本似乎更清晰一些。

PHP 原生 SQLite
$ cat dump_all_views-sqlite.php 
#!/usr/local/bin/php -f
<?php

$litedb = new SQLite3('orasys.db3');

$conn = oci_connect(getenv('DBUSER'), getenv('DBPASS'), getenv('TNS'));

$curs = oci_parse($conn, <<<'EndOfSQL'
select *
from   all_views
where  owner = upper(:the_owner)
EndOfSQL
);

oci_bind_by_name($curs, ':the_owner', $argv[1], -1);

oci_execute($curs);

$q = 'insert into ALL_VIEWS values (';
for($n = 0; $n < oci_num_fields($curs); $n++) $q .= ($n == 0 ? '?' : ',?');
$q .= ')';

$litedb->exec('BEGIN;');

$insert = $litedb->prepare($q);

while($values =
  oci_fetch_array($curs, OCI_NUM + OCI_RETURN_NULLS + OCI_RETURN_LOBS))
  {
    foreach($values as $num => $val)
      $insert->bindParam($num + 1, $values[$num]);
    $insert->execute();
    $insert->reset();
  }

$litedb->exec('COMMIT;');

oci_free_statement($curs);

oci_close($conn);

?>
PHP PDO SQLite
$ cat dump_all_views-PDO.php
#!/usr/local/bin/php -f
<?php

$litedb = new PDO('sqlite:orasys.db3');

$conn = oci_connect(getenv('DBUSER'), getenv('DBPASS'), getenv('TNS'));

$curs = oci_parse($conn, <<<'EndOfSQL'
select *
from   all_views
where  owner = upper(:the_owner)
EndOfSQL
);

oci_bind_by_name($curs, ':the_owner', $argv[1], -1);

oci_execute($curs);

$sql = 'insert into ALL_VIEWS values (';
for($n = 0; $n < oci_num_fields($curs); $n++) $sql .= ($n == 0 ? '?' : ',?');
$sql .= ')';

$litedb->beginTransaction();

$insert = $litedb->prepare($sql);

while($values =
  oci_fetch_array($curs, OCI_NUM + OCI_RETURN_NULLS + OCI_RETURN_LOBS))
    $insert->execute($values);

$litedb->commit();

oci_free_statement($curs);

oci_close($conn);

?>

绑定变量对于 Oracle 比 SQLite 重要得多。Oracle 有一个“库缓存”,能够识别相同的 SQL 并对其进行“软解析”,这比必须“硬解析”的非缓存 SQL 快得多。除了在高容量 SQL 语句上获得性能提升外,绑定变量还提供了一些针对“SQL 注入”的保护,SQLite 可能也共享这种好处。上面的 textract SQL 脚本已使用绑定变量。

SQLite 绑定变量可以是 Oracle 类型(上面脚本中的 :the_owner)或 Sybase/SQL Server 类型 (?,?,?...)。后者按数字位置绑定,前者按显式名称绑定。SQLite 类从 1 而不是零开始 ? 数字位置。

以下是一个示例执行,它将提取此表(请注意,以这种方式传输密码会使其在 /proc/*/environ 中可见,这不是最佳实践)

$ export DBUSER=fishecj \
  TNS='(description=(address=(protocol=tcp)(host=1.2.3.4)(port=1521))(connect_data=(sid=orcl)))'
$ stty -echo; read DBPASS; stty echo; echo ''

$ DBPASS="$DBPASS" ./dump_all_views.php SYS

事务控制似乎可以提高 SQLite 性能,因为如果删除 beginTransactioncommit,脚本的运行速度会慢得多。

现在可以在 SQLite 中看到 sys 用户的 Oracle ALL_VIEWS 的完整内容

$ sqlite3 orasys.db3 
SQLite version 3.36.0 2021-06-18 18:36:39
Enter ".help" for usage hints.

sqlite> select count(*) from all_views;
3812

sqlite> select text from all_views where view_name='ALL_VIEWS';
select u.name, o.name, v.textlength, v.text, t.typetextlength, t.typetext,
       t.oidtextlength, t.oidtext, t.typeowner, t.typename,
       decode(bitand(v.property, 134217728), 134217728,
              (select sv.name from superobj$ h, "_CURRENT_EDITION_OBJ" sv
              where h.subobj# = o.obj# and h.superobj# = sv.obj#), null),
       decode(bitand(v.property, 32), 32, 'Y', 'N'),
       decode(bitand(v.property, 16384), 16384, 'Y', 'N')
from sys."_CURRENT_EDITION_OBJ" o, sys.view$ v, sys.user$ u, sys.typed_view$ t
where o.obj# = v.obj#
  and o.obj# = t.obj#(+)
  and o.owner# = u.user#
  and (o.owner# = userenv('SCHEMAID')
       or o.obj# in
            (select oa.obj#
             from sys.objauth$ oa
             where oa.grantee# in ( select kzsrorol
                                         from x$kzsro
                                  )
            )
        or /* user has system privileges */
          exists (select null from v$enabledprivs
                  where priv_number in (-45 /* LOCK ANY TABLE */,
                                        -47 /* SELECT ANY TABLE */,
                                        -48 /* INSERT ANY TABLE */,
                                        -49 /* UPDATE ANY TABLE */,
                                        -50 /* DELETE ANY TABLE */)
                  )
      )

结论

一些用户对遗留数据库提出了很高的要求,由于可伸缩性或许可问题,这些要求无法轻易满足(Oracle 数据库零售许可为 每个 CPU 内核 47,500 美元)。SQLite 是一种卸载此类用户的选择,而对这种选择的追求启发了本文。

SQLite 中肯定有一些怪癖,使其与传统的 RDBMS 环境截然不同。极端的语法容忍度、类型流动性和缺乏权限对许多用户来说是一种冲击。

也很容易找到许多从未听说过 SQLite 的 DBA。虽然它通常对 IT 管理“不敏感”,但众所周知,开发人员会(滥用)它,因为他们对符合 SOX 规范的企业数据库感到沮丧。当这些开发人员离开后,秘密数据存储通常不会受到适当的关注。

SQLite 在手机和其他消费电子产品中的普及程度也令人震惊。从这个角度来看,它是一种主要的监视工具,对隐私产生危险的影响。

IT 组织最好熟悉 SQLite,审计员最好了解适当的用途和数据泄露危险。对客户端/服务器 RDBMS 中的访问控制的信任可能会因糟糕的 SQLite 体验而瓦解;希望本文能够防止少数人遇到这种情况。

Charles Fisher 拥有爱荷华大学的电气工程学位,并在一家财富 500 强矿业和制造公司担任系统和数据库管理员。

加载 Disqus 评论