use rudb_catalog::{Catalog, Constraint, Database, Schema, TEMP_CATALOG, Table};
use rudb_common::{LogicalType, Result, Value};
use rudb_functions::{
DUCKDB, canonical, column_fields, constraint_fields, database_fields, index_fields,
numeric_facts, schema_fields, sequence_fields, show_database_fields, show_expanded_fields,
show_table_fields, table_fields, type_oid, view_fields,
};
use rudb_parse::{Kind, quoted, tokenize};
use rudb_plan::{Plan, Slice};
use crate::metadata::{Metadata, text};
pub(crate) fn databasenames(
catalog: &Catalog,
plan: &Plan,
index: u32,
columns: Slice,
) -> Result<Metadata> {
let mut rows = Vec::with_capacity(catalog.databases().len());
for database in catalog.databases() {
rows.push(vec![
text(database.name()),
Value::BigInt(database.oid()),
Value::Null,
Value::Null,
empty(),
Value::Boolean(database.internal()),
text(DUCKDB),
Value::Boolean(false),
Value::Boolean(false),
Value::Null,
empty(),
]);
}
Metadata::new("duckdb_databases", &database_fields(), &rows, plan, index, columns)
}
pub(crate) fn schemanames(
catalog: &Catalog,
plan: &Plan,
index: u32,
columns: Slice,
) -> Result<Metadata> {
let mut rows = Vec::new();
for database in catalog.databases() {
for schema in database.schemas() {
rows.push(vec![
Value::BigInt(schema.oid()),
text(database.name()),
Value::BigInt(database.oid()),
text(schema.name()),
Value::Null,
empty(),
Value::Boolean(true),
Value::Null,
Value::Null,
Value::Null,
]);
}
}
Metadata::new("duckdb_schemas", &schema_fields(), &rows, plan, index, columns)
}
pub(crate) fn tablenames(
catalog: &Catalog,
plan: &Plan,
index: u32,
columns: Slice,
) -> Result<Metadata> {
let mut rows = Vec::new();
for (database, schema, table) in entries(catalog) {
let count = i64::try_from(table.columns().len()).unwrap_or(i64::MAX);
rows.push(vec![
text(database.name()),
Value::BigInt(database.oid()),
text(schema.name()),
Value::BigInt(schema.oid()),
text(&table.name().table),
Value::BigInt(table.oid()),
Value::Null,
empty(),
Value::Boolean(entry_internal(database)),
Value::Boolean(table.name().temporary()),
Value::Boolean(table.keys().iter().any(|key| key.primary)),
Value::BigInt(i64::try_from(table.rows().len()).unwrap_or(i64::MAX)),
Value::BigInt(count),
Value::BigInt(
i64::try_from(table.keys().len() + table.foreign().len() + table.indexes().len())
.unwrap_or(i64::MAX),
),
Value::BigInt(i64::try_from(table.checks().len()).unwrap_or(i64::MAX)),
text(&create_table(table)),
]);
}
Metadata::new("duckdb_tables", &table_fields(), &rows, plan, index, columns)
}
pub(crate) fn viewnames(
catalog: &Catalog,
plan: &Plan,
index: u32,
columns: Slice,
) -> Result<Metadata> {
let mut rows = Vec::new();
for database in catalog.databases() {
for schema in database.schemas() {
for view in schema.views() {
let columns = view.columns();
rows.push(vec![
text(database.name()),
Value::BigInt(database.oid()),
text(schema.name()),
Value::BigInt(schema.oid()),
text(&view.name().table),
Value::BigInt(view.oid()),
Value::Null,
empty(),
Value::Boolean(entry_internal(database)),
Value::Boolean(database.internal()),
if columns.is_empty() {
Value::Null
} else {
Value::BigInt(i64::try_from(columns.len()).unwrap_or(i64::MAX))
},
text(view.statement()),
Value::Boolean(!columns.is_empty()),
]);
}
}
}
Metadata::new("duckdb_views", &view_fields(), &rows, plan, index, columns)
}
pub(crate) fn indexnames(
catalog: &Catalog,
plan: &Plan,
index: u32,
columns: Slice,
) -> Result<Metadata> {
let mut rows = Vec::new();
for (database, schema, table) in entries(catalog) {
for held in table.indexes() {
rows.push(vec![
text(database.name()),
Value::BigInt(database.oid()),
text(schema.name()),
Value::BigInt(schema.oid()),
text(&held.name),
Value::BigInt(held.oid),
text(&table.name().table),
Value::BigInt(table.oid()),
Value::Null,
empty(),
Value::Boolean(held.unique),
Value::Boolean(false),
text(&held.expressions),
text(&held.sql),
]);
}
}
Metadata::new("duckdb_indexes", &index_fields(), &rows, plan, index, columns)
}
pub(crate) fn constraintnames(
catalog: &Catalog,
plan: &Plan,
index: u32,
columns: Slice,
) -> Result<Metadata> {
let mut rows = Vec::new();
for database in catalog.databases() {
for schema in database.schemas() {
let mut tables: Vec<&Table> = schema.tables().iter().collect();
tables.sort_by(|left, right| left.name().table.cmp(&right.name().table));
for table in tables {
for held in table.constraints() {
let mut row = vec![
text(database.name()),
Value::BigInt(database.oid()),
text(schema.name()),
Value::BigInt(schema.oid()),
text(&table.name().table),
Value::BigInt(table.oid()),
Value::BigInt(rows.len() as i64),
];
row.extend(constraint_row(catalog, table, held));
rows.push(row);
}
}
}
}
Metadata::new("duckdb_constraints", &constraint_fields(), &rows, plan, index, columns)
}
fn constraint_row(catalog: &Catalog, table: &Table, held: Constraint) -> Vec<Value> {
let name_of = |at: usize| table.columns()[at].name.clone();
let list = |names: &[String]| names.iter().map(|name| quoted(name)).collect::<Vec<_>>();
let (kind, words, expression, places, referenced) = match held {
Constraint::Key(at) => {
let key = &table.keys()[at];
let names: Vec<String> = key.columns.iter().map(|&at| name_of(at)).collect();
let (kind, word) =
if key.primary { ("PRIMARY KEY", "pkey") } else { ("UNIQUE", "key") };
let text = format!("{kind}({})", list(&names).join(", "));
((kind, text), word, None, key.columns.clone(), None)
}
Constraint::Check(at) => {
let expression = table.checks()[at].clone();
let places = check_columns(table, &expression);
let text = format!("CHECK({expression})");
(("CHECK", text), "check", Some(expression), places, None)
}
Constraint::Foreign(at) => {
let foreign = &table.foreign()[at];
let target = catalog.table(&foreign.table).ok().unwrap_or(table);
let wanted: Vec<String> =
foreign.referenced.iter().map(|&at| target.columns()[at].name.clone()).collect();
let names: Vec<String> = foreign.columns.iter().map(|&at| name_of(at)).collect();
let mut held = quoted(&foreign.table.table);
if !foreign.table.schema.eq_ignore_ascii_case("main") {
held = format!("{}.{held}", quoted(&foreign.table.schema));
}
let text = format!(
"FOREIGN KEY ({}) REFERENCES {}({})",
list(&names).join(", "),
held,
list(&wanted).join(", ")
);
let referenced = (foreign.table.table.clone(), wanted);
(("FOREIGN KEY", text), "fkey", None, foreign.columns.clone(), Some(referenced))
}
Constraint::NotNull(at) => {
(("NOT NULL", "NOT NULL".to_string()), "not_null", None, vec![at], None)
}
};
let names: Vec<String> = places.iter().map(|&at| name_of(at)).collect();
let mut constraint = format!("{}_", table.name().table);
for name in &names {
constraint.push_str(&name.to_lowercase());
constraint.push('_');
}
for name in referenced.iter().flat_map(|(_, wanted)| wanted) {
constraint.push_str(&name.to_lowercase());
constraint.push('_');
}
constraint.push_str(words);
let (referenced_table, wanted) = match referenced {
Some((table, wanted)) => (text(&table), wanted),
None => (Value::Null, Vec::new()),
};
vec![
text(kind.0),
text(&kind.1),
expression.map_or(Value::Null, Value::Varchar),
Value::List {
element: LogicalType::BigInt,
values: places.iter().map(|&at| Value::BigInt(at as i64)).collect(),
},
names_of(names.into_iter()),
text(&constraint),
referenced_table,
names_of(wanted.into_iter()),
]
}
fn check_columns(table: &Table, expression: &str) -> Vec<usize> {
let Ok(tokens) = tokenize(expression) else {
return Vec::new();
};
let mut places = Vec::new();
for (at, token) in tokens.iter().enumerate() {
let word = &expression[token.start as usize..token.end as usize];
let word = match token.kind {
Kind::Identifier | Kind::Keyword => word.to_string(),
Kind::QuotedIdentifier => word.trim_matches('"').replace("\"\"", "\""),
_ => continue,
};
let call = tokens.get(at + 1).is_some_and(|next| {
next.kind == Kind::Operator
&& &expression[next.start as usize..next.end as usize] == "("
});
if call {
continue;
}
if let Some(place) =
table.columns().iter().position(|field| field.name.eq_ignore_ascii_case(&word))
{
places.push(place);
}
}
places
}
pub(crate) fn sequencenames(
catalog: &Catalog,
plan: &Plan,
index: u32,
columns: Slice,
) -> Result<Metadata> {
let mut rows = Vec::new();
for database in catalog.databases() {
for schema in database.schemas() {
for sequence in schema.sequences() {
let counter = sequence.counter();
let options = counter.options();
let sql = format!(
"CREATE SEQUENCE {} INCREMENT BY {} MINVALUE {} MAXVALUE {} START {} {};",
sequence.name().table,
options.increment,
options.min,
options.max,
counter.counter(),
if options.cycle { "CYCLE" } else { "NO CYCLE" }
);
rows.push(vec![
text(database.name()),
Value::BigInt(database.oid()),
text(schema.name()),
Value::BigInt(schema.oid()),
text(&sequence.name().table),
Value::BigInt(sequence.oid()),
Value::Null,
empty(),
Value::Boolean(database.name() == TEMP_CATALOG),
Value::BigInt(options.start),
Value::BigInt(options.min),
Value::BigInt(options.max),
Value::BigInt(options.increment),
Value::Boolean(options.cycle),
counter.last().map_or(Value::Null, Value::BigInt),
text(&sql),
]);
}
}
}
Metadata::new("duckdb_sequences", &sequence_fields(), &rows, plan, index, columns)
}
pub(crate) fn columnnames(
catalog: &Catalog,
plan: &Plan,
index: u32,
columns: Slice,
) -> Result<Metadata> {
let mut rows = Vec::new();
for database in catalog.databases() {
for schema in database.schemas() {
for table in schema.tables() {
for (at, column) in table.columns().iter().enumerate() {
rows.push(column_row(
database,
schema,
&table.name().table,
table.oid(),
at,
&column.name,
&column.ty,
!column.not_null,
table.default(at),
));
}
}
for view in schema.views() {
for (at, field) in view.columns().iter().enumerate() {
rows.push(column_row(
database,
schema,
&view.name().table,
view.oid(),
at,
&field.name,
&field.ty,
true,
None,
));
}
}
}
}
Metadata::new("duckdb_columns", &column_fields(), &rows, plan, index, columns)
}
fn entry_internal(database: &Database) -> bool {
database.internal() && !database.name().eq_ignore_ascii_case(TEMP_CATALOG)
}
#[expect(clippy::too_many_arguments, reason = "a row of a twenty one column table")]
fn column_row(
database: &Database,
schema: &Schema,
table: &str,
oid: i64,
at: usize,
name: &str,
ty: &LogicalType,
nullable: bool,
default: Option<&str>,
) -> Vec<Value> {
let (precision, radix, scale) = numeric_facts(ty);
vec![
text(database.name()),
Value::BigInt(database.oid()),
text(schema.name()),
Value::BigInt(schema.oid()),
text(table),
Value::BigInt(oid),
text(name),
Value::Integer(i32::try_from(at + 1).unwrap_or(i32::MAX)),
Value::Null,
Value::Boolean(entry_internal(database)),
default.map_or(Value::Null, text),
Value::Boolean(nullable),
text(&ty.to_string()),
type_oid(&canonical(ty)).map_or(Value::Null, Value::BigInt),
Value::Null,
precision.map_or(Value::Null, Value::Integer),
radix.map_or(Value::Null, Value::Integer),
scale.map_or(Value::Null, Value::Integer),
empty(),
Value::Boolean(false),
Value::Null,
]
}
pub(crate) fn showtables(
catalog: &Catalog,
plan: &Plan,
index: u32,
columns: Slice,
) -> Result<Metadata> {
let mut names = Vec::new();
for database in catalog.databases() {
let reachable = database.name().eq_ignore_ascii_case(catalog.default_catalog())
|| database.name().eq_ignore_ascii_case(TEMP_CATALOG);
if !reachable {
continue;
}
for schema in database.schemas() {
if !schema.name().eq_ignore_ascii_case(catalog.default_schema()) {
continue;
}
names.extend(schema.tables().iter().map(|table| table.name().table.clone()));
names.extend(schema.views().iter().map(|view| view.name().table.clone()));
}
}
names.sort();
let rows: Vec<Vec<Value>> = names.iter().map(|name| vec![text(name)]).collect();
Metadata::new("pragma_show_tables", &show_table_fields(), &rows, plan, index, columns)
}
pub(crate) fn showdatabases(
catalog: &Catalog,
plan: &Plan,
index: u32,
columns: Slice,
) -> Result<Metadata> {
let mut names: Vec<&str> = catalog
.databases()
.iter()
.filter(|database| !database.internal())
.map(Database::name)
.collect();
names.sort_unstable();
let rows: Vec<Vec<Value>> = names.iter().map(|name| vec![text(name)]).collect();
Metadata::new("pragma_show_databases", &show_database_fields(), &rows, plan, index, columns)
}
pub(crate) fn showtablesexpanded(
catalog: &Catalog,
plan: &Plan,
index: u32,
columns: Slice,
) -> Result<Metadata> {
let mut rows = Vec::new();
for database in catalog.databases() {
if entry_internal(database) {
continue;
}
for schema in database.schemas() {
for table in schema.tables() {
rows.push(expanded_row(database, schema, &table.name().table, table.columns()));
}
for view in schema.views() {
rows.push(expanded_row(database, schema, &view.name().table, &view.columns()));
}
}
}
rows.sort_by_key(|row| {
let at = |index: usize| match &row[index] {
Value::Varchar(name) => name.clone(),
_ => String::new(),
};
(at(0), at(1), at(2))
});
Metadata::new(
"pragma_show_tables_expanded",
&show_expanded_fields(),
&rows,
plan,
index,
columns,
)
}
fn expanded_row(
database: &Database,
schema: &Schema,
name: &str,
columns: &[rudb_common::Field],
) -> Vec<Value> {
vec![
text(database.name()),
text(schema.name()),
text(name),
names_of(columns.iter().map(|field| field.name.clone())),
names_of(columns.iter().map(|field| field.ty.to_string())),
Value::Boolean(database.internal()),
]
}
fn names_of(values: impl Iterator<Item = String>) -> Value {
Value::List { element: LogicalType::Varchar, values: values.map(Value::Varchar).collect() }
}
fn entries(catalog: &Catalog) -> impl Iterator<Item = (&Database, &Schema, &Table)> {
catalog.databases().iter().flat_map(|database| {
database.schemas().iter().flat_map(move |schema| {
schema.tables().iter().map(move |table| (database, schema, table))
})
})
}
fn create_table(table: &Table) -> String {
let columns: Vec<String> = table
.columns()
.iter()
.map(|column| {
let null = if column.not_null { " NOT NULL" } else { "" };
format!("{} {}{null}", quoted(&column.name), column.ty)
})
.collect();
format!("CREATE TABLE {}({});", quoted(&table.name().table), columns.join(", "))
}
fn empty() -> Value {
Value::map(LogicalType::Varchar, LogicalType::Varchar, Vec::new())
}