Skip to content
This repository has been archived by the owner on May 3, 2021. It is now read-only.

First draft of a dynamic select clause feature in diesel-dynamic-schema #10

Draft
wants to merge 17 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ name = "diesel-dynamic-schema"
version = "1.0.0"
authors = ["Sean Griffin <[email protected]>"]
license = "MIT OR Apache-2.0"
autotests = false

[dependencies]
diesel = "1.0.0"
diesel = { version = "1.4.2", default-features = false }

[dev-dependencies]
dotenv = "0.14"

[[example]]
name = "querying_basic_schemas"
Expand All @@ -26,4 +30,14 @@ required-features = ["diesel/sqlite"]
name = "integration_tests"
path = "tests/lib.rs"
harness = true
required-features = ["diesel/sqlite"]
#required-features = ["diesel/sqlite"]

[features]
postgres = ["diesel/postgres"]
sqlite = ["diesel/sqlite"]
mysql = ["diesel/mysql"]


[patch.crates-io]
diesel = {git = "https://github.com/GiGainfosystems/diesel", rev = "0744b7e6e05582bf8fca21c0a5fbba08555abd94"}
diesel_derives = {git = "https://github.com/GiGainfosystems/diesel", rev = "0744b7e6e05582bf8fca21c0a5fbba08555abd94"}
10 changes: 10 additions & 0 deletions src/column.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ impl<T, U, ST> Column<T, U, ST> {
_sql_type: PhantomData,
}
}

/// Gets a reference to the table of the column.
pub fn table(&self) -> &T {
&self.table
}

/// Gets the name of the column, as provided on creation.
pub fn name(&self) -> &U {
&self.name
}
}

impl<T, U, ST> QueryId for Column<T, U, ST> {
Expand Down
94 changes: 94 additions & 0 deletions src/dynamic_select.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use diesel::backend::Backend;
use diesel::query_builder::{
AstPass, IntoBoxedSelectClause, QueryFragment, QueryId, SelectClauseExpression,
SelectClauseQueryFragment,
};
use diesel::{
sql_types::HasSqlType, AppearsOnTable, Expression, QueryResult, SelectableExpression,
};
use std::marker::PhantomData;

#[allow(missing_debug_implementations)]
pub struct DynamicSelectClause<'a, DB, QS> {
selects: Vec<Box<dyn QueryFragment<DB> + 'a>>,
p: PhantomData<QS>,
}

impl<'a, DB, QS> QueryId for DynamicSelectClause<'a, DB, QS> {
const HAS_STATIC_QUERY_ID: bool = false;
type QueryId = ();
}

impl<'a, DB, QS> DynamicSelectClause<'a, DB, QS> {
pub fn new() -> Self {
Self {
selects: Vec::new(),
p: PhantomData,
}
}
pub fn add_field<F>(&mut self, field: F)
where
F: QueryFragment<DB> + SelectableExpression<QS> + 'a,
DB: Backend,
{
self.selects.push(Box::new(field))
}
}

impl<'a, QS, DB> Expression for DynamicSelectClause<'a, DB, QS>
where
DB: Backend + HasSqlType<crate::dynamic_value::Any>,
{
type SqlType = crate::dynamic_value::Any;
}

impl<'a, DB, QS> AppearsOnTable<QS> for DynamicSelectClause<'a, DB, QS> where Self: Expression {}

impl<'a, DB, QS> SelectableExpression<QS> for DynamicSelectClause<'a, DB, QS> where
Self: AppearsOnTable<QS>
{
}

impl<'a, QS, DB> SelectClauseExpression<QS> for DynamicSelectClause<'a, DB, QS> {
type SelectClauseSqlType = crate::dynamic_value::Any;
}

impl<'a, QS, DB> SelectClauseQueryFragment<QS, DB> for DynamicSelectClause<'a, QS, DB>
where
DB: Backend,
Self: QueryFragment<DB>,
{
fn walk_ast(&self, _source: &QS, pass: AstPass<DB>) -> QueryResult<()> {
<Self as QueryFragment<DB>>::walk_ast(self, pass)
}
}

