Skip to content

Commit 417e78b

Browse files
committed
add sqlx-nullable
1 parent 9f6ea96 commit 417e78b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+5151
-2
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ members = [
1010
"sqlx-mysql",
1111
"sqlx-postgres",
1212
"sqlx-sqlite",
13+
"sqlx-nullable",
1314
"examples/mysql/todos",
1415
"examples/postgres/axum-social-with-tests",
1516
"examples/postgres/chat",
@@ -133,6 +134,8 @@ sqlx-mysql = { version = "=0.8.2", path = "sqlx-mysql" }
133134
sqlx-postgres = { version = "=0.8.2", path = "sqlx-postgres" }
134135
sqlx-sqlite = { version = "=0.8.2", path = "sqlx-sqlite" }
135136

137+
sqlx-nullable = { version = "=0.8.2", path = "sqlx-nullable"}
138+
136139
# Facade crate (for reference from sqlx-cli)
137140
sqlx = { version = "=0.8.2", path = ".", default-features = false }
138141

sqlx-core/src/ext/ustr.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ impl UStr {
2525
UStr::Shared(s) => s.strip_prefix(prefix).map(|s| Self::Shared(s.into())),
2626
}
2727
}
28+
29+
pub fn as_str(&self) -> &str {
30+
match self {
31+
UStr::Static(s) => s,
32+
UStr::Shared(s) => s
33+
}
34+
}
2835
}
2936

