Skip to content

Commit f6089be

Browse files
authored
Merge pull request #159 from Embucket/df55-uppercase-identifiers
Support opt-in uppercase unquoted identifiers (DF55)
2 parents 8401e99 + 3ab667d commit f6089be

9 files changed

Lines changed: 680 additions & 80 deletions

File tree

‎datafusion/catalog/src/information_schema.rs‎

Lines changed: 109 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ use datafusion_expr::{TableType, Volatility};
4343
use datafusion_physical_plan::SendableRecordBatchStream;
4444
use datafusion_physical_plan::stream::RecordBatchStreamAdapter;
4545
use datafusion_physical_plan::streaming::PartitionStream;
46+
use futures::StreamExt;
4647
use std::collections::{BTreeSet, HashMap, HashSet};
4748
use std::fmt::Debug;
4849
use std::sync::Arc;
@@ -76,6 +77,7 @@ pub const INFORMATION_SCHEMA_TABLES: &[&str] = &[
7677
#[derive(Debug)]
7778
pub struct InformationSchemaProvider {
7879
config: InformationSchemaConfig,
80+
uppercase_identifiers: bool,
7981
}
8082

8183
impl InformationSchemaProvider {
@@ -85,10 +87,19 @@ impl InformationSchemaProvider {
8587
config: InformationSchemaConfig {
8688
catalog_list,
8789
table_functions: HashMap::new(),
90+
uppercase_identifiers: false,
8891
},
92+
uppercase_identifiers: false,
8993
}
9094
}
9195

96+
/// Expose virtual schema table and column names in uppercase.
97+
pub fn with_uppercase_identifiers(mut self, uppercase_identifiers: bool) -> Self {
98+
self.uppercase_identifiers = uppercase_identifiers;
99+
self.config.uppercase_identifiers = uppercase_identifiers;
100+
self
101+
}
102+
92103
/// Attach the session's table (UDTF) functions so that they appear in
93104
/// `information_schema.routines` / `SHOW FUNCTIONS`.
94105
pub fn with_table_functions(
@@ -104,9 +115,18 @@ impl InformationSchemaProvider {
104115
struct InformationSchemaConfig {
105116
catalog_list: Arc<dyn CatalogProviderList>,
106117
table_functions: HashMap<String, Arc<TableFunction>>,
118+
uppercase_identifiers: bool,
107119
}
108120

109121
impl InformationSchemaConfig {
122+
fn virtual_schema_name(&self) -> &'static str {
123+
if self.uppercase_identifiers {
124+
"INFORMATION_SCHEMA"
125+
} else {
126+
INFORMATION_SCHEMA
127+
}
128+
}
129+
110130
/// Construct the `information_schema.tables` virtual table
111131
async fn make_tables(
112132
&self,
@@ -118,7 +138,7 @@ impl InformationSchemaConfig {
118138
let catalog = self.catalog_list.catalog(&catalog_name).unwrap();
119139

120140
for schema_name in catalog.schema_names() {
121-
if schema_name != INFORMATION_SCHEMA {
141+
if schema_name != self.virtual_schema_name() {
122142
// schema name may not exist in the catalog, so we need to check
123143
if let Some(schema) = catalog.schema(&schema_name) {
124144
for table_name in schema.table_names() {
@@ -139,12 +159,21 @@ impl InformationSchemaConfig {
139159

140160
// Add a final list for the information schema tables themselves
141161
for table_name in INFORMATION_SCHEMA_TABLES {
142-
builder.add_table(
143-
&catalog_name,
144-
INFORMATION_SCHEMA,
145-
table_name,
146-
TableType::View,
147-
);
162+
if self.uppercase_identifiers {
163+
builder.add_table(
164+
&catalog_name,
165+
self.virtual_schema_name(),
166+
table_name.to_ascii_uppercase(),
167+
TableType::View,
168+
);
169+
} else {
170+
builder.add_table(
171+
&catalog_name,
172+
INFORMATION_SCHEMA,
173+
table_name,
174+
TableType::View,
175+
);
176+
}
148177
}
149178
}
150179

@@ -156,7 +185,7 @@ impl InformationSchemaConfig {
156185
let catalog = self.catalog_list.catalog(&catalog_name).unwrap();
157186

158187
for schema_name in catalog.schema_names() {
159-
if schema_name != INFORMATION_SCHEMA
188+
if schema_name != self.virtual_schema_name()
160189
&& let Some(schema) = catalog.schema(&schema_name)
161190
{
162191
let schema_owner = schema.owner_name();
@@ -174,7 +203,7 @@ impl InformationSchemaConfig {
174203
let catalog = self.catalog_list.catalog(&catalog_name).unwrap();
175204

176205
for schema_name in catalog.schema_names() {
177-
if schema_name != INFORMATION_SCHEMA {
206+
if schema_name != self.virtual_schema_name() {
178207
// schema name may not exist in the catalog, so we need to check
179208
if let Some(schema) = catalog.schema(&schema_name) {
180209
for table_name in schema.table_names() {
@@ -204,7 +233,7 @@ impl InformationSchemaConfig {
204233
let catalog = self.catalog_list.catalog(&catalog_name).unwrap();
205234

206235
for schema_name in catalog.schema_names() {
207-
if schema_name != INFORMATION_SCHEMA {
236+
if schema_name != self.virtual_schema_name() {
208237
// schema name may not exist in the catalog, so we need to check
209238
if let Some(schema) = catalog.schema(&schema_name) {
210239
for table_name in schema.table_names() {
@@ -616,14 +645,25 @@ impl SchemaProvider for InformationSchemaProvider {
616645
fn table_names(&self) -> Vec<String> {
617646
INFORMATION_SCHEMA_TABLES
618647
.iter()
619-
.map(|t| (*t).to_string())
648+
.map(|name| {
649+
if self.uppercase_identifiers {
650+
name.to_ascii_uppercase()
651+
} else {
652+
(*name).to_string()
653+
}
654+
})
620655
.collect()
621656
}
622657

623658
async fn table(
624659
&self,
625660
name: &str,
626661
) -> Result<Option<Arc<dyn TableProvider>>, DataFusionError> {
662+
if self.uppercase_identifiers
663+
&& name.bytes().any(|byte| byte.is_ascii_lowercase())
664+
{
665+
return Ok(None);
666+
}
627667
let config = self.config.clone();
628668
let table: Arc<dyn PartitionStream> = match name.to_ascii_lowercase().as_str() {
629669
TABLES => Arc::new(InformationSchemaTables::new(config)),
@@ -636,13 +676,70 @@ impl SchemaProvider for InformationSchemaProvider {
636676
_ => return Ok(None),
637677
};
638678

679+
let table: Arc<dyn PartitionStream> = if self.uppercase_identifiers {
680+
Arc::new(UppercaseColumnsPartitionStream::new(table))
681+
} else {
682+
table
683+
};
684+
639685
Ok(Some(Arc::new(
640686
StreamingTable::try_new(Arc::clone(table.schema()), vec![table]).unwrap(),
641687
)))
642688
}
643689

644690
fn table_exist(&self, name: &str) -> bool {
645-
INFORMATION_SCHEMA_TABLES.contains(&name.to_ascii_lowercase().as_str())
691+
(!self.uppercase_identifiers
692+
|| !name.bytes().any(|byte| byte.is_ascii_lowercase()))
693+
&& INFORMATION_SCHEMA_TABLES.contains(&name.to_ascii_lowercase().as_str())
694+
}
695+
}
696+
697+
#[derive(Debug)]
698+
struct UppercaseColumnsPartitionStream {
699+
inner: Arc<dyn PartitionStream>,
700+
schema: SchemaRef,
701+
}
702+
703+
impl UppercaseColumnsPartitionStream {
704+
fn new(inner: Arc<dyn PartitionStream>) -> Self {
705+
let original = inner.schema();
706+
let fields: Vec<FieldRef> = original
707+
.fields()
708+
.iter()
709+
.map(|field| {
710+
Arc::new(
711+
field
712+
.as_ref()
713+
.clone()
714+
.with_name(field.name().to_ascii_uppercase()),
715+
)
716+
})
717+
.collect();
718+
let schema = Arc::new(Schema::new_with_metadata(
719+
fields,
720+
original.metadata().clone(),
721+
));
722+
Self { inner, schema }
723+
}
724+
}
725+
726+
impl PartitionStream for UppercaseColumnsPartitionStream {
727+
fn schema(&self) -> &SchemaRef {
728+
&self.schema
729+
}
730+
731+
fn execute(&self, ctx: Arc<TaskContext>) -> SendableRecordBatchStream {
732+
let schema = Arc::clone(&self.schema);
733+
let batches = self.inner.execute(ctx).map({
734+
let schema = Arc::clone(&schema);
735+
move |batch| {
736+
batch.and_then(|batch| {
737+
RecordBatch::try_new(Arc::clone(&schema), batch.columns().to_vec())
738+
.map_err(Into::into)
739+
})
740+
}
741+
});
742+
Box::pin(RecordBatchStreamAdapter::new(schema, batches))
646743
}
647744
}
648745

‎datafusion/common/src/config.rs‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,9 +275,17 @@ config_namespace! {
275275
/// For example, `10.00` is planned as `DECIMAL(2, 0)` instead of `DECIMAL(4, 2)`.
276276
pub trim_decimal_literal_trailing_zeros: bool, default = false
277277

278-
/// When set to true, SQL parser will normalize ident (convert ident to lowercase when not quoted)
278+
/// When set to true, normalize unquoted identifiers to lowercase by default.
279279
pub enable_ident_normalization: bool, default = true
280280

281+
/// When identifier normalization is enabled, fold unquoted relation, schema,
282+
/// column, and CTE identifiers to uppercase instead of lowercase. Quoted
283+
/// identifiers retain their exact spelling. Set this when creating the session;
284+
/// SQL SET/RESET cannot change it later. Catalogs and schemas used by SQL should
285+
/// be registered under their uppercase names. Function names still follow the
286+
/// existing function-registry lookup rules.
287+
pub uppercase_unquoted_identifiers: bool, default = false
288+
281289
/// When set to true, SQL parser will normalize options value (convert value to lowercase).
282290
/// Note that this option is ignored and will be removed in the future. All case-insensitive values
283291
/// are normalized automatically.

0 commit comments

Comments
 (0)