impl<'a, DB, QS> QueryFragment<DB> for DynamicSelectClause<'a, DB, QS>
where
DB: Backend,
{
fn walk_ast(&self, mut pass: AstPass<DB>) -> QueryResult<()> {
let mut first = true;
for s in &self.selects {
if first {
first = false;
} else {
pass.push_sql(", ");
}
s.walk_ast(pass.reborrow())?;
}
Ok(())
}
}

impl<'a, DB, QS> IntoBoxedSelectClause<'a, DB, QS> for DynamicSelectClause<'a, DB, QS>
where
Self: 'a + QueryFragment<DB> + SelectClauseExpression<QS>,
DB: Backend,
{
type SqlType = <Self as SelectClauseExpression<QS>>::SelectClauseSqlType;

fn into_boxed(self, _source: &QS) -> Box<dyn QueryFragment<DB> + 'a> {
Box::new(self)
}
}
228 changes: 228 additions & 0 deletions src/dynamic_value.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
use diesel::deserialize::{self, FromSql, FromSqlRow, Queryable, QueryableByName};
use diesel::row::{NamedRow, Row};
use diesel::{backend::Backend, QueryId, SqlType};
use std::iter::FromIterator;
use std::ops::Index;

#[derive(Debug, Clone, Copy, Default, QueryId, SqlType)]
#[postgres(oid = "0", array_oid = "0")]
#[sqlite_type = "Integer"]
pub struct Any;

#[cfg(feature = "mysql")]
impl diesel::sql_types::HasSqlType<Any> for diesel::mysql::Mysql {
fn metadata(_lookup: &Self::MetadataLookup) -> Self::TypeMetadata {
None
}
}

#[derive(Debug, Clone)]
pub struct DynamicRow<I> {
values: Vec<I>,
}

#[derive(Debug, Clone, PartialEq)]
pub struct NamedField<I> {
pub name: String,
pub value: I,
}

impl<I> FromIterator<I> for DynamicRow<I> {
fn from_iter<T>(iter: T) -> Self
where
T: IntoIterator<Item = I>,
{
DynamicRow {
values: iter.into_iter().collect(),
}
}
}

impl<I> DynamicRow<I> {
pub fn get(&self, index: usize) -> Option<&I> {
self.values.get(index)
}
}

impl<I> DynamicRow<NamedField<I>> {
pub fn get_by_name<S: AsRef<str>>(&self, name: S) -> Option<&I> {
self.values
.iter()
.find(|f| f.name == name.as_ref())
.map(|f| &f.value)
}
}

#[cfg(feature = "postgres")]
impl<I> FromSqlRow<Any, diesel::pg::Pg> for DynamicRow<I>
where
I: FromSql<Any, diesel::pg::Pg>,
{
const FIELDS_NEEDED: usize = 1;

fn build_from_row<T: Row<diesel::pg::Pg>>(row: &mut T) -> deserialize::Result<Self> {
(0..row.column_count())
.map(|_| I::from_sql(row.take()))
.collect::<deserialize::Result<_>>()
}
}

#[cfg(feature = "sqlite")]
impl<I> FromSqlRow<Any, diesel::sqlite::Sqlite> for DynamicRow<I>
where
I: FromSql<Any, diesel::sqlite::Sqlite>,
{
const FIELDS_NEEDED: usize = 1;

fn build_from_row<T: Row<diesel::sqlite::Sqlite>>(row: &mut T) -> deserialize::Result<Self> {
(0..row.column_count())
.map(|_| I::from_sql(row.take()))
.collect::<deserialize::Result<_>>()
}
}