3037
impl Deref for UStr {

sqlx-nullable/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
target/
2+

sqlx-nullable/Cargo.lock

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

sqlx-nullable/Cargo.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
name = "sqlx-nullable"
3+
version.workspace = true
4+
license.workspace = true
5+
edition.workspace = true
6+
authors.workspace = true
7+
repository.workspace = true
8+
9+
[lib]
10+
name = "sqlx_nullable"
11+
path = "src/lib.rs"
12+
13+
[dependencies]
14+
sqlparser = "0.51"
15+
anyhow = "1"

sqlx-nullable/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#### Implemented
2+
- [x] Joins
3+
- [x] Left, Right, Inner, Outer, Cross join
4+
- [x] On, Using, Natural
5+
- [x] Hardcoded values
6+
- [x] Raw values
7+
- [x] Parameters
8+
- [x] With as
9+
- [x] Returning
10+
- [x] Update
11+
- [x] Delete
12+
- [ ] Functions
13+
- [x] Builtin functions
14+
- [ ] Custom functions
15+
- [ ] ...

sqlx-nullable/src/context.rs

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
use std::collections::HashSet;
2+
3+
use anyhow::{anyhow, Context as _};
4+
use sqlparser::ast::{Expr, Ident, TableFactor, TableWithJoins, With};
5+
6+
use crate::{
7+
expr::visit_expr,
8+
nullable::{Nullable, NullableResult},
9+
source::Source,
10+
wal::{Wal, WalEntry},
11+
SqlFlavour, Table, TableColumn, TableId, Tables,
12+
};
13+
14+
pub struct Context {
15+
pub tables: Tables,
16+
pub source: Source,
17+
pub wal: Wal,
18+
pub flavour: SqlFlavour,
19+
}
20+
21+
impl Context {
22+
pub fn new(tables: Tables, source: Source, wal: Wal, flavour: SqlFlavour) -> Context {
23+
Self {
24+
tables,
25+
source,
26+
wal,
27+
flavour,
28+
}
29+
}
30+
31+
pub fn add_active_tables(&mut self, table: &TableWithJoins) -> anyhow::Result<()> {
32+
self.visit_table_factor(&&table.relation)?;
33+
for join_table in &table.joins {
34+
self.visit_table_factor(&&join_table.relation)?;
35+
}
36+
Ok(())
37+
}
38+
39+
pub fn visit_table_factor(&mut self, table: &TableFactor) -> anyhow::Result<()> {
40+
match table {
41+
TableFactor::Table { name, alias, .. } => {
42+
let mut table = self
43+
.source
44+
.find_by_original_name(&name.0)
45+
.context(format!("could not find table by original name: {name:?}"))?;
46+
table.add_alias(alias);
47+
self.push(table);
48+
Ok(())
49+
}
50+
TableFactor::Derived {
51+
lateral: _,
52+
subquery,
53+
alias,
54+
} => {
55+
let nullables = self.nullable_for(subquery)?;
56+
// dbg!(&nullables);
57+
let mut table = nullables.flatten(); //.to_table(alias);
58+
//
59+
if let Some(alias) = alias {
60+
for (col, col_name) in table.iter_mut().zip(alias.columns.clone()) {
61+
col.column_name = Some(col_name);
62+
}
63+
}
64+
// dbg!(&table);
65+
self.push(table.to_table(alias));
66+
Ok(())
67+
}
68+
TableFactor::UNNEST {
69+
alias,
70+
array_exprs,
71+
with_offset: _,
72+
with_offset_alias: _,
73+
with_ordinality: _,
74+
} => {
75+
let mut table = Table::new(None);
76+
77+
let nullable = {
78+
let results: Vec<_> = array_exprs
79+
.iter()
80+
.map(|expr| visit_expr(expr, None, self))
81+
.flatten()
82+
.collect();
83+
84+
Nullable::new(results).nullable_index(0).unwrap_or(true)
85+
};
86+
87+
if let Some(table_alias) = alias {
88+
table = table.push_column(&table_alias.name.value, nullable)
89+
} else {
90+
table = table.push_column("unnest", nullable)
91+
};
92+
self.push(table);
93+
Ok(())
94+
}
95+
rest => unimplemented!("{rest:#?}"),
96+
}
97+
}
98+
99+
pub fn find_table_by_table_factor(&self, factor: &TableFactor) -> Option<Table> {
100+
match &factor {
101+
TableFactor::Table { name, alias, .. } => {
102+
if let Some(alias) = alias {
103+
return self
104+
.tables
105+
.0
106+
.iter()
107+
.find(|t| t.table_name.as_deref() == Some(&[alias.name.clone()]))
108+
.cloned();
109+
}
110+
self.tables.find_table_by_idents_table(&name.0).cloned()
111+
}
112+
_ => None,
113+
}
114+
}
115+
116+
pub fn recursive_find_joined_tables(&self, expr: &Expr, tables: &mut HashSet<Table>) {
117+
match expr {
118+
Expr::CompoundIdentifier(idents) => {
119+
let table = self.tables.find_col_by_idents(&idents).unwrap();
120+
121+
tables.insert(table.1.clone());
122+
}
123+
Expr::BinaryOp { left, op: _, right } => {
124+
self.recursive_find_joined_tables(&left, tables);
125+
self.recursive_find_joined_tables(&right, tables);
126+
}
127+
Expr::Subscript { expr, subscript: _ } => {
128+
self.recursive_find_joined_tables(expr, tables)
129+
}
130+
Expr::Value(_) => (),
131+
others => unimplemented!("{others:?}"),
132+
}
133+
}
134+
135+
pub fn add_with(&mut self, with: &With) -> anyhow::Result<()> {
136+
for cte in &with.cte_tables {
137+
let _ = self.nullable_for(cte)?;
138+
}
139+
Ok(())
140+
}
141+
142+
pub fn iter_tables(&self) -> impl Iterator<Item = &Table> {
143+
self.tables.0.iter()
144+
}
145+
146+
pub fn find_table_by_idents_table(&self, name: &[Ident]) -> Option<&Table> {
147+
self.tables
148+
.0
149+
.iter()
150+
.find(|t| t.table_name.as_deref() == Some(name))
151+
}
152+
153+
pub fn nullable_for_table_col(
154+
&self,
155+
table: &Table,
156+
col: &TableColumn,
157+
) -> anyhow::Result<NullableResult> {
158+
let col_name = col.column_name.clone();
159+
160+
// check col nullable in wal
161+
if let Some(wal_nullable) = self.wal.nullable_for_col(table, col.column_id) {
162+
// println!("found col null {} {col_name:?}", wal_nullable);
163+
return Ok(NullableResult::new(Some(wal_nullable), col_name));
164+
}
165+
166+
// check table nullable in wal
167+
if let Some(wal_nullable) = self.nullable_for_table(table) {
168+
// println!(
169+
// "found table null {} {col_name:?} {:?}",
170+
// wal_nullable, table.table_id
171+
// );
172+
if wal_nullable {
173+
return Ok(NullableResult::new(Some(wal_nullable), col_name));
174+
}
175+
}
176+
177+
Ok(NullableResult::new(Some(col.catalog_nullable), col_name))
178+
}
179+
pub fn nullable_for_ident(&self, name: &[Ident]) -> anyhow::Result<NullableResult> {
180+
let (col, table) = self.find_col_by_idents(name)?;
181+
self.nullable_for_table_col(table, &col)
182+
}
183+
pub fn find_col_by_idents(&self, name: &[Ident]) -> anyhow::Result<(TableColumn, &Table)> {
184+
// search for col
185+
if name.len() == 1 {
186+
for table in self.tables.0.iter() {
187+
for col in &table.columns {
188+
if col.column_name.as_ref() == Some(&name[0]) {
189+
return Ok((col.clone(), table));
190+
}
191+
}
192+
}
193+
}
194+
195+
// look for original name: `table_alias`.`col_name`
196+
if let Some(table) = self
197+
.tables
198+
.0
199+
.iter()
200+
.find(|table| table.table_name.as_deref() == Some(&name[..name.len() - 1]))
201+
{
202+
if let Some(col) = table
203+
.columns
204+
.iter()
205+
.find(|column| column.column_name.as_ref() == name.last())
206+
{
207+
return Ok((col.clone(), table));
208+
}
209+
}
210+
211+
// look for original name: `original_table_name`.`col_name`
212+
if let Some(table) = self
213+
.tables
214+
.0
215+
.iter()
216+
.find(|table| table.original_name.as_deref() == Some(&name[..name.len() - 1]))
217+
{
218+
if let Some(col) = table
219+
.columns
220+
.iter()
221+
.find(|column| column.column_name.as_ref() == name.last())
222+
{
223+
return Ok((col.clone(), table));
224+
}
225+
}
226+
227+
return Err(anyhow!("Not found"));
228+
}
229+
230+
pub fn push(&mut self, mut table: Table) {
231+
for cur_table in self.tables.0.iter() {
232+
// don't insert duplicate tables
233+
if cur_table.equals(&table) {
234+
return;
235+
}
236+
}
237+
238+
table.table_id = TableId::new(self.tables.len());
239+
240+
for col in table.columns.iter_mut() {
241+
col.table_id = table.table_id
242+
}
243+
244+
self.tables.0.push(table)
245+
}
246+
247+
pub fn nullable_for_table(&self, table: &Table) -> Option<bool> {
248+
for row in self.wal.data.iter().rev() {
249+
match row {
250+
WalEntry::TableNullable { table_id, nullable } if *table_id == table.table_id => {
251+
return Some(*nullable)
252+
}
253+
_ => continue,
254+
}
255+
}
256+
None
257+
}
258+
}

0 commit comments

Comments
 (0)