Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 13b28387c2 | |||
| 5b8596f7bd | |||
| 6220a0964d | |||
| 6cd5bfea7e | |||
| a006d3f00a | |||
| 942520a36a | |||
| 1bb9586b9b | |||
| b4882bcb27 | |||
| 3bb7eb312a | |||
| 73ab6d4ce7 | |||
| cef65958cb |
36
GEMINI.md
36
GEMINI.md
@ -229,6 +229,40 @@ Traits are reusable, non-generating schema fragments used to share properties an
|
|||||||
* **Scalars / Arrays / Items**: Host definitions completely override included traits.
|
* **Scalars / Arrays / Items**: Host definitions completely override included traits.
|
||||||
* The `"include"` keyword is stripped, and `"traits"` maps are omitted from serialization.
|
* The `"include"` keyword is stripped, and `"traits"` maps are omitted from serialization.
|
||||||
|
|
||||||
|
### Static Relation Constraints (Kind Constraints)
|
||||||
|
|
||||||
|
When modeling relational properties on a schema, a developer can define a specialized subset of a related table by applying static property constraints via the `const` or `enum` validation keywords.
|
||||||
|
|
||||||
|
For example, given a general `attachment` table containing a `kind` column (e.g. `'cover'`, `'thumbnail'`, `'document'`), you can define a `cover.attachment` schema that narrows the type using a static `const` assertion:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"cover.attachment": {
|
||||||
|
"type": "attachment",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "cover"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
A parent entity can then define a relationship using this constrained schema under a local property name (e.g. `cover_attachment`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
"cover_attachment": {
|
||||||
|
"properties": {
|
||||||
|
"cover_attachment": {
|
||||||
|
"type": "cover.attachment"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
1. **Validation (L1)**: During payload validation (`jspg_validate`), any incoming object mapped to the constrained property is validated against the static rules (e.g. throwing `CONST_VIOLATED` if `kind` is not `"cover"`).
|
||||||
|
2. **Query Generation (L0)**: When fetching data via `jspg_query`, the Queryer automatically detects the static constraint and compiles it into the SQL subquery's `WHERE` clause (e.g. adding `AND attachment_X.kind = 'cover'`). This produces a pre-filtered view of the related entities natively at the database level.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Database
|
## 3. Database
|
||||||
@ -314,6 +348,8 @@ The Queryer transforms Postgres into a pre-compiled Semantic Query Engine, desig
|
|||||||
* **Multi-Table Branching**: If the Physical Table is a parent to other tables (e.g. `organization` has variations `["organization", "bot", "person"]`), the compiler generates a dynamic `CASE WHEN type = '...' THEN ...` query, expanding into sub-queries for each variation. To ensure safe resolution, the compiler dynamically evaluates correlation boundaries: it attempts standard Relational Edge discovery first. If no explicit relational edge exists (indicating pure Table Inheritance rather than a standard foreign-key graph relationship), it safely invokes a **Table Parity Fallback**. This generates an explicit ID correlation constraint (`AND inner.id = outer.id`), perfectly binding the structural variations back to the parent row to eliminate Cartesian products.
|
* **Multi-Table Branching**: If the Physical Table is a parent to other tables (e.g. `organization` has variations `["organization", "bot", "person"]`), the compiler generates a dynamic `CASE WHEN type = '...' THEN ...` query, expanding into sub-queries for each variation. To ensure safe resolution, the compiler dynamically evaluates correlation boundaries: it attempts standard Relational Edge discovery first. If no explicit relational edge exists (indicating pure Table Inheritance rather than a standard foreign-key graph relationship), it safely invokes a **Table Parity Fallback**. This generates an explicit ID correlation constraint (`AND inner.id = outer.id`), perfectly binding the structural variations back to the parent row to eliminate Cartesian products.
|
||||||
* **Single-Table Bypass**: If the Physical Table is a leaf node with only one variation (e.g. `person` has variations `["person"]`), the compiler cleanly bypasses `CASE` generation and compiles a simple `SELECT` across the base table, as all schema extensions (e.g. `light.person`, `full.person`) are guaranteed to reside in the exact same physical row.
|
* **Single-Table Bypass**: If the Physical Table is a leaf node with only one variation (e.g. `person` has variations `["person"]`), the compiler cleanly bypasses `CASE` generation and compiles a simple `SELECT` across the base table, as all schema extensions (e.g. `light.person`, `full.person`) are guaranteed to reside in the exact same physical row.
|
||||||
* **Polymorphic Relation Type Filtering**: When a relationship maps to a polymorphic target with variations, the Queryer compiles an `IN` clause containing all allowed table variations (e.g., `counterparty_type IN ('bot', 'organization', 'person')`) rather than matching the base type literal, ensuring all polymorphic types are loaded correctly.
|
* **Polymorphic Relation Type Filtering**: When a relationship maps to a polymorphic target with variations, the Queryer compiles an `IN` clause containing all allowed table variations (e.g., `counterparty_type IN ('bot', 'organization', 'person')`) rather than matching the base type literal, ensuring all polymorphic types are loaded correctly.
|
||||||
|
* **Static Relation Constraints (Kind Constraints)**: When a relationship (such as a nested object or array) is defined with a schema that constrains a field value statically using a `const` or `enum` keyword (for example, `kind` constrained to `"cover"` in a `cover_attachment`), the Queryer automatically extracts these static assertions during AST compilation. It injects them directly as static filters into the SQL subquery's `WHERE` clause (e.g. `AND attachment.kind = 'cover'`), allowing developers to query pre-filtered subsets of related tables natively through the schema.
|
||||||
|
* **Proxy Schema Dereferencing / Resolution**: To support punc endpoints that return non-polymorphic table-backed shapes (using `type: "full.X"` proxy schemas at the root response level), the Queryer compiler automatically dereferences non-table schema pointers to their target schemas prior to checking the types. This allows the Queryer to correctly resolve the table relationship edges pre-compiled on the full schema, while avoiding polluting the database registry with relations on ad-hoc punc response schemas during setup.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -87,34 +87,6 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"schedule": {
|
|
||||||
"type": [
|
|
||||||
"opening_hours",
|
|
||||||
"null"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"opening_hours": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"open": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"seasons": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "season"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"season": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"label": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -511,9 +483,7 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"opening_hours": {},
|
|
||||||
"season": {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -191,6 +191,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "request",
|
"name": "request",
|
||||||
|
"field_types": {
|
||||||
|
"inv": "jsonb"
|
||||||
|
},
|
||||||
"schemas": {
|
"schemas": {
|
||||||
"request": {
|
"request": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
1
flow
1
flow
@ -36,7 +36,6 @@ pgrx-down() {
|
|||||||
info "Taking pgrx down..."
|
info "Taking pgrx down..."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
local version
|
local version
|
||||||
version=$(get-version) || return $?
|
version=$(get-version) || return $?
|
||||||
|
|||||||
@ -165,16 +165,8 @@ impl Schema {
|
|||||||
} else if db.enums.contains_key(custom) {
|
} else if db.enums.contains_key(custom) {
|
||||||
Some(vec![format!("{}.condition", custom)])
|
Some(vec![format!("{}.condition", custom)])
|
||||||
} else {
|
} else {
|
||||||
// Only a Table-Backed boundary has a synthesized Composed Filter to proxy to.
|
// Assume anything else is a Relational cross-boundary that already has its own .filter dynamically built
|
||||||
// A Field-Backed JSONB Bubble has none — omit it like an inline object rather
|
Some(vec![format!("{}.filter", custom)])
|
||||||
// than emit a dangling proxy reference, which breaks eager consumers of the
|
|
||||||
// exported registry (downstream code generators).
|
|
||||||
let base = custom.split('.').next_back().unwrap_or(custom);
|
|
||||||
if db.types.contains_key(base) {
|
|
||||||
Some(vec![format!("{}.filter", custom)])
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
use crate::database::Database;
|
use crate::database::Database;
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
|
use serde_json::Value;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub struct Compiler<'a> {
|
pub struct Compiler<'a> {
|
||||||
@ -120,6 +121,21 @@ impl<'a> Compiler<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn compile_reference(&mut self, node: Node<'a>) -> Result<(String, String), String> {
|
fn compile_reference(&mut self, node: Node<'a>) -> Result<(String, String), String> {
|
||||||
|
// Handle Direct Refs via type pointer first
|
||||||
|
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &node.schema.obj.type_ {
|
||||||
|
if !crate::database::object::is_primitive_type(t) {
|
||||||
|
if !self.db.types.contains_key(t) {
|
||||||
|
// If it's just an ad-hoc struct ref, we should resolve it
|
||||||
|
if let Some(target_schema) = self.db.schemas.get(t).cloned() {
|
||||||
|
let mut ref_node = node.clone();
|
||||||
|
ref_node.schema = target_schema.clone();
|
||||||
|
ref_node.schema_id = Some(t.clone());
|
||||||
|
return self.compile_node(ref_node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Determine if this schema represents a Database Entity
|
// Determine if this schema represents a Database Entity
|
||||||
let mut resolved_type = None;
|
let mut resolved_type = None;
|
||||||
|
|
||||||
@ -149,16 +165,9 @@ impl<'a> Compiler<'a> {
|
|||||||
return self.compile_entity(type_def, node.clone(), false);
|
return self.compile_entity(type_def, node.clone(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle Direct Refs via type pointer
|
// Fallback error if the schema pointer was unresolved
|
||||||
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &node.schema.obj.type_ {
|
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &node.schema.obj.type_ {
|
||||||
if !crate::database::object::is_primitive_type(t) {
|
if !crate::database::object::is_primitive_type(t) {
|
||||||
// If it's just an ad-hoc struct ref, we should resolve it
|
|
||||||
if let Some(target_schema) = self.db.schemas.get(t).cloned() {
|
|
||||||
let mut ref_node = node.clone();
|
|
||||||
ref_node.schema = target_schema.clone();
|
|
||||||
ref_node.schema_id = Some(t.clone());
|
|
||||||
return self.compile_node(ref_node);
|
|
||||||
}
|
|
||||||
return Err(format!("Unresolved schema type pointer: {}", t));
|
return Err(format!("Unresolved schema type pointer: {}", t));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -234,17 +243,7 @@ impl<'a> Compiler<'a> {
|
|||||||
let where_clauses = self.compile_where_clause(r#type, &table_aliases, node)?;
|
let where_clauses = self.compile_where_clause(r#type, &table_aliases, node)?;
|
||||||
|
|
||||||
let selection = if is_array {
|
let selection = if is_array {
|
||||||
// Deterministic order: aggregation over an unordered heap made result
|
format!("COALESCE(jsonb_agg({}), '[]'::jsonb)", jsonb_obj_sql)
|
||||||
// order vary run-to-run (flaky reads, unstable grids). Creation order
|
|
||||||
// with id as the tiebreaker; the entity alias exists for every
|
|
||||||
// entity-family type.
|
|
||||||
match table_aliases.get("entity") {
|
|
||||||
Some(entity_alias) => format!(
|
|
||||||
"COALESCE(jsonb_agg({} ORDER BY {}.created_at, {}.id), '[]'::jsonb)",
|
|
||||||
jsonb_obj_sql, entity_alias, entity_alias
|
|
||||||
),
|
|
||||||
None => format!("COALESCE(jsonb_agg({}), '[]'::jsonb)", jsonb_obj_sql),
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
jsonb_obj_sql
|
jsonb_obj_sql
|
||||||
};
|
};
|
||||||
@ -559,8 +558,9 @@ impl<'a> Compiler<'a> {
|
|||||||
where_clauses.push(format!("NOT {}.archived", entity_alias));
|
where_clauses.push(format!("NOT {}.archived", entity_alias));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.compile_filter_conditions(r#type, type_aliases, &node, &base_alias, &mut where_clauses)?;
|
self.compile_filter_conditions(r#type, type_aliases, &node, &base_alias, &mut where_clauses);
|
||||||
self.compile_polymorphic_bounds(r#type, type_aliases, &node, &mut where_clauses);
|
self.compile_polymorphic_bounds(r#type, type_aliases, &node, &mut where_clauses);
|
||||||
|
self.compile_static_property_conditions(r#type, type_aliases, &node, &base_alias, &mut where_clauses);
|
||||||
|
|
||||||
let start_len = where_clauses.len();
|
let start_len = where_clauses.len();
|
||||||
self.compile_relation_conditions(
|
self.compile_relation_conditions(
|
||||||
@ -699,11 +699,6 @@ impl<'a> Compiler<'a> {
|
|||||||
|| pg_type.contains("int")
|
|| pg_type.contains("int")
|
||||||
|| pg_type == "real"
|
|| pg_type == "real"
|
||||||
|| pg_type == "double precision"
|
|| pg_type == "double precision"
|
||||||
// pg catalog typnames for real / double precision — the SQL
|
|
||||||
// names above never appear in field_types (they come from
|
|
||||||
// pg_type.typname), so these are what actually arrives
|
|
||||||
|| pg_type == "float4"
|
|
||||||
|| pg_type == "float8"
|
|
||||||
{
|
{
|
||||||
cast = "::numeric";
|
cast = "::numeric";
|
||||||
} else if pg_type == "text" || pg_type.contains("char") {
|
} else if pg_type == "text" || pg_type.contains("char") {
|
||||||
@ -730,7 +725,7 @@ impl<'a> Compiler<'a> {
|
|||||||
node: &Node,
|
node: &Node,
|
||||||
base_alias: &str,
|
base_alias: &str,
|
||||||
where_clauses: &mut Vec<String>,
|
where_clauses: &mut Vec<String>,
|
||||||
) -> Result<(), String> {
|
) {
|
||||||
for (i, filter_key) in self.filter_keys.iter().enumerate() {
|
for (i, filter_key) in self.filter_keys.iter().enumerate() {
|
||||||
let mut parts = filter_key.split(':');
|
let mut parts = filter_key.split(':');
|
||||||
let full_field_path = parts.next().unwrap_or(filter_key);
|
let full_field_path = parts.next().unwrap_or(filter_key);
|
||||||
@ -760,95 +755,6 @@ impl<'a> Compiler<'a> {
|
|||||||
let param_index = i + 1;
|
let param_index = i + 1;
|
||||||
let p_val = format!("${}#>>'{{}}'", param_index);
|
let p_val = format!("${}#>>'{{}}'", param_index);
|
||||||
|
|
||||||
// jsonb columns never type-check against the text parameter, and for
|
|
||||||
// array-valued properties (tag lists) the meaning of a condition is
|
|
||||||
// CONTAINMENT, not equality. Compile them explicitly; reject the rest
|
|
||||||
// loudly at compile time instead of failing at execution.
|
|
||||||
let is_jsonb = r#type
|
|
||||||
.field_types
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|v| v.as_object())
|
|
||||||
.and_then(|ft| ft.get(field_name))
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
== Some("jsonb");
|
|
||||||
if is_jsonb {
|
|
||||||
// The node schema may be a punc-response proxy without properties —
|
|
||||||
// fall back to the type's own schema. Compiled schemas normalize to
|
|
||||||
// the list form ("type": ["array"]), hand-written ones may use the
|
|
||||||
// single form — accept both.
|
|
||||||
let prop_schema = node
|
|
||||||
.schema
|
|
||||||
.obj
|
|
||||||
.properties
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|p| p.get(field_name))
|
|
||||||
.cloned()
|
|
||||||
.or_else(|| {
|
|
||||||
r#type
|
|
||||||
.schemas
|
|
||||||
.get(&r#type.name)
|
|
||||||
.and_then(|s| s.obj.properties.as_ref())
|
|
||||||
.and_then(|p| p.get(field_name))
|
|
||||||
.cloned()
|
|
||||||
});
|
|
||||||
let is_array_prop = prop_schema
|
|
||||||
.map(|ps| match &ps.obj.type_ {
|
|
||||||
Some(crate::database::object::SchemaTypeOrArray::Single(t)) => t == "array",
|
|
||||||
Some(crate::database::object::SchemaTypeOrArray::Multiple(ts)) => {
|
|
||||||
ts.iter().any(|t| t == "array") && ts.iter().all(|t| t == "array" || t == "null")
|
|
||||||
}
|
|
||||||
None => false,
|
|
||||||
})
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
if is_array_prop {
|
|
||||||
match op {
|
|
||||||
// "the array contains this value"
|
|
||||||
"$eq" => where_clauses.push(format!(
|
|
||||||
"{}.{} ? ({})",
|
|
||||||
filter_alias, field_name, p_val
|
|
||||||
)),
|
|
||||||
"$ne" => where_clauses.push(format!(
|
|
||||||
"NOT ({}.{} ? ({}))",
|
|
||||||
filter_alias, field_name, p_val
|
|
||||||
)),
|
|
||||||
// "the array contains ANY of these values"
|
|
||||||
"$of" => where_clauses.push(format!(
|
|
||||||
"{}.{} ?| ARRAY(SELECT jsonb_array_elements_text(({})::jsonb))",
|
|
||||||
filter_alias, field_name, p_val
|
|
||||||
)),
|
|
||||||
"$nof" => where_clauses.push(format!(
|
|
||||||
"NOT ({}.{} ?| ARRAY(SELECT jsonb_array_elements_text(({})::jsonb)))",
|
|
||||||
filter_alias, field_name, p_val
|
|
||||||
)),
|
|
||||||
other => {
|
|
||||||
return Err(format!(
|
|
||||||
"operator {} is not supported on array property '{}' (jsonb containment supports $eq/$ne/$of/$nof)",
|
|
||||||
other, field_name
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
match op {
|
|
||||||
"$eq" => where_clauses.push(format!(
|
|
||||||
"{}.{} = ({})::jsonb",
|
|
||||||
filter_alias, field_name, p_val
|
|
||||||
)),
|
|
||||||
"$ne" => where_clauses.push(format!(
|
|
||||||
"{}.{} != ({})::jsonb",
|
|
||||||
filter_alias, field_name, p_val
|
|
||||||
)),
|
|
||||||
other => {
|
|
||||||
return Err(format!(
|
|
||||||
"operator {} is not supported on jsonb property '{}' (only $eq/$ne)",
|
|
||||||
other, field_name
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if op == "$of" || op == "$nof" {
|
if op == "$of" || op == "$nof" {
|
||||||
let sql_op = if op == "$of" { "IN" } else { "NOT IN" };
|
let sql_op = if op == "$of" { "IN" } else { "NOT IN" };
|
||||||
let subquery = format!(
|
let subquery = format!(
|
||||||
@ -900,7 +806,6 @@ impl<'a> Compiler<'a> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compile_relation_conditions(
|
fn compile_relation_conditions(
|
||||||
@ -970,4 +875,69 @@ impl<'a> Compiler<'a> {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn compile_static_property_conditions(
|
||||||
|
&self,
|
||||||
|
r#type: &crate::database::r#type::Type,
|
||||||
|
type_aliases: &std::collections::HashMap<String, String>,
|
||||||
|
node: &Node,
|
||||||
|
base_alias: &str,
|
||||||
|
where_clauses: &mut Vec<String>,
|
||||||
|
) {
|
||||||
|
if let Some(props) = node.schema.obj.properties.as_ref() {
|
||||||
|
for (prop_name, prop_schema) in props {
|
||||||
|
let filter_alias = Self::resolve_filter_alias(r#type, type_aliases, base_alias, prop_name);
|
||||||
|
if let Some(const_val) = prop_schema.obj.const_.as_ref() {
|
||||||
|
let sql_val = Self::quote_literal(const_val);
|
||||||
|
where_clauses.push(format!("{}.{} = {}", filter_alias, prop_name, sql_val));
|
||||||
|
}
|
||||||
|
if let Some(enum_vals) = prop_schema.obj.enum_.as_ref() {
|
||||||
|
if !enum_vals.is_empty() {
|
||||||
|
let sql_vals: Vec<String> = enum_vals
|
||||||
|
.iter()
|
||||||
|
.map(|v| Self::quote_literal(v))
|
||||||
|
.collect();
|
||||||
|
where_clauses.push(format!(
|
||||||
|
"{}.{} IN ({})",
|
||||||
|
filter_alias,
|
||||||
|
prop_name,
|
||||||
|
sql_vals.join(", ")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn quote_literal(val: &Value) -> String {
|
||||||
|
match val {
|
||||||
|
Value::Null => "NULL".to_string(),
|
||||||
|
Value::Bool(b) => {
|
||||||
|
if *b {
|
||||||
|
"true".to_string()
|
||||||
|
} else {
|
||||||
|
"false".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Value::Number(n) => {
|
||||||
|
if let Some(f) = n.as_f64() {
|
||||||
|
if f.fract() == 0.0 {
|
||||||
|
return f.trunc().to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n.to_string()
|
||||||
|
}
|
||||||
|
Value::String(s) => {
|
||||||
|
if s.is_empty() {
|
||||||
|
"NULL".to_string()
|
||||||
|
} else {
|
||||||
|
format!("'{}'", s.replace('\'', "''"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => format!(
|
||||||
|
"'{}'",
|
||||||
|
serde_json::to_string(val).unwrap().replace('\'', "''")
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1302,15 +1302,9 @@ fn test_queryer_0_15() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_queryer_0_16() {
|
fn test_queryer_1_0() {
|
||||||
let path = format!("{}/fixtures/queryer.json", env!("CARGO_MANIFEST_DIR"));
|
let path = format!("{}/fixtures/queryer.json", env!("CARGO_MANIFEST_DIR"));
|
||||||
crate::tests::runner::run_test_case(&path, 0, 16).unwrap();
|
crate::tests::runner::run_test_case(&path, 1, 0).unwrap();
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_queryer_0_17() {
|
|
||||||
let path = format!("{}/fixtures/queryer.json", env!("CARGO_MANIFEST_DIR"));
|
|
||||||
crate::tests::runner::run_test_case(&path, 0, 17).unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@ -268,7 +268,8 @@ impl SqlFormatter {
|
|||||||
match &join.join_operator {
|
match &join.join_operator {
|
||||||
JoinOperator::Inner(JoinConstraint::On(expr))
|
JoinOperator::Inner(JoinConstraint::On(expr))
|
||||||
| JoinOperator::Left(JoinConstraint::On(expr))
|
| JoinOperator::Left(JoinConstraint::On(expr))
|
||||||
| JoinOperator::Right(JoinConstraint::On(expr)) => {
|
| JoinOperator::Right(JoinConstraint::On(expr))
|
||||||
|
| JoinOperator::Join(JoinConstraint::On(expr)) => {
|
||||||
self.push_str(" ON ");
|
self.push_str(" ON ");
|
||||||
self.format_expr(expr);
|
self.format_expr(expr);
|
||||||
}
|
}
|
||||||
@ -389,7 +390,6 @@ impl SqlFormatter {
|
|||||||
i += 2;
|
i += 2;
|
||||||
}
|
}
|
||||||
self.indent -= 2;
|
self.indent -= 2;
|
||||||
self.format_function_clauses(list);
|
|
||||||
self.push_line(")");
|
self.push_line(")");
|
||||||
} else {
|
} else {
|
||||||
for (i, arg) in list.args.iter().enumerate() {
|
for (i, arg) in list.args.iter().enumerate() {
|
||||||
@ -397,7 +397,6 @@ impl SqlFormatter {
|
|||||||
self.format_function_arg(arg);
|
self.format_function_arg(arg);
|
||||||
self.push_str(comma);
|
self.push_str(comma);
|
||||||
}
|
}
|
||||||
self.format_function_clauses(list);
|
|
||||||
self.push_str(")");
|
self.push_str(")");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -405,25 +404,6 @@ impl SqlFormatter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aggregate clauses (e.g. jsonb_agg(x ORDER BY y)) — without this the
|
|
||||||
// snapshot silently drops the ORDER BY the compiler emits.
|
|
||||||
fn format_function_clauses(&mut self, list: &sqlparser::ast::FunctionArgumentList) {
|
|
||||||
for clause in &list.clauses {
|
|
||||||
if let sqlparser::ast::FunctionArgumentClause::OrderBy(order) = clause {
|
|
||||||
self.push_str(" ORDER BY ");
|
|
||||||
for (i, ob) in order.iter().enumerate() {
|
|
||||||
if i > 0 {
|
|
||||||
self.push_str(", ");
|
|
||||||
}
|
|
||||||
self.format_expr(&ob.expr);
|
|
||||||
if let Some(asc) = ob.options.asc {
|
|
||||||
self.push_str(if asc { " ASC" } else { " DESC" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn format_function_arg(&mut self, arg: &FunctionArg) {
|
fn format_function_arg(&mut self, arg: &FunctionArg) {
|
||||||
match arg {
|
match arg {
|
||||||
FunctionArg::Unnamed(sqlparser::ast::FunctionArgExpr::Expr(expr)) => self.format_expr(expr),
|
FunctionArg::Unnamed(sqlparser::ast::FunctionArgExpr::Expr(expr)) => self.format_expr(expr),
|
||||||
|
|||||||
@ -259,3 +259,25 @@ pub fn update_sql_fixture(path: &str, suite_idx: usize, case_idx: usize, queries
|
|||||||
let formatted_json = serde_json::to_string_pretty(&file_data).unwrap();
|
let formatted_json = serde_json::to_string_pretty(&file_data).unwrap();
|
||||||
fs::write(path, formatted_json).unwrap();
|
fs::write(path, formatted_json).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update_schemas_fixture(path: &str, suite_idx: usize, case_idx: usize, db: &crate::database::Database) {
|
||||||
|
let content = fs::read_to_string(path).unwrap();
|
||||||
|
let mut file_data: Value = serde_json::from_str(&content).unwrap();
|
||||||
|
|
||||||
|
if let Some(expect) = file_data[suite_idx]["tests"][case_idx].get_mut("expect") {
|
||||||
|
if let Some(schemas_map) = expect.get_mut("schemas").and_then(|v| v.as_object_mut()) {
|
||||||
|
for (key, expected_val) in schemas_map {
|
||||||
|
if expected_val.is_object() && expected_val.as_object().unwrap().is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(actual_ast) = db.schemas.get(key) {
|
||||||
|
let actual_val = serde_json::to_value(actual_ast).unwrap();
|
||||||
|
*expected_val = actual_val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let formatted_json = serde_json::to_string_pretty(&file_data).unwrap();
|
||||||
|
fs::write(path, formatted_json).unwrap();
|
||||||
|
}
|
||||||
|
|||||||
@ -57,6 +57,9 @@ impl Case {
|
|||||||
|
|
||||||
if env::var("UPDATE_EXPECT").is_ok() {
|
if env::var("UPDATE_EXPECT").is_ok() {
|
||||||
update_validation_fixture(path, suite_idx, case_idx, &result.errors);
|
update_validation_fixture(path, suite_idx, case_idx, &result.errors);
|
||||||
|
if let Ok(db) = db_res {
|
||||||
|
crate::tests::runner::update_schemas_fixture(path, suite_idx, case_idx, db);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
expect.assert_drop(&result)?;
|
expect.assert_drop(&result)?;
|
||||||
|
|||||||
Reference in New Issue
Block a user