检查供应商交付数据

SQLiteBeginner
立即练习

介绍

MEND & MOSS 是一家家居用品维修店。两家供应商都使用 SKU BOLT-1,但销售的是不同的螺栓。店铺在保存各供应商的商品名称、价格和版本号时,还会保留自己的货架备注。版本号是正整数,数值越大表示版本越新。

在本引导实验中,你将把供应商 CSV 中的六行数据导入暂存表,而不改变库存;随后创建可复用的 SQL 视图,将可用记录与无效记录分开。视图是保存的查询,并不是这些数据行的另一份副本。你只需要掌握基本的终端导航;使用 SQL 和 SQLite 命令前,文中会先作介绍。所有店铺记录均为合成数据。

全程使用 Terminal 标签页。SQLite 和初始数据库已准备好,无需构建网站。请保持原有库存和 deliveries/ 中的两个文件不变。

查看库存并暂存交付数据

本步骤中,你将把一条正常库存记录与其供应商对应起来,然后将当前交付的完整数据导入单独的暂存表。从 Terminal 开始。cd 用于切换目录;如果不小心进入了错误目录,也可以用这个绝对路径返回:

cd /home/labex/project/supplier-desk

ls 列出文件。你应该看到 stock.db 和 deliveries:

ls

数据库是一个真实文件。sqlite3 stock.db 会在 SQLite 的交互式命令行中打开它。下面的 SQL 和点命令都应在 sqlite> 提示符下输入,不要在 Linux shell 提示符下输入:

sqlite3 stock.db

表使用有名称的列存储数据行。SELECT 选择要读取的列,FROM 指定表,ORDER BY 则让显示顺序保持可预期。SQL 语句以 ; 结尾。两个点命令控制显示方式:.headers on 显示列名,.mode column 对齐各列的值。点命令不加分号:

.headers on
.mode column
SELECT supplier,sku,title,price_cents,revision,shelf_note
FROM stock ORDER BY supplier,sku;

库存有三行。North 的 N / BOLT-1 是 Steel bolt,价格为 120 分,版本号为 2,存放在 Bin A。West 的 W / BOLT-1 是 Brass bolt,价格为 180 分,版本号为 1,存放在 Drawer 4。在这家虚构店铺中,分是最小货币单位;120 表示 1.20。本练习用整数分避免小数舍入。N / WASH-2 也必须继续保留。

supplier,sku 这一组合标识一条库存记录。只靠 SKU 无法区分这两种螺栓。SELECT * 表示读取所有列:

SELECT * FROM suppliers ORDER BY code;

预期看到 N / North Parts 和 W / West Workshop。交付数据中的供应商代码必须存在于此表中。

SQLite 针对每个连接单独检查外键关系。请在当前交互式命令行中启用它。此设置不会修改库存数据:

PRAGMA foreign_keys=ON;

使用 .quit 离开 SQLite,返回 Linux shell:

.quit

cat 输出文本文件的内容。读取实际 CSV 文件:

cat deliveries/current.csv

CSV 指以逗号分隔的值。第一行给出五列的名称。文件以文本形式包含价格和版本号,其中有 12kits、未知供应商 Z,以及 SKU 为空的一行。这些是刻意设置的输入问题,并不是让你猜测如何修正。源文件必须保持不变。提供的 CSV 每个数据行恰好有五个字段,支持标准 CSV 引号规则。

重新打开数据库:

sqlite3 stock.db

预先准备的 raw_delivery 表按相同顺序设有五个 TEXT 列。TEXT 会完整保留收到的内容,包括无效数字;直接导入 stock 会把检查和业务更新混在一起。.import --csv 选择 CSV 解析方式,--skip 1 跳过表头:

.import --csv --skip 1 deliveries/current.csv raw_delivery

rowid 是 SQLite 为这个暂存表提供的行标识符。它可以用作交付数据行号,但不是商品标识。显示导入的每一行:

.headers on
.mode column
SELECT rowid AS delivery_line,* FROM raw_delivery ORDER BY rowid;

预期按文件顺序显示第 1–6 行。第 1–3 行是两种螺栓和新增的修补胶带。第 4 行含 12kits,第 5 行的供应商为 Z,第 6 行的 SKU 为空。AS 为显示的 rowid 指定了更清楚的名称 delivery_line。

导入会追加数据行。如果不小心导入了两次,只用 DELETE FROM raw_delivery; 清空暂存表,然后重新导入一次。这是本单元中普通错误的恢复方法;切勿删除 stock 或重置整个 VM:

DELETE FROM raw_delivery;
.import --csv --skip 1 deliveries/current.csv raw_delivery

最后返回 Linux shell:

.quit

暂存好的交付数据是你的第一个成果。库存仍保留最初的三行及其货架备注。

区分可用记录与无效值

本步骤中,你将保存一份检查脚本,并使用它创建的实际视图审查交付数据。从 Terminal 开始,工作目录为 /home/labex/project/supplier-desk。

店铺接受的记录必须来自已知供应商,SKU 不能为空,商品名称不能只包含空格。价格必须由 1–6 个 ASCII 数字组成,并表示一个正数。版本号必须由 1–3 个 ASCII 数字组成,并表示一个正数。允许前导零。这些较小的长度限制是输入契约,并不表示能验证任意数字格式。