#[cfg(feature = "mysql")]
impl<I> FromSqlRow<Any, diesel::mysql::Mysql> for DynamicRow<I>
where
I: FromSql<Any, diesel::mysql::Mysql>,
{
const FIELDS_NEEDED: usize = 1;

fn build_from_row<T: Row<diesel::mysql::Mysql>>(row: &mut T) -> deserialize::Result<Self> {
(0..row.column_count())
.map(|_| I::from_sql(row.take()))
.collect::<deserialize::Result<_>>()
}
}

impl<I, DB> Queryable<Any, DB> for DynamicRow<I>
where
DB: Backend,
Self: FromSqlRow<Any, DB>,
{
type Row = DynamicRow<I>;

fn build(row: Self::Row) -> Self {
row
}
}

impl<I, DB> QueryableByName<DB> for DynamicRow<NamedField<I>>
where
DB: Backend,
I: FromSql<Any, DB>,
{
fn build<R: NamedRow<DB>>(row: &R) -> deserialize::Result<Self> {
row.field_names()
.into_iter()
.map(|name| {
Ok(NamedField {
name: name.to_owned(),
value: row.get::<Any, I>(name)?,
})
})
.collect()
}
}

#[cfg(feature = "postgres")]
impl<I> QueryableByName<diesel::pg::Pg> for DynamicRow<I>
where
I: FromSql<Any, diesel::pg::Pg>,
{
fn build<R: NamedRow<diesel::pg::Pg>>(row: &R) -> deserialize::Result<Self> {
row.field_names()
.into_iter()
.map(|name| row.get::<Any, I>(name))
.collect()
}
}

#[cfg(feature = "sqlite")]
impl<I> QueryableByName<diesel::sqlite::Sqlite> for DynamicRow<I>
where
I: FromSql<Any, diesel::sqlite::Sqlite>,
{
fn build<R: NamedRow<diesel::sqlite::Sqlite>>(row: &R) -> deserialize::Result<Self> {
row.field_names()
.into_iter()
.map(|name| row.get::<Any, I>(name))
.collect()
}
}

#[cfg(feature = "mysql")]
impl<I> QueryableByName<diesel::mysql::Mysql> for DynamicRow<I>
where
I: FromSql<Any, diesel::mysql::Mysql>,
{
fn build<R: NamedRow<diesel::mysql::Mysql>>(row: &R) -> deserialize::Result<Self> {
row.field_names()
.into_iter()
.map(|name| row.get::<Any, I>(name))
.collect()
}
}

impl<I, DB> FromSqlRow<Any, DB> for DynamicRow<NamedField<I>>
where
DB: Backend,
I: FromSql<Any, DB>,
{
const FIELDS_NEEDED: usize = 1;

fn build_from_row<T: Row<DB>>(row: &mut T) -> deserialize::Result<Self> {
Ok(DynamicRow {
values: (0..row.column_count())
.map(|_| {
let name = row
.column_name()
.ok_or_else(|| "Request name for an unnamed column")?
.into();
Ok(NamedField {
name,
value: I::from_sql(row.take())?,
})
})
.collect::<deserialize::Result<_>>()?,
})
}
}

impl<I> Index<usize> for DynamicRow<I> {
type Output = I;

fn index(&self, index: usize) -> &Self::Output {
&self.values[index]
}
}

impl<'a, I> Index<&'a str> for DynamicRow<NamedField<I>> {
type Output = I;

fn index(&self, field_name: &'a str) -> &Self::Output {
self.values
.iter()
.find(|f| f.name == field_name)
.map(|f| &f.value)
.expect("Field not found")
}
}

impl<V> IntoIterator for DynamicRow<V> {
type Item = V;
type IntoIter = <Vec<V> as IntoIterator>::IntoIter;

fn into_iter(self) -> Self::IntoIter {
self.values.into_iter()
}
}

impl<'a, V> IntoIterator for &'a DynamicRow<V> {
type Item = &'a V;
type IntoIter = <&'a Vec<V> as IntoIterator>::IntoIter;

fn into_iter(self) -> Self::IntoIter {
(&self.values).into_iter()
}
}
Loading