语义层与 SQL 编译:数据集、视图、转换
30 秒导读: PandasAI v3 不再把 CSV/数据库当成"一堆 DataFrame",而是先让你写一份声明式的数据集定义(schema:有哪些列、来自哪里、要做什么清洗),再由一个 schema→SQL 编译器把它翻译成真正的 SQL。神奇的一点:哪怕数据源是本地 CSV,查询也统一交给 DuckDB 执行——于是"本地文件"和"MySQL 表"在上层看起来完全一样。这一章讲清楚这份 schema 的结构、校验规则,以及它怎么被一步步编成 SQL。
本章聚焦 v3 的数据抽象层与其背后的 SQL 编译器。不涉及 Agent 主循环(那是 01-agent-loop.md),也不涉及"LLM 生成的代码怎么被当 SQL 校验执行"(那是 02-code-as-sql.md)。这里只回答一个问题:一份数据集定义,是怎么变成一条可执行 SQL 的?
1. 这是什么(零基础也能懂)
一句话定义
语义层(semantic layer)= 一份描述"数据长什么样、从哪来、要怎么加工"的声明式配置,PandasAI 把它存成一个目录:一份 schema.yaml(定义)+ 一份 data.parquet(如果是本地数据,连数据一起存)。
解决什么问题 / 给谁用
假设你有一张销售 CSV,想让 AI 反复查询它。你不想每次都重新描述"amount 是金额、要按 category 分组、空值填 0"。语义层让你一次性把这些规则写死在数据集里:
- 列的元信息:每列叫什么、什么类型、什么含义(喂给 LLM 当上下文,也用来生成 SQL)。
- 数据源:是本地 CSV,还是远程 Postgres 表,还是多表拼出来的视图。
- 加工规则:分组、聚合、清洗转换(填空、改大小写、去重……)。
写好之后,不管底层是文件还是数据库,上层拿到的都是一个统一的 DataFrame 对象,查询体验一致。
它能做什么
| 能力 | 说明 |
|---|---|
| 定义本地数据集 | pai.create(path, df=..., columns=...) → 落成 schema.yaml + data.parquet |
| 定义远程 SQL 数据集 | source={"type":"postgres","table":...},不落数据,查询时才连库 |
| 定义视图(view) | 多张已有数据集按 relations join 成一张逻辑表 |
| 声明式转换 | transformations=[{"type":"fill_na",...}],编译成 SQL 表达式而非 pandas 代码 |
| 分组聚合 | group_by + 列的 expression(如 sum(amount)),编成 GROUP BY |
用起来什么样
下面是把一个 DataFrame 落成语义层数据集的最小例子(摘自 pandasai/__init__.py 的 create docstring):
import pandasai as pai
pai.create(
path="my-org/sales",
df=sales_df,
columns=[
{"name": "category", "type": "string"},
{"name": "amount", "type": "float",
"expression": "sum(amount)", "alias": "total_sales"}, # 聚合列
],
transformations=[
{"type": "fill_na", "params": {"column": "amount", "value": 0}},
],
group_by=["category"],
)
# → 落地 datasets/my-org/sales/{schema.yaml, data.parquet}
这一句 create 之后,my-org/sales 就成了一个可复用的数据集;之后 pai.load("my-org/sales") 拿回来的 DataFrame,底层查询已经带上了 fill_na、sum(amount)、group by category。
一句话直觉
把语义层当"数据库里的视图定义 + dbt model":你写的是"我想要什么样的一张表",而不是"怎么一步步算出来"。真正的计算被延迟到查询时,由 SQL 引擎(DuckDB 或远程库)完成。
本节到此不碰底层。下面进入"它大概怎么转"。
2. 顶层全景(它大概怎么转)
整条链路分两半:写入侧(把定义落成文件)和读取侧(把文件编译成 SQL 并执行)。
写入侧: pai.create(df, columns, transformations, group_by)
│ 组装 + pydantic 校验
▼
SemanticLayerSchema ──► schema.yaml (定义)
│ data.parquet (本地数据,可选)
▼
读取侧: DatasetLoader.create_loader_from_schema(schema) ← 工厂,按 source 分派
│
┌────────┼─────────────┬───────────────┐
▼ ▼ ▼ ▼
Local SQL View (每个 loader 自带一个 query_builder)
Loader Loader Loader
│ │ │
▼ ▼ ▼
LocalQuery SqlQuery ViewQuery ← schema→SQL 编译器 (sqlglot)
Builder Builder Builder
│ │ │
└────────┴──────┬──────┘
▼
build_query() → 一条 SQL 字符串
│
┌─────────────┴──────────────┐
▼ ▼
DuckDB 执行 远程连接器执行
(本地 CSV/parquet, (transpile 到目标方言 +
统一走 DuckDB) pandasai_sql 等)
怎么读这张图: 上半是"定义如何落地",下半是"定义如何被编译执行"。关键在中间那层——同一份 schema,按 source 类型分派给不同 loader,但它们共享同一套 SQL 编译器基类。
各部件一句话职责:
| 部件 | 干什么 | 在哪个文件 |
|---|---|---|
SemanticLayerSchema | 数据集的 pydantic 模型 + 全部校验规则 | pandasai/data_loader/semantic_layer_schema.py:270 |
create() | 把 df/columns/transformations 组装成 schema 并落盘 | pandasai/__init__.py:46 |
DatasetLoader | 抽象基类 + 工厂,按 source 分派三种 loader | pandasai/data_loader/loader.py:22 |
LocalDatasetLoader | 本地 CSV/parquet,用 DuckDB 执行 | pandasai/data_loader/local_loader.py:17 |
SQLDatasetLoader | 远程 SQL 库,transpile + 安全校验后交连接器 | pandasai/data_loader/sql_loader.py:19 |
ViewDatasetLoader | 多表视图,先加载依赖数据集再 join | pandasai/data_loader/view_loader.py:21 |
VirtualDataFrame | 惰性 DataFrame,不预加载,查询时才打 SQL | pandasai/dataframe/virtual_dataframe.py:14 |
BaseQueryBuilder | schema→SQL 编译器基类(sqlglot) | pandasai/query_builders/base_query_builder.py:12 |
ViewQueryBuilder | 把多表按 relations 拼成带 join 的子查询 | pandasai/query_builders/view_query_builder.py:16 |
SQLTransformationManager | 把语义层 transformation 翻成 SQL 表达式 | pandasai/query_builders/sql_transformation_manager.py:9 |
SQLParser | 方言转换(transpile)、表名替换 | pandasai/query_builders/sql_parser.py:10 |
3. 核心原理(逐个机制,由浅入深)
3.1 SemanticLayerSchema:pydantic 模型与它的校验
要解决的小问题: 数据集定义可以来自用户手写的 YAML,也可以来自 create() 的参数。无论哪来,进入系统前必须保证结构合法——不能有拼错的类型、矛盾的配置、非法的分组。
思路: 用 pydantic 把每个概念建成一个模型,把"什么算合法"写成校验器(validator)。非法输入在构造对象那一刻就抛错,不会带病往下走。
模型结构一览:
SemanticLayerSchema ← 顶层:一个数据集
├── source: Source ← 数据从哪来(本地 path / 远程 connection+table)
├── view: bool ← 或者:这是多表拼出来的视图
├── columns: [Column] ← 每列:name / type / expression / alias
├── relations: [Relation] ← 视图专用:表间 join 关系 (from → to)
├── transformations: [...] ← 清洗规则,每条 = type + params
├── group_by: [str] ← 分组列
└── order_by / limit / ...
Column 携带的关键字段(semantic_layer_schema.py:46):name、type(必须属于 VALID_COLUMN_TYPES)、expression(聚合表达式,构造时用 sqlglot parse_one 试解析,解析不了就报"Invalid SQL expression")、alias。
顶层的 validate_schema 在对象构造完成后(mode="after")依次跑三道校验(semantic_layer_schema.py:303):
校验一:group_by 规则(聚合与分组必须自洽)
这是数据库里的老规矩:用了 GROUP BY,那么每个 SELECT 列要么在 group_by 里,要么是聚合。_validate_group_by_columns(semantic_layer_schema.py:316)把它写死:
# 摘自 _validate_group_by_columns
if col.expression and col.name in group_by_set:
raise ValueError(...) # 聚合列不能又出现在 group_by
if not col.expression and col.name not in group_by_set:
raise ValueError(...) # 非聚合列必须出现在 group_by
一句话:有 expression = 聚合列,不许进 group_by;没 expression = 普通列,一旦指定了 group_by 就必须进 group_by。这保证编译出的 SQL 不会因为分组不完整而报错。
校验二:view 列必须是 dataset.column 格式
_validate_columns_relations(semantic_layer_schema.py:333)用一个正则区分两种世界:
column_re_check = r"^[a-zA-Z0-9_]+\.[a-zA-Z0-9_]+$" # 形如 orders.amount
- 如果
self.view为真:所有列名、所有 relation 的 from/to 都必须是表.列两段式——因为视图要跨多张表,列名必须带表前缀才不歧义。此外若列里涉及多张表(len(tables)>1),每张表都必须被 relations 覆盖到,否则报No relations provided for ...。 - 如果不是 view(普通表):列名反过来不许带点号,必须是纯
列名单段式。
校验三:source 与 view 互斥
同在 _validate_columns_relations(semantic_layer_schema.py:348):
if self.source and self.view:
raise ValueError("Only one of 'source' or 'view' can be defined.")
if not self.source and not self.view:
raise ValueError("Either 'source' or 'view' must be defined.")
一个数据集要么有实际数据源(source),要么是纯逻辑视图(view),二者必居其一且只能其一。
此外 Source 自己也有校验(semantic_layer_schema.py:228):本地类型(csv/parquet)必须给 path;远程类型必须给 connection + table;类型不在白名单直接报错。
关键细节: 校验分两层——字段级 @field_validator(如列类型白名单)在赋值时触发;对象级 @model_validator(mode="after") 在整个对象组装完后触发(因为 group_by 规则要同时看 columns 和 group_by,必须等两者都在)。
3.2 create():把 df + 定义落成 schema.yaml + data.parquet
要解决的小问题: 用户给的是零散参数(df、columns 列表、transformations 字典);要变成上面那个严格的 schema 对象并存盘。
流程(__init__.py:46 create):
1. 校验 path 格式 → org_name / dataset_name (:141)
2. 若目录已存在同名 schema → 报 "already exists" (:150)
3. 把字典参数解析成模型:
Transformation(**t) / Column(**c) / Relation(**r) (:161-164, 180)
4. 按三种情况组装 schema:
├─ 有 df → 复用 df.schema,覆盖 name/columns/...
│ 再 model_validate,并 df.to_parquet 落数据 (:166-178)
├─ view → 用 relations 构造 view=True 的 schema (:179-188)
└─ source → 用 source 构造远程 schema (:189-196)
5. schema.to_yaml() 写入 schema.yaml (:200)
6. 立刻建 loader 并 load(),返回可用的 DataFrame (:204-206)
有 df 时,数据被 df.to_parquet(...) 真实写盘(__init__.py:178);之后所有查询都读这个 parquet,而不是原始 CSV。这就是为什么本地数据集目录里躺着一个 data.parquet。
关键细节: 落盘用的 name 是下划线格式(transform_dash_to_underscore),但建 loader 前又 sanitize_sql_table_name(__init__.py:204)——因为 name 最终要当 SQL 表名/别名用,得先洗成 SQL 安全的标识符。
3.3 DatasetLoader 工厂:按 source 类型分派三种 loader
要解决的小问题: 同样是"加载一个数据集",本地文件、远程库、视图三者的执行方式完全不同,但调用方不该关心。
思路: 一个工厂方法 create_loader_from_schema(loader.py:39)看 schema 就决定实例化哪个子类:
# 摘自 create_loader_from_schema
if schema.source and schema.source.type in LOCAL_SOURCE_TYPES:
loader = LocalDatasetLoader(schema, dataset_path) # csv / parquet
elif schema.view:
loader = ViewDatasetLoader(schema, dataset_path) # 多表视图
else:
loader = SQLDatasetLoader(schema, dataset_path) # 远程 SQL 库
loader.query_builder.validate_query_builder() # 建完立刻试编译一次
分派逻辑一目了然:
| 条件 | Loader | query_builder | 执行引擎 |
|---|---|---|---|
source.type ∈ {csv, parquet} | LocalDatasetLoader | LocalQueryBuilder | DuckDB |
schema.view == True | ViewDatasetLoader | ViewQueryBuilder | DuckDB 或远程 |
其余(有 source) | SQLDatasetLoader | SqlQueryBuilder | 远程连接器 |
关键细节: 工厂最后一行 validate_query_builder()(loader.py:59)会立刻跑一遍 build_query() 并用 sqlglot 解析——相当于"编译期自检",schema 有问题在加载时就暴露,而不是等真查询时才崩。另有一个 create_loader_from_path(loader.py:63)从磁盘读 schema.yaml 再走同一条工厂。
3.4 VirtualDataFrame:惰性执行
要解决的小问题: 远程库有几亿行,不可能一 load 就全拉进内存。
思路: 远程/视图 loader 的 load() 返回的不是真数据,而是一个 VirtualDataFrame(virtual_dataframe.py:14)——它持有 loader,但不预取任何数据。真正要数据时才打 SQL:
# 摘自 VirtualDataFrame
def head(self):
if self._head is None:
self._head = self._loader.load_head() # 要预览了,才发 LIMIT 查询
return self._head
@property
def rows_count(self) -> int:
return self._loader.get_row_count() # 要行数了,才发 COUNT(*)
def execute_sql_query(self, query: str):
return self._loader.execute_query(query) # Agent 生成的 SQL 从这里执行
对比之下,LocalDatasetLoader.load()(local_loader.py:35)是即时的——它当场执行 build_query() 把 parquet 读成真 DataFrame。差别在于:本地数据量可控,远程/视图则用虚拟表延迟到用时再算。
4. 深入实现:schema 怎么被编成 SQL
4.1 BaseQueryBuilder.build_query:用 sqlglot 拼 SQL
编译器的核心在 build_query(base_query_builder.py:25)。它不拼字符串,而是用 sqlglot 的表达式 API 一块块搭:
# 摘自 build_query
query = select(*self._get_columns()).from_(self._get_table_expression())
if self.schema.group_by:
query = query.group_by(*[normalize_identifiers(col) for col in self.schema.group_by])
if self._check_distinct(): # 有 remove_duplicates 转换 → DISTINCT
query = query.distinct()
if self.schema.order_by:
query = query.order_by(*self.schema.order_by)
if self.schema.limit:
query = query.limit(self.schema.limit)
return query.transform(quote_identifiers).sql(pretty=True)
三个"零件"函数由子类各自实现,这正是多态的用武之地:
| 零件 | Base(基类)默认 | 子类覆盖 |
|---|---|---|
_get_columns() | 遍历列,处理 expression/alias/transformation | View 版还要 alias 化列名 |
_get_table_expression() | 用 schema.name 当表名 | Local 版→read_csv/read_parquet(...);SQL 版→真实表名;View 版→带 join 的子查询 |
结尾 query.transform(quote_identifiers):统一给所有标识符加引号,防止列名撞上 SQL 关键字。
4.2 _get_columns:expression / alias / transformation 三件事
_get_columns(base_query_builder.py:64)是列级编译的关键,它对每一列决定"这列在 SELECT 里长什么样":
# 摘自 _get_columns
for col in self.schema.columns:
if col.expression:
column_expr = col.expression # 聚合列:直接用表达式 sum(amount)
else:
column_expr = normalize_identifiers(col.name).sql() # 普通列:规范化列名
if self.schema.transformations: # 该列若有清洗转换
column_expr = self.transformation_manager.apply_column_transformations(
column_expr, col.name, self.schema.transformations) # 包一层 SQL 函数
col.alias = col.alias or normalize_identifiers(col.name).sql()
if col.alias:
column_expr = f"{column_expr} AS {col.alias}" # 加别名
columns.append(column_expr)
三步流水:expression 决定基础表达式 → transformation 在外面套一层 SQL 函数 → alias 收尾。举例:列 amount 带 fill_na(0) 转换,最终生成 COALESCE(amount, 0) AS amount。
4.3 SQLTransformationManager:转换 → SQL 表达式
思路: 语义层里的每个转换是一个 {type, params},manager 把 type 映射到一个生成 SQL 片段的方法(sql_transformation_manager.py:9)。命名约定是"_<type>"——apply_transformations 用 getattr 找到对应方法逐个套上去(sql_transformation_manager.py:36)。
几个代表(注意:它们层层嵌套,前一个的输出是后一个的输入):
| type | 生成的 SQL | 源码 |
|---|---|---|
fill_na | COALESCE(expr, value) | :57 |
to_lowercase | LOWER(expr) | :83 |
round_numbers | ROUND(expr, decimals) | :91 |
map_values | CASE WHEN expr = 'k' THEN 'v' ... END | :67 |
clip | LEAST(GREATEST(expr, lo), hi) | :168 |
remove_duplicates | DISTINCT expr(也触发 SELECT DISTINCT) | :213 |
apply_column_transformations(sql_transformation_manager.py:272)先用 get_column_transformations 按 params.column 筛出只作用于这一列的转换,再依次套用。所以清洗被编译进 SELECT 表达式,而不是事后跑 pandas——转换发生在 SQL 层,这正是 v3 的核心转变。
4.4 ViewQueryBuilder:多表按 relations 拼成 join 子查询
视图是最复杂的一支。ViewQueryBuilder._get_table_expression(view_query_builder.py:100)负责把"几张独立数据集"拼成一张逻辑表。步骤:
1. 找出第一张表(relations[0].from_ 的表名),
从它的 loader 取子查询做 FROM 基底 (:103-116)
2. 按 relation.to 的目标表分组 join 条件,
同一目标表的多个条件用 AND 合并 (:118-127)
3. 逐个目标表:取其 loader 的子查询,JOIN 上去 (:129-136)
→ 得到一个 "把所有源表拼齐" 的内层子查询
4. 在这个子查询外面再 SELECT 真正要的列,
带上 group_by,包成带 alias 的外层子查询 (:141-148)
关键在 _get_sub_query_from_loader(view_query_builder.py:96):每张依赖表不是直接引用表名,而是把该表自己的 build_query() 当作子查询嵌进来——于是每张源表自身的列定义、转换、别名都先各自编译好,视图只管在上面 join。列名用 sanitize_view_column_name 处理点号(orders.amount → 安全别名),避免 SQL 里的歧义。
依赖数据集从哪来?ViewDatasetLoader 在构造时按 relations 里出现的表名,逐个 create_loader_from_path 把它们加载进来(view_loader.py:50),并检查它们的 source 彼此兼容(全本地,或同一个远程连接)才允许拼视图(view_loader.py:64,底层是 is_compatible_source)。
4.5 方言转换与连接器映射
编译出来的 SQL 是 sqlglot 的"通用"方言,执行前要 transpile 到目标库的方言。SQLParser.transpile_sql_dialect(sql_parser.py:60):
# 摘自 transpile_sql_dialect
placeholder = "___PLACEHOLDER___"
query = query.replace("%s", placeholder) # 先把参数占位符藏起来,别被解析器动
query = parse_one(query)
result = query.sql(dialect=to_dialect, pretty=True) # 输出目标方言
if to_dialect == "duckdb":
return result.replace(placeholder, "?") # DuckDB 用 ? 占位
return result.replace(placeholder, "%s") # 其余用 %s
远程执行时(sql_loader.py:39 execute_query):先 transpile 到 source.type 方言 → 过 is_sql_query_safe 安全校验(见 02-code-as-sql.md)→ 按 source 类型查连接器表 SUPPORTED_SOURCE_CONNECTORS 找到对应包并动态 import(sql_loader.py:65):
| source 类型 | 连接器包 |
|---|---|
mysql / postgres / cockroachdb / sqlserver / oracle | pandasai_sql |
bigquery | pandasai_bigquery |
snowflake | pandasai_snowflake |
databricks | pandasai_databricks |
yahoo_finance | pandasai_yfinance |
映射表定义在 constants.py:23,取回的模块里调 load_from_<type> 函数执行(sql_loader.py:70)。
4.6 为什么本地 CSV 也走 SQL:统一用 DuckDB
这是 v3 最值得记住的设计:本地文件不用 pandas 直接读,而是让 DuckDB 用 SQL 读。
LocalQueryBuilder._get_table_expression(local_query_builder.py:13)不返回表名,而是返回 DuckDB 的文件读取函数:
# 摘自 LocalQueryBuilder._get_table_expression
if source_type == "parquet":
return f"read_parquet('{abspath}')" # FROM read_parquet('/…/data.parquet')
elif source_type == "csv":
return f"read_csv('{abspath}')"
于是本地数据集的 build_query() 生成的 SQL 形如 SELECT ... FROM read_parquet('...'),LocalDatasetLoader.execute_query(local_loader.py:53)把它交给 DuckDBConnectionManager(duck_db_connection_manager.py:8)执行——后者内部还会再 transpile 到 duckdb 方言(duck_db_connection_manager.py:31)。
这样做的回报:
- 一套编译器打天下——group_by、transformation、view join 的所有逻辑写一遍,本地和远程共用,不用为 pandas 和 SQL 各写一份。
- 本地也能被 Agent 生成的 SQL 直接查——Agent 产 出的分析是 SQL,不管数据在文件还是库里都能跑。
- 视图能混合本地表——因为本地表也是"可被 SQL join 的东西",只要都在同一个 DuckDB 会话里。
代价是本地读取多绕了一层 SQL 引擎,但换来了整条管线的统一,值得。
5. 巧妙之处(可借鉴的技术)
- schema 即真理,校验前置。 所有"数据集合不合法"的判断集中在 pydantic 模型里(
semantic_layer_schema.py:303),非法输入在构造那一刻就崩,下游不用再防。 - 编译期自检。 工厂建完 loader 立刻
validate_query_builder()(loader.py:59)跑一次build_query+ sqlglot 解析,把"schema 能不能编成合法 SQL"提前到加载时暴露。 - 用 sqlglot 表达式 API 而非字符串拼接。
select().from_().group_by()(base_query_builder.py:25)天然防注入、可 transform、可跨方言输出,比手拼 SQL 干净得多。 - 视图 = 子查询套子查询。 每张源表先各自
build_query成子查询,视图只在上面 join(view_query_builder.py:96),让"表自身的列/转换/别名"和"表间关系"彻底解耦。 - 占位符藏猫猫。 transpile 前先把
%s换成不会被 SQL 解析器碰的哨兵串,转完再换回目标方言的占位符(sql_parser.py:63),避免参数占位符在方言转换中被误伤。 - 统一 DuckDB 执行本地数据(
local_query_builder.py:22)——用一套 SQL 管线抹平"文件 vs 数据库"的差异,是整章的点睛。
6. 边界与局限
- 转换语义未必 1:1。
SQLTransformationManager生成的 SQL 用的是较通用的函数名(如DATE_FORMAT、CONVERT_TZ、REGEXPsql_transformation_manager.py:98/127/199),不同库对这些函数的支持和行为可能有差异,transpile 也不保证每个都能完美翻译。 - 视图要求源兼容。 拼视图的所有源必须全本地或同一远程连接(
view_loader.py:64);跨不同数据库拼视图不支持,会直接报错。 - group_by 是强约束。 一旦指定 group_by,每个非聚合列都必须列进去(
semantic_layer_schema.py:327),否则构造即失败——比某些 SQL 方言的宽松行为更严。 - 本地数据一旦 create 就固化成 parquet。
create时df.to_parquet(__init__.py:178),之后查的是这份 parquet 快照,不是活的原始文件;源文件变了不会自动反映。 - 连接器是可选依赖。 远程源需要额外装
pandasai_sql[...]等包,没装则在执行时才报ImportError(sql_loader.py:55),不是加载时。
7. 横向对比
同属 ai-agent-reference 的 data-agents:PandasAI 的取舍是**"把数据抽象成声明式 schema,再统一编译成 SQL 交给 DuckDB/远程库执行"**——计算下推到 SQL 引擎,Agent 只负责生成 SQL(见 01-agent-loop.md)和被当 SQL 校验安全执行(见 02-code-as-sql.md)。这与"让 LLM 直接写 pandas 代码在内存里跑"的路线相反:后者灵活但难沙箱、难跨源;PandasAI 用 SQL 换来了统一的执行面和更强的可校验性。
全景与阅读地图见本组 index.md。
8. 代码地图(导航索引)
| 主题 | 文件路径 | 符号名 |
|---|---|---|
| 数据集 pydantic 模型 + 全部校验 | pandasai/data_loader/semantic_layer_schema.py | SemanticLayerSchema、validate_schema |
| group_by 规则校验 | pandasai/data_loader/semantic_layer_schema.py | _validate_group_by_columns |
| view 列格式 / source-view 互斥 | pandasai/data_loader/semantic_layer_schema.py | _validate_columns_relations |
| 列 / 源 / 关系 / 转换模型 | pandasai/data_loader/semantic_layer_schema.py | Column、Source、Relation、Transformation |
| 落盘为 schema.yaml + data.parquet | pandasai/__init__.py | create |
| Loader 工厂(按 source 分派) | pandasai/data_loader/loader.py | DatasetLoader.create_loader_from_schema |
| 本地 loader(DuckDB 执行) | pandasai/data_loader/local_loader.py | LocalDatasetLoader |
| 远程 SQL loader | pandasai/data_loader/sql_loader.py | SQLDatasetLoader.execute_query |
| 视图 loader(加载依赖 + 兼容校验) | pandasai/data_loader/view_loader.py | ViewDatasetLoader |
| 惰性 DataFrame | pandasai/dataframe/virtual_dataframe.py | VirtualDataFrame |
| schema→SQL 编译器基类 | pandasai/query_builders/base_query_builder.py | BaseQueryBuilder.build_query、_get_columns |
| 本地文件表达式(read_csv/parquet) | pandasai/query_builders/local_query_builder.py | LocalQueryBuilder._get_table_expression |
| 远程表名表达式 | pandasai/query_builders/sql_query_builder.py | SqlQueryBuilder |
| 视图 join 编译 | pandasai/query_builders/view_query_builder.py | ViewQueryBuilder._get_table_expression |
| 转换 → SQL 表达式 | pandasai/query_builders/sql_transformation_manager.py | SQLTransformationManager、apply_column_transformations |
| 方言 transpile / 表名替换 | pandasai/query_builders/sql_parser.py | SQLParser.transpile_sql_dialect |
| DuckDB 会话管理 | pandasai/data_loader/duck_db_connection_manager.py | DuckDBConnectionManager |
| 连接器映射 / 类型白名单 | pandasai/constants.py | SUPPORTED_SOURCE_CONNECTORS、LOCAL_SOURCE_TYPES |