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
115 changes: 69 additions & 46 deletions crates/sqlbuild-core/src/json/_helpers/emitter.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Serialize a [`JsonValue`] in the layout of one Python serializer.

use crate::json::_helpers::floats::{orjson_text, python_repr};
use crate::json::_helpers::strings::{StringEscaping, json_string, json_utf16_string};
use crate::json::_helpers::strings::{StringEscaping, with_json_string, with_json_utf16_string};
use crate::json::constants::{
ORJSON_MAX_CONTAINER_DEPTH, ORJSON_MAX_INTEGER, ORJSON_MIN_INTEGER, PYTHON_INT_MAX_STR_DIGITS,
STDLIB_MAX_CONTAINER_DEPTH,
Expand Down Expand Up @@ -48,9 +48,14 @@ impl<'options> Layout<'options> {
}
}

fn newline(&self, level: usize) -> String {
self.indent
.map_or_else(String::new, |indent| format!("\n{}", indent.repeat(level)))
fn with_newline(&self, mut output: String, level: usize) -> String {
if let Some(indent) = self.indent {
output.push('\n');
for _ in 0..level {
output.push_str(indent);
}
}
output
}

fn integer(&self, value: &JsonInteger) -> Result<String, JsonEmitError> {
Expand Down Expand Up @@ -94,67 +99,85 @@ impl<'options> Layout<'options> {
counted && level >= limit
}

fn value(&self, value: &JsonValue, level: usize) -> Result<String, JsonEmitError> {
fn with_value(
&self,
mut output: String,
value: &JsonValue,
level: usize,
) -> Result<String, JsonEmitError> {
if self.too_deep(value, level) {
return Err(JsonEmitError::NestingTooDeep);
}
match value {
JsonValue::Null => Ok("null".to_owned()),
JsonValue::Bool(flag) => Ok(flag.to_string()),
JsonValue::Integer(number) => self.integer(number),
JsonValue::Float(number) => self.float(*number),
JsonValue::String(text) => Ok(json_string(text, self.escaping)),
JsonValue::Utf16(units) => json_utf16_string(units, self.escaping),
JsonValue::Array(items) => {
let members = items
.iter()
.map(|item| self.value(item, level + 1))
.collect::<Result<Vec<_>, _>>()?;
Ok(self.container(('[', ']'), members, level))
JsonValue::Null => output.push_str("null"),
JsonValue::Bool(flag) => output.push_str(if *flag { "true" } else { "false" }),
JsonValue::Integer(number) => output.push_str(&self.integer(number)?),
JsonValue::Float(number) => output.push_str(&self.float(*number)?),
JsonValue::String(text) => return Ok(with_json_string(output, text, self.escaping)),
JsonValue::Utf16(units) => {
return with_json_utf16_string(output, units, self.escaping);
}
JsonValue::Object(entries) => self.object(entries, level),
JsonValue::Array(items) => return self.with_array(output, items, level),
JsonValue::Object(entries) => return self.with_object(output, entries, level),
}
Ok(output)
}

/// `output` followed by the text before the member at `index` of a container at `level`.
fn with_separator(&self, mut output: String, index: usize, level: usize) -> String {
if index > 0 {
output.push_str(self.item_separator);
}
self.with_newline(output, level + 1)
}

fn with_array(
&self,
mut output: String,
items: &[JsonValue],
level: usize,
) -> Result<String, JsonEmitError> {
if items.is_empty() {
output.push_str("[]");
return Ok(output);
}
output.push('[');
for (index, item) in items.iter().enumerate() {
output = self.with_separator(output, index, level);
output = self.with_value(output, item, level + 1)?;
}
output = self.with_newline(output, level);
output.push(']');
Ok(output)
}

fn object(
fn with_object(
&self,
mut output: String,
entries: &[(String, JsonValue)],
level: usize,
) -> Result<String, JsonEmitError> {
if entries.is_empty() {
output.push_str("{}");
return Ok(output);
}
let mut ordered: Vec<&(String, JsonValue)> = entries.iter().collect();
if self.sort_keys {
ordered.sort_by(|left, right| left.0.cmp(&right.0));
}
let members = ordered
.into_iter()
.map(|(key, item)| {
Ok(format!(
"{}{}{}",
json_string(key, self.escaping),
self.key_separator,
self.value(item, level + 1)?
))
})
.collect::<Result<Vec<_>, _>>()?;
Ok(self.container(('{', '}'), members, level))
}

fn container(&self, brackets: (char, char), members: Vec<String>, level: usize) -> String {
if members.is_empty() {
return format!("{}{}", brackets.0, brackets.1);
output.push('{');
for (index, (key, item)) in ordered.into_iter().enumerate() {
output = self.with_separator(output, index, level);
output = with_json_string(output, key, self.escaping);
output.push_str(self.key_separator);
output = self.with_value(output, item, level + 1)?;
}
let opening = self.newline(level + 1);
let separator = format!("{}{opening}", self.item_separator);
let closing = self.newline(level);
format!(
"{}{opening}{}{closing}{}",
brackets.0,
members.join(&separator),
brackets.1
)
output = self.with_newline(output, level);
output.push('}');
Ok(output)
}
}

pub(crate) fn emit(value: &JsonValue, dialect: &JsonDialect) -> Result<String, JsonEmitError> {
Layout::new(dialect).value(value, 0)
Layout::new(dialect).with_value(String::new(), value, 0)
}
93 changes: 53 additions & 40 deletions crates/sqlbuild-core/src/json/_helpers/strings.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//! JSON string escaping of `json.dumps` and orjson.

use std::fmt::Write;

use crate::json::constants::FIRST_PRINTABLE_ASCII;
use crate::json::errors::JsonEmitError;

Expand All @@ -12,67 +14,78 @@ pub(crate) enum StringEscaping {
Unicode,
}

fn unicode_escape(unit: u32) -> String {
format!("\\u{unit:04x}")
fn with_unicode_escape(mut output: String, unit: u32) -> String {
let _ = write!(output, "\\u{unit:04x}");
output
}

fn escaped(character: char, escaping: StringEscaping) -> String {
fn with_escaped(mut output: String, character: char, escaping: StringEscaping) -> String {
match character {
'"' => "\\\"".to_owned(),
'\\' => "\\\\".to_owned(),
'\n' => "\\n".to_owned(),
'\r' => "\\r".to_owned(),
'\t' => "\\t".to_owned(),
'\u{8}' => "\\b".to_owned(),
'\u{c}' => "\\f".to_owned(),
' '..='~' => character.to_string(),
_ if character < FIRST_PRINTABLE_ASCII => unicode_escape(u32::from(character)),
_ if escaping == StringEscaping::Unicode => character.to_string(),
_ => character
.encode_utf16(&mut [0_u16; 2])
.iter()
.map(|unit| unicode_escape(u32::from(*unit)))
.collect(),
'"' => output.push_str("\\\""),
'\\' => output.push_str("\\\\"),
'\n' => output.push_str("\\n"),
'\r' => output.push_str("\\r"),
'\t' => output.push_str("\\t"),
'\u{8}' => output.push_str("\\b"),
'\u{c}' => output.push_str("\\f"),
' '..='~' => output.push(character),
_ if character < FIRST_PRINTABLE_ASCII => {
return with_unicode_escape(output, u32::from(character));
}
_ if escaping == StringEscaping::Unicode => output.push(character),
_ => {
for unit in character.encode_utf16(&mut [0_u16; 2]).iter() {
output = with_unicode_escape(output, u32::from(*unit));
}
}
}
output
}

/// Whether `byte` is printable ASCII that a JSON string carries unescaped.
fn plain(byte: u8) -> bool {
(b' '..=b'~').contains(&byte) && byte != b'"' && byte != b'\\'
}

/// `text` as a quoted JSON string.
pub(crate) fn json_string(text: &str, escaping: StringEscaping) -> String {
let mut output = String::with_capacity(text.len() + 2);
/// `output` followed by `text` as a quoted JSON string.
pub(crate) fn with_json_string(mut output: String, text: &str, escaping: StringEscaping) -> String {
output.reserve(text.len() + 2);
output.push('"');
for character in text.chars() {
match character {
' '..='~' if character != '"' && character != '\\' => output.push(character),
_ => output.push_str(&escaped(character, escaping)),
}
let mut rest: &str = text;
while !rest.is_empty() {
let run: usize = rest
.bytes()
.position(|byte| !plain(byte))
.unwrap_or(rest.len());
output.push_str(&rest[..run]);
rest = &rest[run..];
let Some(character) = rest.chars().next() else {
break;
};
output = with_escaped(output, character, escaping);
rest = &rest[character.len_utf8()..];
}
output.push('"');
output
}

/// UTF-16 `units` as a quoted JSON string; `ensure_ascii` escapes a lone surrogate as `\uXXXX`.
pub(crate) fn json_utf16_string(
/// `output` followed by UTF-16 `units` as a quoted JSON string; `ensure_ascii` escapes lone surrogates.
pub(crate) fn with_json_utf16_string(
mut output: String,
units: &[u16],
escaping: StringEscaping,
) -> Result<String, JsonEmitError> {
let mut output = String::with_capacity(units.len() + 2);
output.reserve(units.len() + 2);
output.push('"');
for decoded in char::decode_utf16(units.iter().copied()) {
match (decoded, escaping) {
(Ok(character), _) => output.push_str(&escaped_or_plain(character, escaping)),
output = match (decoded, escaping) {
(Ok(character), _) => with_escaped(output, character, escaping),
(Err(lone), StringEscaping::Ascii) => {
output.push_str(&unicode_escape(u32::from(lone.unpaired_surrogate())));
with_unicode_escape(output, u32::from(lone.unpaired_surrogate()))
}
(Err(_), StringEscaping::Unicode) => return Err(JsonEmitError::LoneSurrogate),
}
};
}
output.push('"');
Ok(output)
}

fn escaped_or_plain(character: char, escaping: StringEscaping) -> String {
match character {
' '..='~' if character != '"' && character != '\\' => character.to_string(),
_ => escaped(character, escaping),
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//! The compile-lifetime binding catalog: known relation shapes around one native project catalog.

use std::sync::atomic::{AtomicBool, Ordering};

use pyo3::exceptions::{PyTypeError, PyValueError};
use pyo3::prelude::{
Bound, Py, PyAny, PyAnyMethods, PyDictMethods, PyModule, PyModuleMethods, PyResult, Python,
Expand Down Expand Up @@ -50,12 +52,19 @@ pub(crate) struct BindingCatalog {
pub(crate) expression_shapes: Py<PyDict>,
#[pyo3(get)]
pub(crate) native: Py<ProjectCatalog>,
/// Whether binding preparation has added relations to `schemas`.
pub(crate) extended: AtomicBool,
}

/// A catalog's dialect, case mode, known functions and known types.
pub(crate) type CatalogKey = (String, bool, Vec<String>, Vec<String>);

impl BindingCatalog {
/// Whether `schemas` holds exactly the relations it was created with and later given.
pub(crate) fn unextended(&self) -> bool {
!self.extended.load(Ordering::Relaxed)
}

/// A catalog for `key` that starts with `relations`.
pub(crate) fn create(
py: Python<'_>,
Expand All @@ -80,6 +89,7 @@ impl BindingCatalog {
quoted_ignore_case,
analysis_shapes: PyDict::new(py).unbind(),
expression_shapes: PyDict::new(py).unbind(),
extended: AtomicBool::new(false),
native: Py::new(py, native)?,
})
}
Expand All @@ -103,6 +113,7 @@ impl BindingCatalog {
}
}
if !additions.is_empty() {
self.extended.store(true, Ordering::Relaxed);
let relations: Relations = conversions::relations(additions.as_any())?;
self.native.borrow_mut(py).inner.update_relations(relations);
}
Expand Down Expand Up @@ -207,6 +218,7 @@ impl BindingCatalog {
analysis_shapes: self.analysis_shapes.bind(py).copy()?.unbind(),
expression_shapes: self.expression_shapes.bind(py).copy()?.unbind(),
native: Py::new(py, native)?,
extended: AtomicBool::new(true),
})
}

Expand Down
Loading
Loading