Skip to content
Merged
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 3 additions & 5 deletions crates/bindings-csharp/BSATN.Runtime/Builtins.cs
Original file line number Diff line number Diff line change
Expand Up @@ -339,11 +339,9 @@ public static implicit operator Timestamp(DateTimeOffset offset) =>
// Should be consistent with Rust implementation of Display.
public override readonly string ToString()
{
var sign = MicrosecondsSinceUnixEpoch < 0 ? "-" : "";
var pos = Math.Abs(MicrosecondsSinceUnixEpoch);
var secs = pos / Util.MicrosecondsPerSecond;
var microsRemaining = pos % Util.MicrosecondsPerSecond;
return $"{sign}{secs}.{microsRemaining:D6}";
var date = ToStd();

return date.ToString("yyyy-MM-dd'T'HH:mm:ss.ffffffK");
}

public static readonly Timestamp UNIX_EPOCH = new(0);
Expand Down
27 changes: 6 additions & 21 deletions crates/cli/src/subcommands/sql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ use crate::util::{database_identity, get_auth_header, ResponseExt, UNSTABLE_WARN
use anyhow::Context;
use clap::{Arg, ArgAction, ArgMatches};
use reqwest::RequestBuilder;
use spacetimedb::sql::compiler::build_table;
use spacetimedb_lib::de::serde::SeedWrapper;
use spacetimedb_lib::sats::{satn, Typespace};
use tabled::settings::Style;
use spacetimedb_lib::sats::Typespace;

pub fn cli() -> clap::Command {
clap::Command::new("sql")
Expand Down Expand Up @@ -160,27 +160,12 @@ pub(crate) async fn run_sql(builder: RequestBuilder, sql: &str, with_stats: bool
fn stmt_result_to_table(stmt_result: &StmtResultJson) -> anyhow::Result<(StmtStats, tabled::Table)> {
let stats = StmtStats::from(stmt_result);
let StmtResultJson { schema, rows, .. } = stmt_result;

let mut builder = tabled::builder::Builder::default();
builder.set_header(
schema
.elements
.iter()
.enumerate()
.map(|(i, e)| e.name.clone().unwrap_or_else(|| format!("column {i}").into())),
);

let ty = Typespace::EMPTY.with_type(schema);
for row in rows {
let row = from_json_seed(row.get(), SeedWrapper(ty))?;
builder.push_record(
ty.with_values(&row)
.map(|value| satn::PsqlWrapper { ty: ty.ty(), value }.to_string()),
);
}

let mut table = builder.build();
table.with(Style::psql());
let table = build_table(
schema,
rows.iter().map(|row| from_json_seed(row.get(), SeedWrapper(ty))),
)?;

Ok((stats, table))
}
Expand Down
1 change: 1 addition & 0 deletions crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ sled.workspace = true
smallvec.workspace = true
sqlparser.workspace = true
strum.workspace = true
tabled.workspace = true
tempfile.workspace = true
thiserror.workspace = true
thin-vec.workspace = true
Expand Down
164 changes: 124 additions & 40 deletions crates/core/src/sql/compiler.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use super::ast::TableSchemaView;
use super::ast::{compile_to_ast, Column, From, Join, Selection, SqlAst};
use super::type_check::TypeCheck;
use crate::db::datastore::locking_tx_datastore::state_view::StateView;
Expand All @@ -8,13 +9,13 @@ use spacetimedb_data_structures::map::IntMap;
use spacetimedb_lib::identity::AuthCtx;
use spacetimedb_lib::relation::{self, ColExpr, DbTable, FieldName, Header};
use spacetimedb_primitives::ColId;
use spacetimedb_sats::satn::PsqlType;
use spacetimedb_sats::{satn, ProductType, ProductValue, Typespace};
use spacetimedb_schema::schema::TableSchema;
use spacetimedb_vm::expr::{CrudExpr, Expr, FieldExpr, QueryExpr, SourceExpr};
use spacetimedb_vm::operator::OpCmp;
use std::sync::Arc;

use super::ast::TableSchemaView;

/// DIRTY HACK ALERT: Maximum allowed length, in UTF-8 bytes, of SQL queries.
/// Any query longer than this will be rejected.
/// This prevents a stack overflow when compiling queries with deeply-nested `AND` and `OR` conditions.
Expand Down Expand Up @@ -227,18 +228,55 @@ fn compile_statement(db: &RelationalDB, statement: SqlAst) -> Result<CrudExpr, P
Ok(q.optimize(&|table_id, table_name| db.row_count(table_id, table_name)))
}

/// Generates a [`tabled::Table`] from a schema and rows, using the style of a psql table.
pub fn build_table<E>(
schema: &ProductType,
rows: impl Iterator<Item = Result<ProductValue, E>>,
) -> Result<tabled::Table, E> {
let mut builder = tabled::builder::Builder::default();
builder.set_header(
schema
.elements
.iter()
.enumerate()
.map(|(i, e)| e.name.clone().unwrap_or_else(|| format!("column {i}").into())),
);

let ty = Typespace::EMPTY.with_type(schema);
for row in rows {
let row = row?;
builder.push_record(ty.with_values(&row).enumerate().map(|(idx, value)| {
let ty = PsqlType {
tuple: ty.ty(),
field: &ty.ty().elements[idx],
idx,
};

satn::PsqlWrapper { ty, value }.to_string()
}));
}

let mut table = builder.build();
table.with(tabled::settings::Style::psql());

Ok(table)
}

#[cfg(test)]
mod tests {
use super::*;
use crate::db::datastore::traits::IsolationLevel;
use crate::db::relational_db::tests_utils::{insert, TestDB};
use crate::execution_context::Workload;
use crate::sql::execute::tests::run_for_testing;
use itertools::Itertools;
use spacetimedb_lib::error::{ResultTest, TestError};
use spacetimedb_lib::{ConnectionId, Identity};
use spacetimedb_primitives::{col_list, ColList, TableId};
use spacetimedb_sats::time_duration::TimeDuration;
use spacetimedb_sats::timestamp::Timestamp;
use spacetimedb_sats::{
product, satn, AlgebraicType, AlgebraicValue, GroundSpacetimeType as _, ProductType, Typespace, ValueWithType,
product, AlgebraicType, AlgebraicValue, GroundSpacetimeType as _, ProductType, ProductValue,
};
use spacetimedb_vm::expr::{ColumnOp, IndexJoin, IndexScan, JoinExpr, Query};
use std::convert::From;
Expand Down Expand Up @@ -403,54 +441,100 @@ mod tests {
Ok(())
}

// Verify the output of `sql` matches the inputs for `Identity`, 'ConnectionId' & binary data.
#[test]
fn output_identity_connection_id() -> ResultTest<()> {
let row = product![AlgebraicValue::from(Identity::__dummy())];
let kind: ProductType = [("i", Identity::get_type())].into();
let ty = Typespace::EMPTY.with_type(&kind);
let out = ty
.with_values(&row)
.map(|value| satn::PsqlWrapper { ty: &kind, value }.to_string())
.collect::<Vec<_>>()
.join(", ");
assert_eq!(out, "0");
fn expect_psql_table(ty: &ProductType, rows: Vec<ProductValue>, expected: &str) {
let table = build_table(ty, rows.into_iter().map(Ok::<_, ()>)).unwrap().to_string();
let mut table = table.split('\n').map(|x| x.trim_end()).join("\n");
table.insert(0, '\n');
assert_eq!(expected, table);
}

// Verify the output of `sql` matches the inputs that return true for [`AlgebraicType::is_special()`]
#[test]
fn output_special_types() -> ResultTest<()> {
// Check tuples
let kind = [
("a", AlgebraicType::String),
("b", AlgebraicType::U256),
("o", Identity::get_type()),
("p", ConnectionId::get_type()),
let kind: ProductType = [
AlgebraicType::String,
AlgebraicType::U256,
Identity::get_type(),
ConnectionId::get_type(),
Timestamp::get_type(),
TimeDuration::get_type(),
]
.into();
let value = product![
"a",
Identity::ZERO.to_u256(),
Identity::ZERO,
ConnectionId::ZERO,
Timestamp::UNIX_EPOCH,
TimeDuration::ZERO
];

let value = AlgebraicValue::product([
AlgebraicValue::String("a".into()),
Identity::ZERO.to_u256().into(),
Identity::ZERO.to_u256().into(),
ConnectionId::ZERO.to_u128().into(),
]);

assert_eq!(
satn::PsqlWrapper { ty: &kind, value }.to_string().as_str(),
"(0 = \"a\", 1 = 0, 2 = 0, 3 = 0)"
expect_psql_table(
&kind,
vec![value],
r#"
column 0 | column 1 | column 2 | column 3 | column 4 | column 5
----------+----------+--------------------------------------------------------------------+------------------------------------+---------------------------+-----------
"a" | 0 | 0x0000000000000000000000000000000000000000000000000000000000000000 | 0x00000000000000000000000000000000 | 1970-01-01T00:00:00+00:00 | +0.000000"#,
);

let ty = Typespace::EMPTY.with_type(&kind);

// Check struct
let kind: ProductType = [
("bool", AlgebraicType::Bool),
("str", AlgebraicType::String),
("bytes", AlgebraicType::bytes()),
("identity", Identity::get_type()),
("connection_id", ConnectionId::get_type()),
("timestamp", Timestamp::get_type()),
("duration", TimeDuration::get_type()),
]
.into();

let value = product![
"a",
Identity::ZERO.to_u256(),
AlgebraicValue::product([Identity::ZERO.to_u256().into()]),
AlgebraicValue::product([ConnectionId::ZERO.to_u128().into()]),
true,
"This is spacetimedb".to_string(),
AlgebraicValue::Bytes([1, 2, 3, 4, 5, 6, 7].into()),
Identity::ZERO,
ConnectionId::ZERO,
Timestamp::UNIX_EPOCH,
TimeDuration::ZERO
];

let value = ValueWithType::new(ty, &value);
assert_eq!(
satn::PsqlWrapper { ty: ty.ty(), value }.to_string().as_str(),
"(a = \"a\", b = 0, o = 0, p = 0)"
expect_psql_table(
&kind,
vec![value.clone()],
r#"
bool | str | bytes | identity | connection_id | timestamp | duration
------+-----------------------+------------------+--------------------------------------------------------------------+------------------------------------+---------------------------+-----------
true | "This is spacetimedb" | 0x01020304050607 | 0x0000000000000000000000000000000000000000000000000000000000000000 | 0x00000000000000000000000000000000 | 1970-01-01T00:00:00+00:00 | +0.000000"#,
);

// Check nested struct, tuple...
let kind: ProductType = [(None, AlgebraicType::product(kind))].into();

let value = product![value.clone()];

expect_psql_table(
&kind,
vec![value.clone()],
r#"
column 0
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
(bool = true, str = "This is spacetimedb", bytes = 0x01020304050607, identity = 0x0000000000000000000000000000000000000000000000000000000000000000, connection_id = 0x00000000000000000000000000000000, timestamp = 1970-01-01T00:00:00+00:00, duration = +0.000000)"#,
);

let kind: ProductType = [("tuple", AlgebraicType::product(kind))].into();

let value = product![value];

expect_psql_table(
&kind,
vec![value],
r#"
tuple
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
(0 = (bool = true, str = "This is spacetimedb", bytes = 0x01020304050607, identity = 0x0000000000000000000000000000000000000000000000000000000000000000, connection_id = 0x00000000000000000000000000000000, timestamp = 1970-01-01T00:00:00+00:00, duration = +0.000000))"#,
);

Ok(())
Expand Down
2 changes: 1 addition & 1 deletion crates/expr/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ where
/// Parses a source text literal as a particular type
pub(crate) fn parse(value: &str, ty: &AlgebraicType) -> anyhow::Result<AlgebraicValue> {
let to_timestamp = || {
Timestamp::parse_from_str(value)?
Timestamp::parse_from_rfc3339(value)?
.serialize(ValueSerializer)
.with_context(|| "Could not parse timestamp")
};
Expand Down
2 changes: 1 addition & 1 deletion crates/sats/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ bitflags.workspace = true
bytes.workspace = true
bytemuck.workspace = true
bytestring = { workspace = true, optional = true }
chrono.workspace = true
chrono = { workspace = true, features = ["alloc"] }
decorum.workspace = true
derive_more.workspace = true
enum-as-inner.workspace = true
Expand Down
20 changes: 20 additions & 0 deletions crates/sats/src/product_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,26 @@ impl ProductType {
self.is_i64_newtype(TIME_DURATION_TAG)
}

/// Returns whether this is the special tag of [`Identity`].
pub fn is_identity_tag(tag_name: &str) -> bool {
tag_name == IDENTITY_TAG
}

/// Returns whether this is the special tag of [`ConnectionId`].
pub fn is_connection_id_tag(tag_name: &str) -> bool {
tag_name == CONNECTION_ID_TAG
}

/// Returns whether this is the special tag of [`Timestamp`].
pub fn is_timestamp_tag(tag_name: &str) -> bool {
tag_name == TIMESTAMP_TAG
}

/// Returns whether this is the special tag of [`TimeDuration`].
pub fn is_time_duration_tag(tag_name: &str) -> bool {
tag_name == TIME_DURATION_TAG
}

/// Returns whether this is a special known `tag`,
/// currently `Address`, `Identity`, `Timestamp` or `TimeDuration`.
pub fn is_special_tag(tag_name: &str) -> bool {
Expand Down
Loading
Loading