Skip to content

Commit 4ade692

Browse files
authored
Update formatting of timestamps and identities in PsqlFormatter (#2486)
1 parent a2200be commit 4ade692

File tree

14 files changed

+354
-127
lines changed

14 files changed

+354
-127
lines changed

Cargo.lock

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

crates/bindings-csharp/BSATN.Runtime/Builtins.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -339,11 +339,9 @@ public static implicit operator Timestamp(DateTimeOffset offset) =>
339339
// Should be consistent with Rust implementation of Display.
340340
public override readonly string ToString()
341341
{
342-
var sign = MicrosecondsSinceUnixEpoch < 0 ? "-" : "";
343-
var pos = Math.Abs(MicrosecondsSinceUnixEpoch);
344-
var secs = pos / Util.MicrosecondsPerSecond;
345-
var microsRemaining = pos % Util.MicrosecondsPerSecond;
346-
return $"{sign}{secs}.{microsRemaining:D6}";
342+
var date = ToStd();
343+
344+
return date.ToString("yyyy-MM-dd'T'HH:mm:ss.ffffffK");
347345
}
348346

349347
public static readonly Timestamp UNIX_EPOCH = new(0);

crates/cli/src/subcommands/sql.rs

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ use crate::util::{database_identity, get_auth_header, ResponseExt, UNSTABLE_WARN
99
use anyhow::Context;
1010
use clap::{Arg, ArgAction, ArgMatches};
1111
use reqwest::RequestBuilder;
12+
use spacetimedb::sql::compiler::build_table;
1213
use spacetimedb_lib::de::serde::SeedWrapper;
13-
use spacetimedb_lib::sats::{satn, Typespace};
14-
use tabled::settings::Style;
14+
use spacetimedb_lib::sats::Typespace;
1515

1616
pub fn cli() -> clap::Command {
1717
clap::Command::new("sql")
@@ -160,27 +160,12 @@ pub(crate) async fn run_sql(builder: RequestBuilder, sql: &str, with_stats: bool
160160
fn stmt_result_to_table(stmt_result: &StmtResultJson) -> anyhow::Result<(StmtStats, tabled::Table)> {
161161
let stats = StmtStats::from(stmt_result);
162162
let StmtResultJson { schema, rows, .. } = stmt_result;
163-
164-
let mut builder = tabled::builder::Builder::default();
165-
builder.set_header(
166-
schema
167-
.elements
168-
.iter()
169-
.enumerate()
170-
.map(|(i, e)| e.name.clone().unwrap_or_else(|| format!("column {i}").into())),
171-
);
172-
173163
let ty = Typespace::EMPTY.with_type(schema);
174-
for row in rows {
175-
let row = from_json_seed(row.get(), SeedWrapper(ty))?;
176-
builder.push_record(
177-
ty.with_values(&row)
178-
.map(|value| satn::PsqlWrapper { ty: ty.ty(), value }.to_string()),
179-
);
180-
}
181164

182-
let mut table = builder.build();
183-
table.with(Style::psql());
165+
let table = build_table(
166+
schema,
167+
rows.iter().map(|row| from_json_seed(row.get(), SeedWrapper(ty))),
168+
)?;
184169

185170
Ok((stats, table))
186171
}

crates/core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ sled.workspace = true
8888
smallvec.workspace = true
8989
sqlparser.workspace = true
9090
strum.workspace = true
91+
tabled.workspace = true
9192
tempfile.workspace = true
9293
thiserror.workspace = true
9394
thin-vec.workspace = true

crates/core/src/sql/compiler.rs

Lines changed: 124 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use super::ast::TableSchemaView;
12
use super::ast::{compile_to_ast, Column, From, Join, Selection, SqlAst};
23
use super::type_check::TypeCheck;
34
use crate::db::datastore::locking_tx_datastore::state_view::StateView;
@@ -8,13 +9,13 @@ use spacetimedb_data_structures::map::IntMap;
89
use spacetimedb_lib::identity::AuthCtx;
910
use spacetimedb_lib::relation::{self, ColExpr, DbTable, FieldName, Header};
1011
use spacetimedb_primitives::ColId;
12+
use spacetimedb_sats::satn::PsqlType;
13+
use spacetimedb_sats::{satn, ProductType, ProductValue, Typespace};
1114
use spacetimedb_schema::schema::TableSchema;
1215
use spacetimedb_vm::expr::{CrudExpr, Expr, FieldExpr, QueryExpr, SourceExpr};
1316
use spacetimedb_vm::operator::OpCmp;
1417
use std::sync::Arc;
1518

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

231+
/// Generates a [`tabled::Table`] from a schema and rows, using the style of a psql table.
232+
pub fn build_table<E>(
233+
schema: &ProductType,
234+
rows: impl Iterator<Item = Result<ProductValue, E>>,
235+
) -> Result<tabled::Table, E> {
236+
let mut builder = tabled::builder::Builder::default();
237+
builder.set_header(
238+
schema
239+
.elements
240+
.iter()
241+
.enumerate()
242+
.map(|(i, e)| e.name.clone().unwrap_or_else(|| format!("column {i}").into())),
243+
);
244+
245+
let ty = Typespace::EMPTY.with_type(schema);
246+
for row in rows {
247+
let row = row?;
248+
builder.push_record(ty.with_values(&row).enumerate().map(|(idx, value)| {
249+
let ty = PsqlType {
250+
tuple: ty.ty(),
251+
field: &ty.ty().elements[idx],
252+
idx,
253+
};
254+
255+
satn::PsqlWrapper { ty, value }.to_string()
256+
}));
257+
}
258+
259+
let mut table = builder.build();
260+
table.with(tabled::settings::Style::psql());
261+
262+
Ok(table)
263+
}
264+
230265
#[cfg(test)]
231266
mod tests {
232267
use super::*;
233268
use crate::db::datastore::traits::IsolationLevel;
234269
use crate::db::relational_db::tests_utils::{insert, TestDB};
235270
use crate::execution_context::Workload;
236271
use crate::sql::execute::tests::run_for_testing;
272+
use itertools::Itertools;
237273
use spacetimedb_lib::error::{ResultTest, TestError};
238274
use spacetimedb_lib::{ConnectionId, Identity};
239275
use spacetimedb_primitives::{col_list, ColList, TableId};
276+
use spacetimedb_sats::time_duration::TimeDuration;
277+
use spacetimedb_sats::timestamp::Timestamp;
240278
use spacetimedb_sats::{
241-
product, satn, AlgebraicType, AlgebraicValue, GroundSpacetimeType as _, ProductType, Typespace, ValueWithType,
279+
product, AlgebraicType, AlgebraicValue, GroundSpacetimeType as _, ProductType, ProductValue,
242280
};
243281
use spacetimedb_vm::expr::{ColumnOp, IndexJoin, IndexScan, JoinExpr, Query};
244282
use std::convert::From;
@@ -403,54 +441,100 @@ mod tests {
403441
Ok(())
404442
}
405443

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

451+
// Verify the output of `sql` matches the inputs that return true for [`AlgebraicType::is_special()`]
452+
#[test]
453+
fn output_special_types() -> ResultTest<()> {
419454
// Check tuples
420-
let kind = [
421-
("a", AlgebraicType::String),
422-
("b", AlgebraicType::U256),
423-
("o", Identity::get_type()),
424-
("p", ConnectionId::get_type()),
455+
let kind: ProductType = [
456+
AlgebraicType::String,
457+
AlgebraicType::U256,
458+
Identity::get_type(),
459+
ConnectionId::get_type(),
460+
Timestamp::get_type(),
461+
TimeDuration::get_type(),
425462
]
426463
.into();
464+
let value = product![
465+
"a",
466+
Identity::ZERO.to_u256(),
467+
Identity::ZERO,
468+
ConnectionId::ZERO,
469+
Timestamp::UNIX_EPOCH,
470+
TimeDuration::ZERO
471+
];
427472

428-
let value = AlgebraicValue::product([
429-
AlgebraicValue::String("a".into()),
430-
Identity::ZERO.to_u256().into(),
431-
Identity::ZERO.to_u256().into(),
432-
ConnectionId::ZERO.to_u128().into(),
433-
]);
434-
435-
assert_eq!(
436-
satn::PsqlWrapper { ty: &kind, value }.to_string().as_str(),
437-
"(0 = \"a\", 1 = 0, 2 = 0, 3 = 0)"
473+
expect_psql_table(
474+
&kind,
475+
vec![value],
476+
r#"
477+
column 0 | column 1 | column 2 | column 3 | column 4 | column 5
478+
----------+----------+--------------------------------------------------------------------+------------------------------------+---------------------------+-----------
479+
"a" | 0 | 0x0000000000000000000000000000000000000000000000000000000000000000 | 0x00000000000000000000000000000000 | 1970-01-01T00:00:00+00:00 | +0.000000"#,
438480
);
439481

440-
let ty = Typespace::EMPTY.with_type(&kind);
441-
442482
// Check struct
483+
let kind: ProductType = [
484+
("bool", AlgebraicType::Bool),
485+
("str", AlgebraicType::String),
486+
("bytes", AlgebraicType::bytes()),
487+
("identity", Identity::get_type()),
488+
("connection_id", ConnectionId::get_type()),
489+
("timestamp", Timestamp::get_type()),
490+
("duration", TimeDuration::get_type()),
491+
]
492+
.into();
493+
443494
let value = product![
444-
"a",
445-
Identity::ZERO.to_u256(),
446-
AlgebraicValue::product([Identity::ZERO.to_u256().into()]),
447-
AlgebraicValue::product([ConnectionId::ZERO.to_u128().into()]),
495+
true,
496+
"This is spacetimedb".to_string(),
497+
AlgebraicValue::Bytes([1, 2, 3, 4, 5, 6, 7].into()),
498+
Identity::ZERO,
499+
ConnectionId::ZERO,
500+
Timestamp::UNIX_EPOCH,
501+
TimeDuration::ZERO
448502
];
449503

450-
let value = ValueWithType::new(ty, &value);
451-
assert_eq!(
452-
satn::PsqlWrapper { ty: ty.ty(), value }.to_string().as_str(),
453-
"(a = \"a\", b = 0, o = 0, p = 0)"
504+
expect_psql_table(
505+
&kind,
506+
vec![value.clone()],
507+
r#"
508+
bool | str | bytes | identity | connection_id | timestamp | duration
509+
------+-----------------------+------------------+--------------------------------------------------------------------+------------------------------------+---------------------------+-----------
510+
true | "This is spacetimedb" | 0x01020304050607 | 0x0000000000000000000000000000000000000000000000000000000000000000 | 0x00000000000000000000000000000000 | 1970-01-01T00:00:00+00:00 | +0.000000"#,
511+
);
512+
513+
// Check nested struct, tuple...
514+
let kind: ProductType = [(None, AlgebraicType::product(kind))].into();
515+
516+
let value = product![value.clone()];
517+
518+
expect_psql_table(
519+
&kind,
520+
vec![value.clone()],
521+
r#"
522+
column 0
523+
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
524+
(bool = true, str = "This is spacetimedb", bytes = 0x01020304050607, identity = 0x0000000000000000000000000000000000000000000000000000000000000000, connection_id = 0x00000000000000000000000000000000, timestamp = 1970-01-01T00:00:00+00:00, duration = +0.000000)"#,
525+
);
526+
527+
let kind: ProductType = [("tuple", AlgebraicType::product(kind))].into();
528+
529+
let value = product![value];
530+
531+
expect_psql_table(
532+
&kind,
533+
vec![value],
534+
r#"
535+
tuple
536+
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
537+
(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))"#,
454538
);
455539

456540
Ok(())

crates/expr/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ where
194194
/// Parses a source text literal as a particular type
195195
pub(crate) fn parse(value: &str, ty: &AlgebraicType) -> anyhow::Result<AlgebraicValue> {
196196
let to_timestamp = || {
197-
Timestamp::parse_from_str(value)?
197+
Timestamp::parse_from_rfc3339(value)?
198198
.serialize(ValueSerializer)
199199
.with_context(|| "Could not parse timestamp")
200200
};

crates/sats/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ bitflags.workspace = true
3636
bytes.workspace = true
3737
bytemuck.workspace = true
3838
bytestring = { workspace = true, optional = true }
39-
chrono.workspace = true
39+
chrono = { workspace = true, features = ["alloc"] }
4040
decorum.workspace = true
4141
derive_more.workspace = true
4242
enum-as-inner.workspace = true

crates/sats/src/product_type.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,26 @@ impl ProductType {
121121
self.is_i64_newtype(TIME_DURATION_TAG)
122122
}
123123

124+
/// Returns whether this is the special tag of [`Identity`].
125+
pub fn is_identity_tag(tag_name: &str) -> bool {
126+
tag_name == IDENTITY_TAG
127+
}
128+
129+
/// Returns whether this is the special tag of [`ConnectionId`].
130+
pub fn is_connection_id_tag(tag_name: &str) -> bool {
131+
tag_name == CONNECTION_ID_TAG
132+
}
133+
134+
/// Returns whether this is the special tag of [`Timestamp`].
135+
pub fn is_timestamp_tag(tag_name: &str) -> bool {
136+
tag_name == TIMESTAMP_TAG
137+
}
138+
139+
/// Returns whether this is the special tag of [`TimeDuration`].
140+
pub fn is_time_duration_tag(tag_name: &str) -> bool {
141+
tag_name == TIME_DURATION_TAG
142+
}
143+
124144
/// Returns whether this is a special known `tag`,
125145
/// currently `Address`, `Identity`, `Timestamp` or `TimeDuration`.
126146
pub fn is_special_tag(tag_name: &str) -> bool {

0 commit comments

Comments
 (0)