CREATE VIEW name AS SELECT ... 将查询保存为一个命名视图。DROP VIEW IF EXISTS 只会在该视图存在时删除保存的查询,不会删除源表。WHERE 保留条件为真的行。BETWEEN 1 AND 6 包含两个端点;NOT BETWEEN 检测范围之外的值。

CASE 表达式选择第一个匹配的原因。WHEN 指定条件,THEN 指定结果;没有匹配项时返回 NULL,此处表示没有错误。IS NULL 检查这种无值状态。OR 组合多个拒绝条件。NOT IN (SELECT code FROM suppliers) 检查供应商是否不在供应商表中。trim 去掉两侧空格,length 计算字符数。

在数字检查中,GLOB '*[^0-9]*' 检测是否存在 ASCII 数字之外的字符:* 匹配任意字符序列,[^0-9] 匹配一个非数字字符。单独的长度检查会拒绝空字符串。CAST(... AS INTEGER) 把文本转换为整数,但只在数字字符和长度检查之后使用。仅靠类型转换并不安全:12kits 这样的文本不能当作 12 接受。

你将创建四个视图。delivery_check 保留原始值并提供原因。typed_delivery 只转换有效行。delivery_errors 列出被拒绝的行号和原因。delivery_ready 提供适合后续更新的值。脚本只删除这些指定名称的视图,因此可以重复执行;它从不删除库存或源数据行。

下面的 Linux 命令将 SQL 保存到 inspect.sql。> 会替换目标文件。<<'SQL' 开始一个按字面保留内容的 here-document:粘贴完整代码块,包括最后独占一行的 SQL。不要在 SQLite 中执行这条 cat 命令。

cat > inspect.sql <<'SQL'
DROP VIEW IF EXISTS delivery_ready;
DROP VIEW IF EXISTS delivery_errors;
DROP VIEW IF EXISTS typed_delivery;
DROP VIEW IF EXISTS delivery_check;
CREATE VIEW delivery_check AS
SELECT rowid AS delivery_line, *,
  CASE
    WHEN supplier NOT IN (SELECT code FROM suppliers) OR supplier IS NULL
      THEN 'unknown supplier'
    WHEN sku IS NULL OR sku = '' THEN 'missing SKU'
    WHEN title IS NULL OR trim(title) = '' THEN 'missing title'
    WHEN price_cents IS NULL OR length(price_cents) NOT BETWEEN 1 AND 6
      OR price_cents GLOB '*[^0-9]*' OR CAST(price_cents AS INTEGER) < 1
      THEN 'invalid price'
    WHEN revision IS NULL OR length(revision) NOT BETWEEN 1 AND 3
      OR revision GLOB '*[^0-9]*' OR CAST(revision AS INTEGER) < 1
      THEN 'invalid revision'
  END AS reason
FROM raw_delivery;
CREATE VIEW typed_delivery AS
SELECT delivery_line, supplier, sku, title,
       CAST(price_cents AS INTEGER) AS price_cents,
       CAST(revision AS INTEGER) AS revision
FROM delivery_check WHERE reason IS NULL;
CREATE VIEW delivery_errors AS
SELECT delivery_line, reason FROM delivery_check WHERE reason IS NOT NULL;
CREATE VIEW delivery_ready AS
SELECT supplier,sku,title,price_cents,revision FROM typed_delivery;
SQL

如果终端一直等待,说明缺少结束标记:单独输入一行 SQL。如果某行输错,重新执行完整代码块以替换脚本即可,无需编辑器或重置环境。

使用 Linux 输入重定向 < 执行脚本。-bail 告诉 SQLite 遇到第一个错误就停止,不再继续后面的语句。成功时预期没有输出:

sqlite3 -bail stock.db < inspect.sql

如果只是文件名或语法错误,先用 ls 查看文件,用步骤 1 中的绝对路径 cd 返回目录,再使用完整代码块修正 inspect.sql 并重新执行。库存尚未被修改。

SQLite 也接受在数据库路径之后提供一条带引号的 SQL 语句。-header -column 为这一次命令选择易读的显示方式。引号让空格和分号保留在同一个 shell 参数中:

sqlite3 -header -column stock.db 'SELECT * FROM delivery_errors ORDER BY delivery_line;'

预期输出:

delivery_line  reason
-------------  ----------------
4              invalid price
5              unknown supplier
6              missing SKU

原因对应一条交付数据行,因此可以把被拒绝的商品追溯到来源。没有任何源值被修正或删除。

sqlite3 -header -column stock.db 'SELECT * FROM delivery_ready ORDER BY supplier,sku;'

预期有三条可接受记录:North 的螺栓为 135 分、版本号 3,修补胶带为 450 分、版本号 1,West 的螺栓为 190 分、版本号 2。两种螺栓的标识仍然彼此独立。typeof 报告 SQLite 中值的实际类型:

sqlite3 -header -column stock.db 'SELECT sku,typeof(price_cents),typeof(revision) FROM delivery_ready ORDER BY supplier,sku;'

两个类型列都应该显示 integer。最后,读取真实库存:

sqlite3 -header -column stock.db 'SELECT * FROM stock ORDER BY supplier,sku;'

它仍保留原始值和货架备注。你已经准备好一套可复用的收货检查,但尚未应用供应商的变更。下一个独立引导实验将从新数据库和一批更大的已暂存交付数据开始。

总结

你通过真实的 SQLite 数据行,建立了供应商标识、交付数据与本地库存之间的联系。你解读了实际接受和拒绝的记录,保留了原始来源,并保存了供下一批交付复用的 SQL。