diff --git a/.github/workflows/rust.yaml b/.github/workflows/rust.yaml index a815e0e4..2db4185a 100644 --- a/.github/workflows/rust.yaml +++ b/.github/workflows/rust.yaml @@ -66,6 +66,9 @@ jobs: - name: Run tests run: cargo test -- --test-threads=1 + env: + MYSQL_VERSION: ${{ matrix.db.mysql }} + PG_VERSION: ${{ matrix.db.postgres }} lint: runs-on: ubuntu-latest diff --git a/docker-compose.yml b/docker-compose.yml index 99f07597..2aae8c6b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,7 @@ version: '3.1' services: postgres: + platform: linux/amd64 image: postgres:${PG_VERSION:-13} restart: always environment: @@ -13,6 +14,7 @@ services: - 54321:5432 mysql: + platform: linux/amd64 image: mysql:${MYSQL_VERSION:-8} restart: always volumes: diff --git a/playpen/db/mysql_migration.sql b/playpen/db/mysql_migration.sql index 51f81fbb..ca03f912 100644 --- a/playpen/db/mysql_migration.sql +++ b/playpen/db/mysql_migration.sql @@ -172,3 +172,54 @@ CREATE TABLE random ( -- JSON types json1 JSON ); + +-- JSON Test Data Table +-- This table contains various JSON structures for testing JSON operators and functions +CREATE TABLE json_test_data ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + data JSON NOT NULL, + metadata JSON, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +INSERT INTO json_test_data (name, data, metadata) VALUES +-- Simple object +('user_profile', + '{"userId": 1, "username": "john_doe", "email": "john@example.com", "age": 30, "active": true}', + '{"source": "api", "version": "1.0"}'), + +-- Nested object with address +('user_with_address', + '{"userId": 2, "username": "jane_smith", "email": "jane@example.com", "address": {"street": "123 Main St", "city": "Springfield", "state": "IL", "zipCode": "62701", "country": "USA"}}', + '{"source": "import", "version": "1.0"}'), + +-- Array of items +('shopping_cart', + '{"cartId": 101, "items": [{"productId": 1, "name": "Laptop", "quantity": 1, "price": 999.99}, {"productId": 2, "name": "Mouse", "quantity": 2, "price": 25.50}], "totalPrice": 1050.99}', + '{"source": "web", "version": "2.0"}'), + +-- Array of strings +('tags', + '{"postId": 42, "title": "MySQL JSON Functions", "tags": ["database", "mysql", "json", "tutorial"], "published": true}', + '{"source": "cms", "version": "1.0"}'), + +-- Nested arrays and objects +('game_stats', + '{"playerId": 123, "stats": {"level": 50, "experience": 125000, "inventory": [{"slot": 1, "item": "Sword of Fire", "rarity": "legendary"}, {"slot": 2, "item": "Shield of Light", "rarity": "epic"}], "achievements": ["First Kill", "Level 50", "Legendary Item"]}}', + '{"source": "game_server", "version": "3.0"}'), + +-- Deep nesting +('nested_config', + '{"app": {"name": "MyApp", "version": "1.0.0", "settings": {"database": {"host": "localhost", "port": 3306, "credentials": {"username": "admin", "encrypted": true}}, "features": {"darkMode": true, "notifications": {"email": true, "push": false}}}}}', + '{"source": "config", "version": "1.0"}'), + +-- Array of objects with nulls +('product_reviews', + '{"productId": 456, "reviews": [{"reviewId": 1, "rating": 5, "comment": "Excellent product!", "reviewer": "Alice"}, {"reviewId": 2, "rating": 4, "comment": null, "reviewer": "Bob"}, {"reviewId": 3, "rating": 3, "comment": "Average", "reviewer": null}]}', + '{"source": "reviews", "version": "1.0"}'), + +-- Mixed types +('analytics', + '{"date": "2024-01-15", "metrics": {"visitors": 1500, "pageViews": 4500, "bounceRate": 0.35, "sources": {"organic": 850, "direct": 400, "referral": 250}}}', + '{"source": "analytics", "version": "1.0"}'); diff --git a/playpen/db/mysql_migration_5_6.sql b/playpen/db/mysql_migration_5_6.sql index ef5cc6c1..0c805fe2 100644 --- a/playpen/db/mysql_migration_5_6.sql +++ b/playpen/db/mysql_migration_5_6.sql @@ -158,3 +158,54 @@ CREATE TABLE random ( json1 TEXT ); + +-- JSON Test Data Table +-- This table contains various JSON structures for testing JSON operators and functions +CREATE TABLE json_test_data ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + data TEXT NOT NULL, + metadata TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +INSERT INTO json_test_data (name, data, metadata) VALUES +-- Simple object +('user_profile', + '{"userId": 1, "username": "john_doe", "email": "john@example.com", "age": 30, "active": true}', + '{"source": "api", "version": "1.0"}'), + +-- Nested object with address +('user_with_address', + '{"userId": 2, "username": "jane_smith", "email": "jane@example.com", "address": {"street": "123 Main St", "city": "Springfield", "state": "IL", "zipCode": "62701", "country": "USA"}}', + '{"source": "import", "version": "1.0"}'), + +-- Array of items +('shopping_cart', + '{"cartId": 101, "items": [{"productId": 1, "name": "Laptop", "quantity": 1, "price": 999.99}, {"productId": 2, "name": "Mouse", "quantity": 2, "price": 25.50}], "totalPrice": 1050.99}', + '{"source": "web", "version": "2.0"}'), + +-- Array of strings +('tags', + '{"postId": 42, "title": "MySQL JSON Functions", "tags": ["database", "mysql", "json", "tutorial"], "published": true}', + '{"source": "cms", "version": "1.0"}'), + +-- Nested arrays and objects +('game_stats', + '{"playerId": 123, "stats": {"level": 50, "experience": 125000, "inventory": [{"slot": 1, "item": "Sword of Fire", "rarity": "legendary"}, {"slot": 2, "item": "Shield of Light", "rarity": "epic"}], "achievements": ["First Kill", "Level 50", "Legendary Item"]}}', + '{"source": "game_server", "version": "3.0"}'), + +-- Deep nesting +('nested_config', + '{"app": {"name": "MyApp", "version": "1.0.0", "settings": {"database": {"host": "localhost", "port": 3306, "credentials": {"username": "admin", "encrypted": true}}, "features": {"darkMode": true, "notifications": {"email": true, "push": false}}}}}', + '{"source": "config", "version": "1.0"}'), + +-- Array of objects with nulls +('product_reviews', + '{"productId": 456, "reviews": [{"reviewId": 1, "rating": 5, "comment": "Excellent product!", "reviewer": "Alice"}, {"reviewId": 2, "rating": 4, "comment": null, "reviewer": "Bob"}, {"reviewId": 3, "rating": 3, "comment": "Average", "reviewer": null}]}', + '{"source": "reviews", "version": "1.0"}'), + +-- Mixed types +('analytics', + '{"date": "2024-01-15", "metrics": {"visitors": 1500, "pageViews": 4500, "bounceRate": 0.35, "sources": {"organic": 850, "direct": 400, "referral": 250}}}', + '{"source": "analytics", "version": "1.0"}'); diff --git a/playpen/db/postgres_migration.sql b/playpen/db/postgres_migration.sql index 7553bdfb..5caf844a 100644 --- a/playpen/db/postgres_migration.sql +++ b/playpen/db/postgres_migration.sql @@ -218,3 +218,54 @@ INSERT INTO classes (name, specialization) VALUES ('druid', '{"role": "hybrid", "weapon": "staff", "abilities": ["shapeshift", "moonfire", "regrowth"]}'), ('mage', '{"role": "ranged", "weapon": "wand", "abilities": ["fireball", "frostbolt", "arcane blast"]}'), ('warlock', '{"role": "ranged", "weapon": "dagger", "abilities": ["summon demon", "shadowbolt", "curse of agony"]}'); + +-- JSON Test Data Table +-- This table contains various JSON structures for testing JSON operators and functions +CREATE TABLE json_test_data ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + data JSONB NOT NULL, + metadata JSON, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +INSERT INTO json_test_data (name, data, metadata) VALUES +-- Simple object +('user_profile', + '{"userId": 1, "username": "john_doe", "email": "john@example.com", "age": 30, "active": true}', + '{"source": "api", "version": "1.0"}'), + +-- Nested object with address +('user_with_address', + '{"userId": 2, "username": "jane_smith", "email": "jane@example.com", "address": {"street": "123 Main St", "city": "Springfield", "state": "IL", "zipCode": "62701", "country": "USA"}}', + '{"source": "import", "version": "1.0"}'), + +-- Array of items +('shopping_cart', + '{"cartId": 101, "items": [{"productId": 1, "name": "Laptop", "quantity": 1, "price": 999.99}, {"productId": 2, "name": "Mouse", "quantity": 2, "price": 25.50}], "totalPrice": 1050.99}', + '{"source": "web", "version": "2.0"}'), + +-- Array of strings +('tags', + '{"postId": 42, "title": "PostgreSQL JSON Functions", "tags": ["database", "postgresql", "json", "tutorial"], "published": true}', + '{"source": "cms", "version": "1.0"}'), + +-- Nested arrays and objects +('game_stats', + '{"playerId": 123, "stats": {"level": 50, "experience": 125000, "inventory": [{"slot": 1, "item": "Sword of Fire", "rarity": "legendary"}, {"slot": 2, "item": "Shield of Light", "rarity": "epic"}], "achievements": ["First Kill", "Level 50", "Legendary Item"]}}', + '{"source": "game_server", "version": "3.0"}'), + +-- Deep nesting +('nested_config', + '{"app": {"name": "MyApp", "version": "1.0.0", "settings": {"database": {"host": "localhost", "port": 5432, "credentials": {"username": "admin", "encrypted": true}}, "features": {"darkMode": true, "notifications": {"email": true, "push": false}}}}}', + '{"source": "config", "version": "1.0"}'), + +-- Array of objects with nulls +('product_reviews', + '{"productId": 456, "reviews": [{"reviewId": 1, "rating": 5, "comment": "Excellent product!", "reviewer": "Alice"}, {"reviewId": 2, "rating": 4, "comment": null, "reviewer": "Bob"}, {"reviewId": 3, "rating": 3, "comment": "Average", "reviewer": null}]}', + '{"source": "reviews", "version": "1.0"}'), + +-- Mixed types +('analytics', + '{"date": "2024-01-15", "metrics": {"visitors": 1500, "pageViews": 4500, "bounceRate": 0.35, "sources": {"organic": 850, "direct": 400, "referral": 250}}}', + '{"source": "analytics", "version": "1.0"}'); diff --git a/src/ts_generator/sql_parser/expressions/function_handlers/json_functions.rs b/src/ts_generator/sql_parser/expressions/function_handlers/json_functions.rs new file mode 100644 index 00000000..49ffb205 --- /dev/null +++ b/src/ts_generator/sql_parser/expressions/function_handlers/json_functions.rs @@ -0,0 +1,205 @@ +use crate::common::lazy::DB_SCHEMA; +use crate::core::connection::DBConn; +use crate::ts_generator::errors::TsGeneratorError; +use crate::ts_generator::sql_parser::expressions::function_handlers::FunctionHandlersContext; +use crate::ts_generator::sql_parser::expressions::translate_data_type::translate_value; +use crate::ts_generator::sql_parser::expressions::translate_table_with_joins::translate_table_from_expr; +use crate::ts_generator::sql_parser::quoted_strings::DisplayIndent; +use crate::ts_generator::types::ts_query::TsFieldType; +use sqlparser::ast::{Expr, FunctionArg, FunctionArgExpr, TableWithJoins, Value}; + +/// Extract key name from a function argument (should be a string literal) +fn extract_key_name(arg: &FunctionArg) -> Option { + match arg { + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(val))) => match &val.value { + Value::SingleQuotedString(s) | Value::DoubleQuotedString(s) => Some(s.clone()), + _ => None, + }, + _ => None, + } +} + +/// Extract expression from a function argument +fn extract_expr_from_arg(arg: &FunctionArg) -> Option<&Expr> { + match arg { + FunctionArg::Unnamed(FunctionArgExpr::Expr(expr)) => Some(expr), + FunctionArg::Named { + arg: FunctionArgExpr::Expr(expr), + .. + } => Some(expr), + _ => None, + } +} + +/// Infer the TypeScript type from an SQL expression +pub async fn infer_type_from_expr( + expr: &Expr, + single_table_name: &Option<&str>, + table_with_joins: &Option>, + db_conn: &DBConn, +) -> Option<(TsFieldType, bool)> { + match expr { + Expr::Identifier(ident) => { + let column_name = DisplayIndent(ident).to_string(); + if let Some(table_name) = single_table_name { + let table_details = DB_SCHEMA.lock().await.fetch_table(&vec![table_name], db_conn).await; + + if let Some(table_details) = table_details { + if let Some(field) = table_details.get(&column_name) { + Some((field.field_type.to_owned(), field.is_nullable)) + } else { + Some((TsFieldType::Any, false)) + } + } else { + Some((TsFieldType::Any, false)) + } + } else { + Some((TsFieldType::Any, false)) + } + } + Expr::CompoundIdentifier(idents) if idents.len() == 2 => { + let column_name = DisplayIndent(&idents[1]).to_string(); + if let Ok(table_name) = translate_table_from_expr(table_with_joins, expr) { + let table_details = DB_SCHEMA + .lock() + .await + .fetch_table(&vec![table_name.as_str()], db_conn) + .await; + + if let Some(table_details) = table_details { + if let Some(field) = table_details.get(&column_name) { + Some((field.field_type.to_owned(), field.is_nullable)) + } else { + Some((TsFieldType::Any, false)) + } + } else { + Some((TsFieldType::Any, false)) + } + } else { + Some((TsFieldType::Any, false)) + } + } + Expr::Value(val) => { + if let Some(ts_field_type) = translate_value(&val.value) { + Some((ts_field_type, false)) + } else { + Some((TsFieldType::Any, false)) + } + } + _ => Some((TsFieldType::Any, false)), + } +} + +/// Process key-value pairs from JSON build object arguments +pub async fn process_json_build_object_args( + args: &[FunctionArg], + single_table_name: &Option<&str>, + table_with_joins: &Option>, + db_conn: &DBConn, +) -> Option> { + if !args.len().is_multiple_of(2) { + // Invalid number of arguments + return None; + } + + let mut object_fields = vec![]; + + // Process key-value pairs + for i in (0..args.len()).step_by(2) { + let key_arg = &args[i]; + let value_arg = &args[i + 1]; + + // Extract key name + let key_name = extract_key_name(key_arg)?; + + // Extract value expression + let value_expr = extract_expr_from_arg(value_arg)?; + + // Infer value type + let (value_type, is_nullable) = + infer_type_from_expr(value_expr, single_table_name, table_with_joins, db_conn).await?; + + object_fields.push((key_name, value_type, is_nullable)); + } + + Some(object_fields) +} + +/// Handle JSON build functions (jsonb_build_object, json_build_object, etc.) +pub async fn handle_json_build_function( + function_name: &str, + args: &[FunctionArg], + ctx: &mut FunctionHandlersContext<'_>, +) -> Result<(), TsGeneratorError> { + let expr_log = ctx.expr_for_logging.unwrap_or(""); + + // Handle jsonb_build_object / json_build_object / json_object (MySQL) + let func_upper = function_name.to_uppercase(); + if func_upper == "JSONB_BUILD_OBJECT" || func_upper == "JSON_BUILD_OBJECT" || func_upper == "JSON_OBJECT" { + if let Some(object_fields) = + process_json_build_object_args(args, ctx.single_table_name, ctx.table_with_joins, ctx.db_conn).await + { + let object_type = TsFieldType::StructuredObject(object_fields); + return ctx + .ts_query + .insert_result(Some(ctx.alias), &[object_type], ctx.is_selection, false, expr_log); + } + } + + // For other build functions or on failure, return Any + ctx + .ts_query + .insert_result(Some(ctx.alias), &[TsFieldType::Any], ctx.is_selection, false, expr_log) +} + +/// Handle JSON aggregation functions (jsonb_agg, json_agg, etc.) +pub async fn handle_json_agg_function( + args: &[FunctionArg], + ctx: &mut FunctionHandlersContext<'_>, +) -> Result<(), TsGeneratorError> { + use super::super::functions::is_json_build_function; + use sqlparser::ast::FunctionArguments; + + let expr_log = ctx.expr_for_logging.unwrap_or(""); + + // jsonb_agg typically takes a single expression + if args.len() != 1 { + return ctx + .ts_query + .insert_result(Some(ctx.alias), &[TsFieldType::Any], ctx.is_selection, false, expr_log); + } + + let arg_expr = extract_expr_from_arg(&args[0]); + + // Check if the argument is a jsonb_build_object function + if let Some(Expr::Function(inner_func)) = arg_expr { + let inner_func_name = inner_func.name.to_string(); + if is_json_build_function(inner_func_name.as_str()) { + // Extract arguments from the inner function + let inner_args = match &inner_func.args { + FunctionArguments::List(arg_list) => &arg_list.args, + _ => { + return ctx + .ts_query + .insert_result(Some(ctx.alias), &[TsFieldType::Any], ctx.is_selection, false, expr_log); + } + }; + + // Process the inner jsonb_build_object + if let Some(object_fields) = + process_json_build_object_args(inner_args, ctx.single_table_name, ctx.table_with_joins, ctx.db_conn).await + { + let object_type = TsFieldType::StructuredObject(object_fields); + let array_type = TsFieldType::Array(Box::new(object_type)); + return ctx + .ts_query + .insert_result(Some(ctx.alias), &[array_type], ctx.is_selection, false, expr_log); + } + } + } + + // If we can't infer the type, return Any + ctx + .ts_query + .insert_result(Some(ctx.alias), &[TsFieldType::Any], ctx.is_selection, false, expr_log) +} diff --git a/src/ts_generator/sql_parser/expressions/function_handlers/mod.rs b/src/ts_generator/sql_parser/expressions/function_handlers/mod.rs new file mode 100644 index 00000000..2796df6d --- /dev/null +++ b/src/ts_generator/sql_parser/expressions/function_handlers/mod.rs @@ -0,0 +1,17 @@ +use crate::core::connection::DBConn; +use crate::ts_generator::types::ts_query::TsQuery; +use sqlparser::ast::TableWithJoins; + +pub mod json_functions; +pub mod polymorphic_functions; + +/// Context for function type inference +pub struct FunctionHandlersContext<'a> { + pub ts_query: &'a mut TsQuery, + pub single_table_name: &'a Option<&'a str>, + pub table_with_joins: &'a Option>, + pub db_conn: &'a DBConn, + pub alias: &'a str, + pub is_selection: bool, + pub expr_for_logging: Option<&'a str>, +} diff --git a/src/ts_generator/sql_parser/expressions/function_handlers/polymorphic_functions.rs b/src/ts_generator/sql_parser/expressions/function_handlers/polymorphic_functions.rs new file mode 100644 index 00000000..e32d04f4 --- /dev/null +++ b/src/ts_generator/sql_parser/expressions/function_handlers/polymorphic_functions.rs @@ -0,0 +1,97 @@ +use crate::common::lazy::DB_SCHEMA; +use crate::ts_generator::errors::TsGeneratorError; +use crate::ts_generator::sql_parser::expressions::function_handlers::FunctionHandlersContext; +use crate::ts_generator::sql_parser::expressions::translate_data_type::translate_value; +use crate::ts_generator::sql_parser::expressions::translate_table_with_joins::translate_table_from_expr; +use crate::ts_generator::sql_parser::quoted_strings::DisplayIndent; +use crate::ts_generator::types::ts_query::TsFieldType; +use sqlparser::ast::{Expr, Function, FunctionArg, FunctionArgExpr, FunctionArguments}; + +pub async fn handle_polymorphic_functions( + func_obj: &Function, + ctx: &mut FunctionHandlersContext<'_>, +) -> Result<(), TsGeneratorError> { + let expr_log = ctx.expr_for_logging.unwrap_or(""); + // In sqlparser 0.59.0, args is a FunctionArguments enum + // Extract the first argument from the appropriate variant + let first_arg = match &func_obj.args { + FunctionArguments::List(arg_list) => arg_list.args.first(), + FunctionArguments::None => None, + FunctionArguments::Subquery(_) => None, // Can't infer type from subquery easily + }; + + if let Some(first_arg) = first_arg { + let first_expr = match first_arg { + FunctionArg::Unnamed(FunctionArgExpr::Expr(expr)) => Some(expr), + FunctionArg::Named { + arg: FunctionArgExpr::Expr(expr), + .. + } => Some(expr), + _ => None, + }; + + if let Some(arg_expr) = first_expr { + // Try to infer type from the first argument + match arg_expr { + Expr::Identifier(ident) => { + let column_name = DisplayIndent(ident).to_string(); + if let Some(table_name) = ctx.single_table_name { + let table_details = &DB_SCHEMA + .lock() + .await + .fetch_table(&vec![table_name], ctx.db_conn) + .await; + + if let Some(table_details) = table_details { + if let Some(field) = table_details.get(&column_name) { + return ctx.ts_query.insert_result( + Some(ctx.alias), + &[field.field_type.to_owned()], + ctx.is_selection, + false, // IFNULL/COALESCE removes nullability + expr_log, + ); + } + } + } + } + Expr::CompoundIdentifier(idents) if idents.len() == 2 => { + let column_name = DisplayIndent(&idents[1]).to_string(); + if let Ok(table_name) = translate_table_from_expr(ctx.table_with_joins, arg_expr) { + let table_details = &DB_SCHEMA + .lock() + .await + .fetch_table(&vec![table_name.as_str()], ctx.db_conn) + .await; + + if let Some(table_details) = table_details { + if let Some(field) = table_details.get(&column_name) { + return ctx.ts_query.insert_result( + Some(ctx.alias), + &[field.field_type.to_owned()], + ctx.is_selection, + false, // IFNULL/COALESCE removes nullability + expr_log, + ); + } + } + } + } + Expr::Value(val) => { + // If first arg is a literal value, infer from that + if let Some(ts_field_type) = translate_value(&val.value) { + return ctx + .ts_query + .insert_result(Some(ctx.alias), &[ts_field_type], ctx.is_selection, false, expr_log); + } + } + _ => {} + } + } + } + + // Fallback to Any if we couldn't infer the type + ctx + .ts_query + .insert_result(Some(ctx.alias), &[TsFieldType::Any], ctx.is_selection, false, expr_log) +} diff --git a/src/ts_generator/sql_parser/expressions/functions.rs b/src/ts_generator/sql_parser/expressions/functions.rs index fb8d2bd4..8d086b90 100644 --- a/src/ts_generator/sql_parser/expressions/functions.rs +++ b/src/ts_generator/sql_parser/expressions/functions.rs @@ -170,3 +170,29 @@ pub static TYPE_POLYMORPHIC_FUNCTIONS: &[&str] = &[ pub fn is_type_polymorphic_function(func_name: &str) -> bool { TYPE_POLYMORPHIC_FUNCTIONS.contains(&func_name.to_uppercase().as_str()) } + +// JSON/JSONB functions that build objects/arrays +pub static JSON_BUILD_FUNCTIONS: &[&str] = &[ + "JSONB_BUILD_OBJECT", + "JSON_BUILD_OBJECT", + "JSONB_BUILD_ARRAY", + "JSON_BUILD_ARRAY", + "JSON_OBJECT", // MySQL JSON_OBJECT function +]; + +// JSON/JSONB aggregation functions +pub static JSON_AGG_FUNCTIONS: &[&str] = &[ + "JSONB_AGG", + "JSON_AGG", + "JSON_OBJECT_AGG", + "JSONB_OBJECT_AGG", + "JSON_ARRAYAGG", // MySQL JSON_ARRAYAGG function +]; + +pub fn is_json_build_function(func_name: &str) -> bool { + JSON_BUILD_FUNCTIONS.contains(&func_name.to_uppercase().as_str()) +} + +pub fn is_json_agg_function(func_name: &str) -> bool { + JSON_AGG_FUNCTIONS.contains(&func_name.to_uppercase().as_str()) +} diff --git a/src/ts_generator/sql_parser/expressions/mod.rs b/src/ts_generator/sql_parser/expressions/mod.rs index 81d8188f..be4f30b1 100644 --- a/src/ts_generator/sql_parser/expressions/mod.rs +++ b/src/ts_generator/sql_parser/expressions/mod.rs @@ -4,6 +4,8 @@ pub mod translate_expr; pub mod translate_table_with_joins; pub mod translate_wildcard_expr; +pub mod function_handlers; + #[cfg(test)] #[path = "./functions.test.rs"] mod functions_test; diff --git a/src/ts_generator/sql_parser/expressions/translate_expr.rs b/src/ts_generator/sql_parser/expressions/translate_expr.rs index d47ea135..1e076fb3 100644 --- a/src/ts_generator/sql_parser/expressions/translate_expr.rs +++ b/src/ts_generator/sql_parser/expressions/translate_expr.rs @@ -1,8 +1,13 @@ -use super::functions::{is_date_function, is_numeric_function, is_type_polymorphic_function}; +use super::function_handlers::json_functions::{handle_json_agg_function, handle_json_build_function}; +use super::function_handlers::polymorphic_functions::handle_polymorphic_functions; +use super::functions::{ + is_date_function, is_json_agg_function, is_json_build_function, is_numeric_function, is_type_polymorphic_function, +}; use crate::common::lazy::DB_SCHEMA; use crate::common::logger::{error, warning}; use crate::core::connection::DBConn; use crate::ts_generator::errors::TsGeneratorError; +use crate::ts_generator::sql_parser::expressions::function_handlers::FunctionHandlersContext; use crate::ts_generator::sql_parser::expressions::translate_data_type::translate_value; use crate::ts_generator::sql_parser::expressions::translate_table_with_joins::translate_table_from_expr; use crate::ts_generator::sql_parser::expressions::{ @@ -629,82 +634,67 @@ pub async fn translate_expr( // Handle type-polymorphic functions (IFNULL, COALESCE, etc.) // These functions return the type of their first argument if is_type_polymorphic_function(function_name_str) { - use sqlparser::ast::{FunctionArg, FunctionArgExpr, FunctionArguments}; - - // In sqlparser 0.59.0, args is a FunctionArguments enum - // Extract the first argument from the appropriate variant - let first_arg = match &func_obj.args { - FunctionArguments::List(arg_list) => arg_list.args.first(), - FunctionArguments::None => None, - FunctionArguments::Subquery(_) => None, // Can't infer type from subquery easily + let mut ctx = FunctionHandlersContext { + ts_query, + single_table_name, + table_with_joins, + db_conn, + alias, + is_selection, + expr_for_logging: Some(expr_for_logging), }; - if let Some(first_arg) = first_arg { - let first_expr = match first_arg { - FunctionArg::Unnamed(FunctionArgExpr::Expr(expr)) => Some(expr), - FunctionArg::Named { - arg: FunctionArgExpr::Expr(expr), - .. - } => Some(expr), - _ => None, - }; - - if let Some(arg_expr) = first_expr { - // Try to infer type from the first argument - match arg_expr { - Expr::Identifier(ident) => { - let column_name = DisplayIndent(ident).to_string(); - if let Some(table_name) = single_table_name { - let table_details = &DB_SCHEMA.lock().await.fetch_table(&vec![table_name], db_conn).await; - - if let Some(table_details) = table_details { - if let Some(field) = table_details.get(&column_name) { - return ts_query.insert_result( - Some(alias), - &[field.field_type.to_owned()], - is_selection, - false, // IFNULL/COALESCE removes nullability - expr_for_logging, - ); - } - } - } - } - Expr::CompoundIdentifier(idents) if idents.len() == 2 => { - let column_name = DisplayIndent(&idents[1]).to_string(); - if let Ok(table_name) = translate_table_from_expr(table_with_joins, arg_expr) { - let table_details = &DB_SCHEMA - .lock() - .await - .fetch_table(&vec![table_name.as_str()], db_conn) - .await; - - if let Some(table_details) = table_details { - if let Some(field) = table_details.get(&column_name) { - return ts_query.insert_result( - Some(alias), - &[field.field_type.to_owned()], - is_selection, - false, // IFNULL/COALESCE removes nullability - expr_for_logging, - ); - } - } - } - } - Expr::Value(val) => { - // If first arg is a literal value, infer from that - if let Some(ts_field_type) = translate_value(&val.value) { - return ts_query.insert_result(Some(alias), &[ts_field_type], is_selection, false, expr_for_logging); - } - } - _ => {} - } + return handle_polymorphic_functions(func_obj, &mut ctx).await; + } + + // Handle JSON build functions (jsonb_build_object, json_build_object, etc.) + if is_json_build_function(function_name_str) { + use sqlparser::ast::FunctionArguments; + + let args = match &func_obj.args { + FunctionArguments::List(arg_list) => &arg_list.args, + _ => { + // If no arguments or subquery, return Any + return ts_query.insert_result(Some(alias), &[TsFieldType::Any], is_selection, false, expr_for_logging); } - } + }; + + let mut ctx = FunctionHandlersContext { + ts_query, + single_table_name, + table_with_joins, + db_conn, + alias, + is_selection, + expr_for_logging: Some(expr_for_logging), + }; + + return handle_json_build_function(function_name_str, args, &mut ctx).await; + } + + // Handle JSON aggregation functions (jsonb_agg, json_agg, etc.) + if is_json_agg_function(function_name_str) { + use sqlparser::ast::FunctionArguments; + + let args = match &func_obj.args { + FunctionArguments::List(arg_list) => &arg_list.args, + _ => { + // If no arguments or subquery, return Any + return ts_query.insert_result(Some(alias), &[TsFieldType::Any], is_selection, false, expr_for_logging); + } + }; + + let mut ctx = FunctionHandlersContext { + ts_query, + single_table_name, + table_with_joins, + db_conn, + alias, + is_selection, + expr_for_logging: Some(expr_for_logging), + }; - // Fallback to Any if we couldn't infer the type - return ts_query.insert_result(Some(alias), &[TsFieldType::Any], is_selection, false, expr_for_logging); + return handle_json_agg_function(args, &mut ctx).await; } // Handle other function types diff --git a/src/ts_generator/types/ts_query.rs b/src/ts_generator/types/ts_query.rs index b31797be..a6e72c9e 100644 --- a/src/ts_generator/types/ts_query.rs +++ b/src/ts_generator/types/ts_query.rs @@ -10,12 +10,108 @@ use crate::ts_generator::errors::TsGeneratorError; type Array2DContent = Vec>; +/// Check if a string is a valid TypeScript identifier +fn is_valid_ts_identifier(name: &str) -> bool { + // TypeScript identifier regex: must start with letter/underscore/dollar, followed by letters/digits/underscore/dollar + let identifier_regex = Regex::new(r"^[a-zA-Z_$][a-zA-Z0-9_$]*$").unwrap(); + + if !identifier_regex.is_match(name) { + return false; + } + + // Check against TypeScript reserved keywords + let reserved_keywords = [ + "break", + "case", + "catch", + "class", + "const", + "continue", + "debugger", + "default", + "delete", + "do", + "else", + "enum", + "export", + "extends", + "false", + "finally", + "for", + "function", + "if", + "import", + "in", + "instanceof", + "new", + "null", + "return", + "super", + "switch", + "this", + "throw", + "true", + "try", + "typeof", + "var", + "void", + "while", + "with", + "as", + "implements", + "interface", + "let", + "package", + "private", + "protected", + "public", + "static", + "yield", + "any", + "boolean", + "constructor", + "declare", + "get", + "module", + "require", + "number", + "set", + "string", + "symbol", + "type", + "from", + "of", + "namespace", + "async", + "await", + "abstract", + "readonly", + "never", + "unknown", + "bigint", + ]; + + !reserved_keywords.contains(&name) +} + +/// Format a field name for TypeScript object literal, quoting if necessary +fn format_ts_field_name(name: &str) -> String { + if is_valid_ts_identifier(name) { + name.to_string() + } else { + // Quote the field name and escape quotes inside + format!("\"{}\"", name.replace('\"', "\\\"")) + } +} + #[derive(Debug, Clone, PartialEq)] pub enum TsFieldType { String, Number, Boolean, Object, + // Structured object with named fields: Vec<(field_name, field_type, is_nullable)> + StructuredObject(Vec<(String, TsFieldType, bool)>), Date, Null, Enum(Vec), @@ -34,6 +130,21 @@ impl fmt::Display for TsFieldType { TsFieldType::Number => write!(f, "number"), TsFieldType::String => write!(f, "string"), TsFieldType::Object => write!(f, "object"), + TsFieldType::StructuredObject(fields) => { + let field_strings: Vec = fields + .iter() + .map(|(field_name, field_type, is_nullable)| { + let type_str = if *is_nullable { + format!("{} | null", field_type) + } else { + field_type.to_string() + }; + let formatted_name = format_ts_field_name(field_name); + format!("{}: {}", formatted_name, type_str) + }) + .collect(); + write!(f, "{{ {} }}", field_strings.join("; ")) + } TsFieldType::Date => write!(f, "Date"), TsFieldType::Any => write!(f, "any"), TsFieldType::Null => write!(f, "null"), @@ -115,6 +226,7 @@ impl TsFieldType { "binary" | "bit" | "blob" | "char" | "text" | "varbinary" | "varchar" => Self::String, "tinyint" => Self::Boolean, "date" | "datetime" | "timestamp" => Self::Date, + "json" => Self::Object, "enum" => { if let Some(enum_values) = enum_values { return Self::Enum(enum_values); diff --git a/tests/demo/select/no-default-table.queries.ts b/tests/demo/select/no-default-table.queries.ts index 7ae64d07..f78500bb 100644 --- a/tests/demo/select/no-default-table.queries.ts +++ b/tests/demo/select/no-default-table.queries.ts @@ -70,10 +70,10 @@ export interface IAllStringsResult { extractSecond1: Date; extractYear1: Date; jsonArray1: any; - jsonBuildObject1: any; + jsonBuildObject1: { key: string }; jsonExtractPathText1: string; jsonbArray1: any; - jsonbBuildObject1: any; + jsonbBuildObject1: { key: string }; jsonbExtractPathText1: string; left1: string; length1: string; diff --git a/tests/demo/select/no-default-table.snapshot.ts b/tests/demo/select/no-default-table.snapshot.ts index dbc129c7..dc0b75dd 100644 --- a/tests/demo/select/no-default-table.snapshot.ts +++ b/tests/demo/select/no-default-table.snapshot.ts @@ -70,10 +70,10 @@ export interface IAllStringsResult { extractSecond1: Date; extractYear1: Date; jsonArray1: any; - jsonBuildObject1: any; + jsonBuildObject1: { key: string }; jsonExtractPathText1: string; jsonbArray1: any; - jsonbBuildObject1: any; + jsonbBuildObject1: { key: string }; jsonbExtractPathText1: string; left1: string; length1: string; diff --git a/tests/demo_happy_path.rs b/tests/demo_happy_path.rs index 33c27036..9ce9c394 100644 --- a/tests/demo_happy_path.rs +++ b/tests/demo_happy_path.rs @@ -2,17 +2,14 @@ mod demo_happy_path_tests { use assert_cmd::cargo::cargo_bin_cmd; use pretty_assertions::assert_eq; + use std::env; use std::env::current_dir; use std::fs; use std::io::Write; + use std::path::Path; use walkdir::WalkDir; - #[test] - fn all_demo_should_pass() -> Result<(), Box> { - // SETUP - let root_path = current_dir().unwrap(); - let demo_path = root_path.join("tests/demo"); - + fn run_demo_test(demo_path: &Path) -> Result<(), Box> { // EXECUTE - Generate types for .ts files let mut cmd = cargo_bin_cmd!("sqlx-ts"); cmd @@ -90,6 +87,33 @@ mod demo_happy_path_tests { Ok(()) } + #[test] + fn all_demo_should_pass() -> Result<(), Box> { + let root_path = current_dir().unwrap(); + let demo_path = root_path.join("tests/demo"); + run_demo_test(&demo_path) + } + + #[test] + fn all_demo_json_postgres() -> Result<(), Box> { + // PostgreSQL JSON tests - compatible with all PostgreSQL versions that support JSON + let root_path = current_dir().unwrap(); + let demo_path = root_path.join("tests/demo_json/postgres"); + run_demo_test(&demo_path) + } + + #[test] + fn all_demo_json_mysql() -> Result<(), Box> { + // MySQL 5.7+ and PostgreSQL JSON tests + if env::var("MYSQL_VERSION").ok() == Some("5.6".to_string()) { + return Ok(()); // Skip test for MySQL 5.6 which doesn't support JSON functions + } + + let root_path = current_dir().unwrap(); + let demo_path = root_path.join("tests/demo_json/mysql"); + run_demo_test(&demo_path) + } + #[test] fn test_js_files() -> Result<(), Box> { // SETUP diff --git a/tests/demo_json/mysql/json_access_operators.queries.ts b/tests/demo_json/mysql/json_access_operators.queries.ts new file mode 100644 index 00000000..0577bd18 --- /dev/null +++ b/tests/demo_json/mysql/json_access_operators.queries.ts @@ -0,0 +1,124 @@ +export type JsonFieldAccessParams = []; + +export interface IJsonFieldAccessResult { + activeJson: string; + ageJson: string; + id: number; + name: string; + usernameJson: string; +} + +export interface IJsonFieldAccessQuery { + params: JsonFieldAccessParams; + result: IJsonFieldAccessResult; +} + +export type JsonFieldAccessTextParams = []; + +export interface IJsonFieldAccessTextResult { + active: number; + age: number; + email: string; + id: number; + name: string; + username: string; +} + +export interface IJsonFieldAccessTextQuery { + params: JsonFieldAccessTextParams; + result: IJsonFieldAccessTextResult; +} + +export type JsonNestedAccessParams = []; + +export interface IJsonNestedAccessResult { + addressJson: string; + cityJson: string; + id: number; + name: string; +} + +export interface IJsonNestedAccessQuery { + params: JsonNestedAccessParams; + result: IJsonNestedAccessResult; +} + +export type JsonArrayAccessParams = []; + +export interface IJsonArrayAccessResult { + firstItemJson: string; + firstItemPrice: number; + id: number; + itemsJson: string; + name: string; + secondItemJson: string; +} + +export interface IJsonArrayAccessQuery { + params: JsonArrayAccessParams; + result: IJsonArrayAccessResult; +} + +export type JsonPathAccessParams = []; + +export interface IJsonPathAccessResult { + firstItemJson: string; + firstItemName: any; + firstItemRarity: any; + id: number; + level: number; + levelJson: string; + name: string; +} + +export interface IJsonPathAccessQuery { + params: JsonPathAccessParams; + result: IJsonPathAccessResult; +} + +export type JsonDeepPathAccessParams = []; + +export interface IJsonDeepPathAccessResult { + darkMode: number; + dbHost: any; + dbHostJson: string; + dbPort: number; + emailNotifications: number; + id: number; + name: string; +} + +export interface IJsonDeepPathAccessQuery { + params: JsonDeepPathAccessParams; + result: IJsonDeepPathAccessResult; +} + +export type JsonFilterByFieldParams = []; + +export interface IJsonFilterByFieldResult { + email: string; + id: number; + name: string; + username: string; +} + +export interface IJsonFilterByFieldQuery { + params: JsonFilterByFieldParams; + result: IJsonFilterByFieldResult; +} + +export type JsonNullHandlingParams = []; + +export interface IJsonNullHandlingResult { + firstComment: any; + firstReviewer: any; + id: number; + secondComment: any; + thirdComment: any; + thirdReviewer: any; +} + +export interface IJsonNullHandlingQuery { + params: JsonNullHandlingParams; + result: IJsonNullHandlingResult; +} diff --git a/tests/demo_json/mysql/json_access_operators.snapshot.ts b/tests/demo_json/mysql/json_access_operators.snapshot.ts new file mode 100644 index 00000000..0577bd18 --- /dev/null +++ b/tests/demo_json/mysql/json_access_operators.snapshot.ts @@ -0,0 +1,124 @@ +export type JsonFieldAccessParams = []; + +export interface IJsonFieldAccessResult { + activeJson: string; + ageJson: string; + id: number; + name: string; + usernameJson: string; +} + +export interface IJsonFieldAccessQuery { + params: JsonFieldAccessParams; + result: IJsonFieldAccessResult; +} + +export type JsonFieldAccessTextParams = []; + +export interface IJsonFieldAccessTextResult { + active: number; + age: number; + email: string; + id: number; + name: string; + username: string; +} + +export interface IJsonFieldAccessTextQuery { + params: JsonFieldAccessTextParams; + result: IJsonFieldAccessTextResult; +} + +export type JsonNestedAccessParams = []; + +export interface IJsonNestedAccessResult { + addressJson: string; + cityJson: string; + id: number; + name: string; +} + +export interface IJsonNestedAccessQuery { + params: JsonNestedAccessParams; + result: IJsonNestedAccessResult; +} + +export type JsonArrayAccessParams = []; + +export interface IJsonArrayAccessResult { + firstItemJson: string; + firstItemPrice: number; + id: number; + itemsJson: string; + name: string; + secondItemJson: string; +} + +export interface IJsonArrayAccessQuery { + params: JsonArrayAccessParams; + result: IJsonArrayAccessResult; +} + +export type JsonPathAccessParams = []; + +export interface IJsonPathAccessResult { + firstItemJson: string; + firstItemName: any; + firstItemRarity: any; + id: number; + level: number; + levelJson: string; + name: string; +} + +export interface IJsonPathAccessQuery { + params: JsonPathAccessParams; + result: IJsonPathAccessResult; +} + +export type JsonDeepPathAccessParams = []; + +export interface IJsonDeepPathAccessResult { + darkMode: number; + dbHost: any; + dbHostJson: string; + dbPort: number; + emailNotifications: number; + id: number; + name: string; +} + +export interface IJsonDeepPathAccessQuery { + params: JsonDeepPathAccessParams; + result: IJsonDeepPathAccessResult; +} + +export type JsonFilterByFieldParams = []; + +export interface IJsonFilterByFieldResult { + email: string; + id: number; + name: string; + username: string; +} + +export interface IJsonFilterByFieldQuery { + params: JsonFilterByFieldParams; + result: IJsonFilterByFieldResult; +} + +export type JsonNullHandlingParams = []; + +export interface IJsonNullHandlingResult { + firstComment: any; + firstReviewer: any; + id: number; + secondComment: any; + thirdComment: any; + thirdReviewer: any; +} + +export interface IJsonNullHandlingQuery { + params: JsonNullHandlingParams; + result: IJsonNullHandlingResult; +} diff --git a/tests/demo_json/mysql/json_access_operators.ts b/tests/demo_json/mysql/json_access_operators.ts new file mode 100644 index 00000000..1d8559d3 --- /dev/null +++ b/tests/demo_json/mysql/json_access_operators.ts @@ -0,0 +1,120 @@ +import { sql } from 'sqlx-ts' + + +// Test -> operator (get JSON field as JSON) +const jsonFieldAccess = sql` +-- @db: db_mysql +-- @name: json field access +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + data -> '$.username' AS username_json, + data -> '$.age' AS age_json, + data -> '$.active' AS active_json +FROM json_test_data +WHERE json_test_data.name = 'user_profile' +` + +// Test ->> operator (get JSON field as text) +const jsonFieldAccessText = sql` +-- @db: db_mysql +-- @name: json field access text +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + data ->> '$.username' AS username, + data ->> '$.email' AS email, + CAST(data ->> '$.age' AS UNSIGNED) AS age, + CAST(data ->> '$.active' AS UNSIGNED) AS active +FROM json_test_data +WHERE json_test_data.name = 'user_profile' +` + +// Test nested field access with JSON_EXTRACT +const jsonNestedAccess = sql` +-- @db: db_mysql +-- @name: json nested access +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + data -> '$.address' AS address_json, + data -> '$.address.city' AS city_json +FROM json_test_data +WHERE json_test_data.name = 'user_with_address' +` + +// Test array element access by index +const jsonArrayAccess = sql` +-- @db: db_mysql +-- @name: json array access +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + data -> '$.items' AS items_json, + data -> '$.items[0]' AS first_item_json, + data -> '$.items[1]' AS second_item_json, + CAST(data -> '$.items[0].price' AS DECIMAL(10,2)) AS first_item_price +FROM json_test_data +WHERE json_test_data.name = 'shopping_cart' +` + +// Test path access with deep nesting +const jsonPathAccess = sql` +-- @db: db_mysql +-- @name: json path access +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + data -> '$.stats.level' AS level_json, + data -> '$.stats.inventory[0].item' AS first_item_json, + CAST(JSON_UNQUOTE(data -> '$.stats.level') AS UNSIGNED) AS level, + JSON_UNQUOTE(data -> '$.stats.inventory[0].item') AS first_item_name, + JSON_UNQUOTE(data -> '$.stats.inventory[0].rarity') AS first_item_rarity +FROM json_test_data +WHERE json_test_data.name = 'game_stats' +` + +// Test deep nested path access +const jsonDeepPathAccess = sql` +-- @db: db_mysql +-- @name: json deep path access +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + data -> '$.app.settings.database.host' AS db_host_json, + JSON_UNQUOTE(data -> '$.app.settings.database.host') AS db_host, + CAST(JSON_UNQUOTE(data -> '$.app.settings.database.port') AS UNSIGNED) AS db_port, + CAST(JSON_UNQUOTE(data -> '$.app.settings.features.darkMode') AS UNSIGNED) AS dark_mode, + CAST(JSON_UNQUOTE(data -> '$.app.settings.features.notifications.email') AS UNSIGNED) AS email_notifications +FROM json_test_data +WHERE json_test_data.name = 'nested_config' +` + +// Test filtering by JSON field +const jsonFilterByField = sql` +-- @db: db_mysql +-- @name: json filter by field +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + data ->> '$.username' AS username, + data ->> '$.email' AS email +FROM json_test_data +WHERE data ->> '$.active' = 'true' + AND CAST(data ->> '$.age' AS UNSIGNED) > 25 +` + +// Test null handling in JSON +const jsonNullHandling = sql` +-- @db: db_mysql +-- @name: json null handling +SELECT + json_test_data.id AS id, + JSON_UNQUOTE(data -> '$.reviews[0].comment') AS first_comment, + JSON_UNQUOTE(data -> '$.reviews[1].comment') AS second_comment, + JSON_UNQUOTE(data -> '$.reviews[2].comment') AS third_comment, + JSON_UNQUOTE(data -> '$.reviews[0].reviewer') AS first_reviewer, + JSON_UNQUOTE(data -> '$.reviews[2].reviewer') AS third_reviewer +FROM json_test_data +WHERE json_test_data.name = 'product_reviews' +` diff --git a/tests/demo_json/mysql/json_array_functions.queries.ts b/tests/demo_json/mysql/json_array_functions.queries.ts new file mode 100644 index 00000000..6b057d4f --- /dev/null +++ b/tests/demo_json/mysql/json_array_functions.queries.ts @@ -0,0 +1,130 @@ +export type JsonArrayLengthParams = []; + +export interface IJsonArrayLengthResult { + id: number; + itemsCount: any; + name: string; + tagsCount: any; +} + +export interface IJsonArrayLengthQuery { + params: JsonArrayLengthParams; + result: IJsonArrayLengthResult; +} + +export type JsonArrayExtractParams = []; + +export interface IJsonArrayExtractResult { + firstTag: any; + id: number; + name: string; + secondTag: any; + thirdTag: any; +} + +export interface IJsonArrayExtractQuery { + params: JsonArrayExtractParams; + result: IJsonArrayExtractResult; +} + +export type JsonArrayContainsParams = []; + +export interface IJsonArrayContainsResult { + hasDatabase: any; + hasMysql: any; + id: number; + name: string; + tags: any; +} + +export interface IJsonArrayContainsQuery { + params: JsonArrayContainsParams; + result: IJsonArrayContainsResult; +} + +export type JsonArrayMembershipParams = []; + +export interface IJsonArrayMembershipResult { + hasMysqlTag: any; + hasTutorialTag: any; + id: number; + name: string; +} + +export interface IJsonArrayMembershipQuery { + params: JsonArrayMembershipParams; + result: IJsonArrayMembershipResult; +} + +export type JsonNestedArrayAccessParams = []; + +export interface IJsonNestedArrayAccessResult { + firstItemName: any; + firstItemPrice: any; + id: number; + name: string; + secondItemName: any; + secondItemQuantity: any; +} + +export interface IJsonNestedArrayAccessQuery { + params: JsonNestedArrayAccessParams; + result: IJsonNestedArrayAccessResult; +} + +export type JsonDeepNestedArrayParams = []; + +export interface IJsonDeepNestedArrayResult { + firstAchievement: any; + firstInventoryItem: any; + firstItemRarity: any; + id: number; + name: string; + secondInventoryItem: any; +} + +export interface IJsonDeepNestedArrayQuery { + params: JsonDeepNestedArrayParams; + result: IJsonDeepNestedArrayResult; +} + +export type JsonArrayBuildParams = []; + +export interface IJsonArrayBuildResult { + firstTwoTags: any; + id: number; + name: string; +} + +export interface IJsonArrayBuildQuery { + params: JsonArrayBuildParams; + result: IJsonArrayBuildResult; +} + +export type JsonArrayAppendParams = []; + +export interface IJsonArrayAppendResult { + id: number; + name: string; + originalTags: any; + tagsWithNew: any; +} + +export interface IJsonArrayAppendQuery { + params: JsonArrayAppendParams; + result: IJsonArrayAppendResult; +} + +export type JsonArrayInsertParams = []; + +export interface IJsonArrayInsertResult { + id: number; + name: string; + originalTags: any; + tagsWithInsert: any; +} + +export interface IJsonArrayInsertQuery { + params: JsonArrayInsertParams; + result: IJsonArrayInsertResult; +} diff --git a/tests/demo_json/mysql/json_array_functions.snapshot.ts b/tests/demo_json/mysql/json_array_functions.snapshot.ts new file mode 100644 index 00000000..6b057d4f --- /dev/null +++ b/tests/demo_json/mysql/json_array_functions.snapshot.ts @@ -0,0 +1,130 @@ +export type JsonArrayLengthParams = []; + +export interface IJsonArrayLengthResult { + id: number; + itemsCount: any; + name: string; + tagsCount: any; +} + +export interface IJsonArrayLengthQuery { + params: JsonArrayLengthParams; + result: IJsonArrayLengthResult; +} + +export type JsonArrayExtractParams = []; + +export interface IJsonArrayExtractResult { + firstTag: any; + id: number; + name: string; + secondTag: any; + thirdTag: any; +} + +export interface IJsonArrayExtractQuery { + params: JsonArrayExtractParams; + result: IJsonArrayExtractResult; +} + +export type JsonArrayContainsParams = []; + +export interface IJsonArrayContainsResult { + hasDatabase: any; + hasMysql: any; + id: number; + name: string; + tags: any; +} + +export interface IJsonArrayContainsQuery { + params: JsonArrayContainsParams; + result: IJsonArrayContainsResult; +} + +export type JsonArrayMembershipParams = []; + +export interface IJsonArrayMembershipResult { + hasMysqlTag: any; + hasTutorialTag: any; + id: number; + name: string; +} + +export interface IJsonArrayMembershipQuery { + params: JsonArrayMembershipParams; + result: IJsonArrayMembershipResult; +} + +export type JsonNestedArrayAccessParams = []; + +export interface IJsonNestedArrayAccessResult { + firstItemName: any; + firstItemPrice: any; + id: number; + name: string; + secondItemName: any; + secondItemQuantity: any; +} + +export interface IJsonNestedArrayAccessQuery { + params: JsonNestedArrayAccessParams; + result: IJsonNestedArrayAccessResult; +} + +export type JsonDeepNestedArrayParams = []; + +export interface IJsonDeepNestedArrayResult { + firstAchievement: any; + firstInventoryItem: any; + firstItemRarity: any; + id: number; + name: string; + secondInventoryItem: any; +} + +export interface IJsonDeepNestedArrayQuery { + params: JsonDeepNestedArrayParams; + result: IJsonDeepNestedArrayResult; +} + +export type JsonArrayBuildParams = []; + +export interface IJsonArrayBuildResult { + firstTwoTags: any; + id: number; + name: string; +} + +export interface IJsonArrayBuildQuery { + params: JsonArrayBuildParams; + result: IJsonArrayBuildResult; +} + +export type JsonArrayAppendParams = []; + +export interface IJsonArrayAppendResult { + id: number; + name: string; + originalTags: any; + tagsWithNew: any; +} + +export interface IJsonArrayAppendQuery { + params: JsonArrayAppendParams; + result: IJsonArrayAppendResult; +} + +export type JsonArrayInsertParams = []; + +export interface IJsonArrayInsertResult { + id: number; + name: string; + originalTags: any; + tagsWithInsert: any; +} + +export interface IJsonArrayInsertQuery { + params: JsonArrayInsertParams; + result: IJsonArrayInsertResult; +} diff --git a/tests/demo_json/mysql/json_array_functions.ts b/tests/demo_json/mysql/json_array_functions.ts new file mode 100644 index 00000000..bc5ddeb2 --- /dev/null +++ b/tests/demo_json/mysql/json_array_functions.ts @@ -0,0 +1,127 @@ +import { sql } from 'sqlx-ts' + + +// Test JSON_LENGTH - get array length +const jsonArrayLength = sql` +-- @db: db_mysql +-- @name: json array length +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + JSON_LENGTH(data, '$.items') AS items_count, + JSON_LENGTH(data, '$.tags') AS tags_count +FROM json_test_data +WHERE json_test_data.name IN ('shopping_cart', 'tags') +` + +// Test JSON_EXTRACT with array index +const jsonArrayExtract = sql` +-- @db: db_mysql +-- @name: json array extract +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + JSON_EXTRACT(data, '$.tags[0]') AS first_tag, + JSON_EXTRACT(data, '$.tags[1]') AS second_tag, + JSON_EXTRACT(data, '$.tags[2]') AS third_tag +FROM json_test_data +WHERE json_test_data.name = 'tags' +` + +// Test array contains using JSON_CONTAINS +const jsonArrayContains = sql` +-- @db: db_mysql +-- @name: json array contains +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + JSON_EXTRACT(data, '$.tags') AS tags, + JSON_CONTAINS(JSON_EXTRACT(data, '$.tags'), JSON_QUOTE('mysql')) AS has_mysql, + JSON_CONTAINS(JSON_EXTRACT(data, '$.tags'), JSON_QUOTE('database')) AS has_database +FROM json_test_data +WHERE json_test_data.name = 'tags' +` + +// Test array element membership +const jsonArrayMembership = sql` +-- @db: db_mysql +-- @name: json array membership +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + JSON_CONTAINS(JSON_EXTRACT(data, '$.tags'), JSON_QUOTE('mysql')) AS has_mysql_tag, + JSON_CONTAINS(JSON_EXTRACT(data, '$.tags'), JSON_QUOTE('tutorial')) AS has_tutorial_tag +FROM json_test_data +WHERE json_test_data.name = 'tags' +` + +// Test nested array access +const jsonNestedArrayAccess = sql` +-- @db: db_mysql +-- @name: json nested array access +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + JSON_EXTRACT(data, '$.items[0].name') AS first_item_name, + JSON_EXTRACT(data, '$.items[0].price') AS first_item_price, + JSON_EXTRACT(data, '$.items[1].name') AS second_item_name, + JSON_EXTRACT(data, '$.items[1].quantity') AS second_item_quantity +FROM json_test_data +WHERE json_test_data.name = 'shopping_cart' +` + +// Test deep nested array +const jsonDeepNestedArray = sql` +-- @db: db_mysql +-- @name: json deep nested array +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + JSON_EXTRACT(data, '$.stats.inventory[0].item') AS first_inventory_item, + JSON_EXTRACT(data, '$.stats.inventory[0].rarity') AS first_item_rarity, + JSON_EXTRACT(data, '$.stats.inventory[1].item') AS second_inventory_item, + JSON_EXTRACT(data, '$.stats.achievements[0]') AS first_achievement +FROM json_test_data +WHERE json_test_data.name = 'game_stats' +` + +// Test JSON_ARRAY to build arrays +const jsonArrayBuild = sql` +-- @db: db_mysql +-- @name: json array build +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + JSON_ARRAY( + JSON_EXTRACT(data, '$.tags[0]'), + JSON_EXTRACT(data, '$.tags[1]') + ) AS first_two_tags +FROM json_test_data +WHERE json_test_data.name = 'tags' +` + +// Test JSON_ARRAY_APPEND to add elements +const jsonArrayAppend = sql` +-- @db: db_mysql +-- @name: json array append +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + JSON_EXTRACT(data, '$.tags') AS original_tags, + JSON_ARRAY_APPEND(JSON_EXTRACT(data, '$.tags'), '$', 'new_tag') AS tags_with_new +FROM json_test_data +WHERE json_test_data.name = 'tags' +` + +// Test JSON_ARRAY_INSERT to insert elements +const jsonArrayInsert = sql` +-- @db: db_mysql +-- @name: json array insert +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + JSON_EXTRACT(data, '$.tags') AS original_tags, + JSON_ARRAY_INSERT(JSON_EXTRACT(data, '$.tags'), '$[1]', 'inserted_tag') AS tags_with_insert +FROM json_test_data +WHERE json_test_data.name = 'tags' +` diff --git a/tests/demo_json/mysql/json_comprehensive.queries.ts b/tests/demo_json/mysql/json_comprehensive.queries.ts new file mode 100644 index 00000000..870eb844 --- /dev/null +++ b/tests/demo_json/mysql/json_comprehensive.queries.ts @@ -0,0 +1,175 @@ +export type JsonExtractParams = []; + +export interface IJsonExtractResult { + age: number; + email: any; + id: number; + name: string; + username: any; +} + +export interface IJsonExtractQuery { + params: JsonExtractParams; + result: IJsonExtractResult; +} + +export type JsonExtractShorthandParams = []; + +export interface IJsonExtractShorthandResult { + email: string; + id: number; + name: string; + username: string; +} + +export interface IJsonExtractShorthandQuery { + params: JsonExtractShorthandParams; + result: IJsonExtractShorthandResult; +} + +export type JsonNestedPathParams = []; + +export interface IJsonNestedPathResult { + city: any; + id: number; + name: string; + zipCode: any; +} + +export interface IJsonNestedPathQuery { + params: JsonNestedPathParams; + result: IJsonNestedPathResult; +} + +export type JsonArrayIndexParams = []; + +export interface IJsonArrayIndexResult { + firstItemName: any; + firstItemPrice: number; + id: number; + name: string; +} + +export interface IJsonArrayIndexQuery { + params: JsonArrayIndexParams; + result: IJsonArrayIndexResult; +} + +export type JsonArrayLengthParams = []; + +export interface IJsonArrayLengthResult { + id: number; + name: string; + tagsCount: any; +} + +export interface IJsonArrayLengthQuery { + params: JsonArrayLengthParams; + result: IJsonArrayLengthResult; +} + +export type JsonTypeParams = []; + +export interface IJsonTypeResult { + ageType: any; + id: number; + tagsType: any; + usernameType: any; +} + +export interface IJsonTypeQuery { + params: JsonTypeParams; + result: IJsonTypeResult; +} + +export type JsonContainsParams = []; + +export interface IJsonContainsResult { + id: number; + isActive: any; + name: string; +} + +export interface IJsonContainsQuery { + params: JsonContainsParams; + result: IJsonContainsResult; +} + +export type JsonKeysParams = []; + +export interface IJsonKeysResult { + allKeys: any; + id: number; + name: string; +} + +export interface IJsonKeysQuery { + params: JsonKeysParams; + result: IJsonKeysResult; +} + +export type JsonObjectBuildParams = []; + +export interface IJsonObjectBuildResult { + id: number; + name: string; + userSummary: { id: number; name: string; username: any; email: any }; +} + +export interface IJsonObjectBuildQuery { + params: JsonObjectBuildParams; + result: IJsonObjectBuildResult; +} + +export type JsonFilterParams = []; + +export interface IJsonFilterResult { + id: number; + name: string; + username: any; +} + +export interface IJsonFilterQuery { + params: JsonFilterParams; + result: IJsonFilterResult; +} + +export type JsonDeepPathParams = []; + +export interface IJsonDeepPathResult { + appName: any; + dbHost: any; + dbPort: number; + id: number; +} + +export interface IJsonDeepPathQuery { + params: JsonDeepPathParams; + result: IJsonDeepPathResult; +} + +export type JsonValidParams = []; + +export interface IJsonValidResult { + id: number; + isValidJson: any; + name: string; +} + +export interface IJsonValidQuery { + params: JsonValidParams; + result: IJsonValidResult; +} + +export type JsonSearchParams = []; + +export interface IJsonSearchResult { + id: number; + name: string; + usernamePath: any; +} + +export interface IJsonSearchQuery { + params: JsonSearchParams; + result: IJsonSearchResult; +} diff --git a/tests/demo_json/mysql/json_comprehensive.snapshot.ts b/tests/demo_json/mysql/json_comprehensive.snapshot.ts new file mode 100644 index 00000000..f3e50ea0 --- /dev/null +++ b/tests/demo_json/mysql/json_comprehensive.snapshot.ts @@ -0,0 +1,176 @@ +export type JsonExtractParams = []; + +export interface IJsonExtractResult { + age: number; + email: any; + id: number; + name: string; + username: any; +} + +export interface IJsonExtractQuery { + params: JsonExtractParams; + result: IJsonExtractResult; +} + +export type JsonExtractShorthandParams = []; + +export interface IJsonExtractShorthandResult { + email: string; + id: number; + name: string; + username: string; +} + +export interface IJsonExtractShorthandQuery { + params: JsonExtractShorthandParams; + result: IJsonExtractShorthandResult; +} + +export type JsonNestedPathParams = []; + +export interface IJsonNestedPathResult { + city: any; + id: number; + name: string; + zipCode: any; +} + +export interface IJsonNestedPathQuery { + params: JsonNestedPathParams; + result: IJsonNestedPathResult; +} + +export type JsonArrayIndexParams = []; + +export interface IJsonArrayIndexResult { + firstItemName: any; + firstItemPrice: number; + id: number; + name: string; +} + +export interface IJsonArrayIndexQuery { + params: JsonArrayIndexParams; + result: IJsonArrayIndexResult; +} + +export type JsonArrayLengthParams = []; + +export interface IJsonArrayLengthResult { + id: number; + name: string; + tagsCount: any; +} + +export interface IJsonArrayLengthQuery { + params: JsonArrayLengthParams; + result: IJsonArrayLengthResult; +} + +export type JsonTypeParams = []; + +export interface IJsonTypeResult { + ageType: any; + id: number; + tagsType: any; + usernameType: any; +} + +export interface IJsonTypeQuery { + params: JsonTypeParams; + result: IJsonTypeResult; +} + +export type JsonContainsParams = []; + +export interface IJsonContainsResult { + id: number; + isActive: any; + name: string; +} + +export interface IJsonContainsQuery { + params: JsonContainsParams; + result: IJsonContainsResult; +} + +export type JsonKeysParams = []; + +export interface IJsonKeysResult { + allKeys: any; + id: number; + name: string; +} + +export interface IJsonKeysQuery { + params: JsonKeysParams; + result: IJsonKeysResult; +} + +export type JsonObjectBuildParams = []; + +export interface IJsonObjectBuildResult { + id: number; + name: string; + userSummary: { id: number; name: string; username: any; email: any }; +} + +export interface IJsonObjectBuildQuery { + params: JsonObjectBuildParams; + result: IJsonObjectBuildResult; +} + +export type JsonFilterParams = []; + +export interface IJsonFilterResult { + id: number; + name: string; + username: any; +} + +export interface IJsonFilterQuery { + params: JsonFilterParams; + result: IJsonFilterResult; +} + +export type JsonDeepPathParams = []; + +export interface IJsonDeepPathResult { + appName: any; + dbHost: any; + dbPort: number; + id: number; +} + +export interface IJsonDeepPathQuery { + params: JsonDeepPathParams; + result: IJsonDeepPathResult; +} + +export type JsonValidParams = []; + +export interface IJsonValidResult { + id: number; + isValidJson: any; + name: string; +} + +export interface IJsonValidQuery { + params: JsonValidParams; + result: IJsonValidResult; +} + +export type JsonSearchParams = []; + +export interface IJsonSearchResult { + id: number; + name: string; + usernamePath: any; +} + +export interface IJsonSearchQuery { + params: JsonSearchParams; + result: IJsonSearchResult; +} + diff --git a/tests/demo_json/mysql/json_comprehensive.ts b/tests/demo_json/mysql/json_comprehensive.ts new file mode 100644 index 00000000..86069e37 --- /dev/null +++ b/tests/demo_json/mysql/json_comprehensive.ts @@ -0,0 +1,171 @@ +import { sql } from 'sqlx-ts' + + +// Test 1: JSON_EXTRACT with -> operator +const jsonExtract = sql` +-- @db: db_mysql +-- @name: json extract +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + JSON_UNQUOTE(JSON_EXTRACT(data, '$.username')) AS username, + JSON_UNQUOTE(JSON_EXTRACT(data, '$.email')) AS email, + CAST(JSON_EXTRACT(data, '$.age') AS UNSIGNED) AS age +FROM json_test_data +WHERE json_test_data.name = 'user_profile' +` + +// Test 2: JSON_EXTRACT with ->> operator (shorthand) +const jsonExtractShorthand = sql` +-- @db: db_mysql +-- @name: json extract shorthand +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + data->>'$.username' AS username, + data->>'$.email' AS email +FROM json_test_data +WHERE json_test_data.name = 'user_profile' +` + +// Test 3: Nested JSON path +const jsonNestedPath = sql` +-- @db: db_mysql +-- @name: json nested path +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + JSON_UNQUOTE(JSON_EXTRACT(data, '$.address.city')) AS city, + JSON_UNQUOTE(JSON_EXTRACT(data, '$.address.zipCode')) AS zip_code +FROM json_test_data +WHERE json_test_data.name = 'user_with_address' +` + +// Test 4: JSON array index access +const jsonArrayIndex = sql` +-- @db: db_mysql +-- @name: json array index +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + JSON_UNQUOTE(JSON_EXTRACT(data, '$.items[0].name')) AS first_item_name, + CAST(JSON_EXTRACT(data, '$.items[0].price') AS DECIMAL(10,2)) AS first_item_price +FROM json_test_data +WHERE json_test_data.name = 'shopping_cart' +` + +// Test 5: JSON_LENGTH for array length +const jsonArrayLength = sql` +-- @db: db_mysql +-- @name: json array length +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + JSON_LENGTH(JSON_EXTRACT(data, '$.tags')) AS tags_count +FROM json_test_data +WHERE json_test_data.name = 'tags' +` + +// Test 6: JSON_TYPE +const jsonType = sql` +-- @db: db_mysql +-- @name: json type +SELECT + json_test_data.id AS id, + JSON_TYPE(JSON_EXTRACT(data, '$.username')) AS username_type, + JSON_TYPE(JSON_EXTRACT(data, '$.age')) AS age_type, + JSON_TYPE(JSON_EXTRACT(data, '$.tags')) AS tags_type +FROM json_test_data +WHERE json_test_data.name IN ('user_profile', 'tags') +` + +// Test 7: JSON_CONTAINS for containment check +const jsonContains = sql` +-- @db: db_mysql +-- @name: json contains +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + JSON_CONTAINS(data, 'true', '$.active') AS is_active +FROM json_test_data +WHERE json_test_data.name = 'user_profile' +` + +// Test 8: JSON_KEYS to get object keys +const jsonKeys = sql` +-- @db: db_mysql +-- @name: json keys +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + JSON_KEYS(data) AS all_keys +FROM json_test_data +WHERE json_test_data.name = 'user_profile' +` + +// Test 9: JSON_OBJECT to build objects with type inference +const jsonObjectBuild = sql` +-- @db: db_mysql +-- @name: json object build +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + JSON_OBJECT( + 'id', json_test_data.id, + 'name', json_test_data.name, + 'username', JSON_EXTRACT(data, '$.username'), + 'email', JSON_EXTRACT(data, '$.email') + ) AS user_summary +FROM json_test_data +WHERE json_test_data.name = 'user_profile' +` + +// Test 10: Filter using JSON values +const jsonFilter = sql` +-- @db: db_mysql +-- @name: json filter +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + JSON_UNQUOTE(JSON_EXTRACT(data, '$.username')) AS username +FROM json_test_data +WHERE CAST(JSON_EXTRACT(data, '$.active') AS UNSIGNED) = 1 + AND CAST(JSON_EXTRACT(data, '$.age') AS UNSIGNED) > 25 +` + +// Test 11: Deep nested path +const jsonDeepPath = sql` +-- @db: db_mysql +-- @name: json deep path +SELECT + json_test_data.id AS id, + JSON_UNQUOTE(JSON_EXTRACT(data, '$.app.name')) AS app_name, + JSON_UNQUOTE(JSON_EXTRACT(data, '$.app.settings.database.host')) AS db_host, + CAST(JSON_EXTRACT(data, '$.app.settings.database.port') AS UNSIGNED) AS db_port +FROM json_test_data +WHERE json_test_data.name = 'nested_config' +` + +// Test 12: JSON_VALID +const jsonValid = sql` +-- @db: db_mysql +-- @name: json valid +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + JSON_VALID(data) AS is_valid_json +FROM json_test_data +LIMIT 3 +` + +// Test 13: JSON_SEARCH to find values +const jsonSearch = sql` +-- @db: db_mysql +-- @name: json search +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + JSON_SEARCH(data, 'one', 'john_doe') AS username_path +FROM json_test_data +WHERE json_test_data.name = 'user_profile' +` diff --git a/tests/demo_json/mysql/json_object_functions.queries.ts b/tests/demo_json/mysql/json_object_functions.queries.ts new file mode 100644 index 00000000..8d6588e4 --- /dev/null +++ b/tests/demo_json/mysql/json_object_functions.queries.ts @@ -0,0 +1,210 @@ +export type JsonObjectKeysParams = []; + +export interface IJsonObjectKeysResult { + id: number; + name: string; + objectKeys: any; +} + +export interface IJsonObjectKeysQuery { + params: JsonObjectKeysParams; + result: IJsonObjectKeysResult; +} + +export type JsonObjectKeysPathParams = []; + +export interface IJsonObjectKeysPathResult { + addressKeys: any; + id: number; + name: string; +} + +export interface IJsonObjectKeysPathQuery { + params: JsonObjectKeysPathParams; + result: IJsonObjectKeysPathResult; +} + +export type JsonTypeofParams = []; + +export interface IJsonTypeofResult { + activeType: any; + ageType: any; + id: number; + itemsType: any; + name: string; + tagsType: any; + usernameType: any; +} + +export interface IJsonTypeofQuery { + params: JsonTypeofParams; + result: IJsonTypeofResult; +} + +export type JsonContainsParams = []; + +export interface IJsonContainsResult { + hasSpecificUsername: any; + id: number; + isActive: any; + name: string; +} + +export interface IJsonContainsQuery { + params: JsonContainsParams; + result: IJsonContainsResult; +} + +export type JsonContainsPathParams = []; + +export interface IJsonContainsPathResult { + hasAddress: any; + hasBoth: any; + hasNonexistent: any; + hasUsername: any; + id: number; + name: string; +} + +export interface IJsonContainsPathQuery { + params: JsonContainsPathParams; + result: IJsonContainsPathResult; +} + +export type JsonObjectBuildParams = []; + +export interface IJsonObjectBuildResult { + id: number; + name: string; + userSummary: { id: number; name: string; username: any; email: any }; +} + +export interface IJsonObjectBuildQuery { + params: JsonObjectBuildParams; + result: IJsonObjectBuildResult; +} + +export type JsonSetParams = []; + +export interface IJsonSetResult { + id: number; + name: string; + originalData: object; + updatedAge: any; + updatedCity: any; +} + +export interface IJsonSetQuery { + params: JsonSetParams; + result: IJsonSetResult; +} + +export type JsonInsertParams = []; + +export interface IJsonInsertResult { + id: number; + name: string; + originalData: object; + withPhone: any; +} + +export interface IJsonInsertQuery { + params: JsonInsertParams; + result: IJsonInsertResult; +} + +export type JsonReplaceParams = []; + +export interface IJsonReplaceResult { + id: number; + name: string; + originalData: object; + withNewUsername: any; +} + +export interface IJsonReplaceQuery { + params: JsonReplaceParams; + result: IJsonReplaceResult; +} + +export type JsonRemoveParams = []; + +export interface IJsonRemoveResult { + id: number; + name: string; + originalData: object; + withoutAge: any; +} + +export interface IJsonRemoveQuery { + params: JsonRemoveParams; + result: IJsonRemoveResult; +} + +export type JsonMergePatchParams = []; + +export interface IJsonMergePatchResult { + id: number; + mergedData: any; + name: string; + originalData: object; +} + +export interface IJsonMergePatchQuery { + params: JsonMergePatchParams; + result: IJsonMergePatchResult; +} + +export type JsonMergePreserveParams = []; + +export interface IJsonMergePreserveResult { + id: number; + mergedData: any; + name: string; + originalData: object; +} + +export interface IJsonMergePreserveQuery { + params: JsonMergePreserveParams; + result: IJsonMergePreserveResult; +} + +export type JsonSearchParams = []; + +export interface IJsonSearchResult { + emailPath: any; + id: number; + name: string; + usernamePath: any; +} + +export interface IJsonSearchQuery { + params: JsonSearchParams; + result: IJsonSearchResult; +} + +export type JsonDepthParams = []; + +export interface IJsonDepthResult { + dataDepth: any; + id: number; + name: string; +} + +export interface IJsonDepthQuery { + params: JsonDepthParams; + result: IJsonDepthResult; +} + +export type JsonValidParams = []; + +export interface IJsonValidResult { + id: number; + isValidJson: any; + name: string; +} + +export interface IJsonValidQuery { + params: JsonValidParams; + result: IJsonValidResult; +} diff --git a/tests/demo_json/mysql/json_object_functions.snapshot.ts b/tests/demo_json/mysql/json_object_functions.snapshot.ts new file mode 100644 index 00000000..9537f5bb --- /dev/null +++ b/tests/demo_json/mysql/json_object_functions.snapshot.ts @@ -0,0 +1,211 @@ +export type JsonObjectKeysParams = []; + +export interface IJsonObjectKeysResult { + id: number; + name: string; + objectKeys: any; +} + +export interface IJsonObjectKeysQuery { + params: JsonObjectKeysParams; + result: IJsonObjectKeysResult; +} + +export type JsonObjectKeysPathParams = []; + +export interface IJsonObjectKeysPathResult { + addressKeys: any; + id: number; + name: string; +} + +export interface IJsonObjectKeysPathQuery { + params: JsonObjectKeysPathParams; + result: IJsonObjectKeysPathResult; +} + +export type JsonTypeofParams = []; + +export interface IJsonTypeofResult { + activeType: any; + ageType: any; + id: number; + itemsType: any; + name: string; + tagsType: any; + usernameType: any; +} + +export interface IJsonTypeofQuery { + params: JsonTypeofParams; + result: IJsonTypeofResult; +} + +export type JsonContainsParams = []; + +export interface IJsonContainsResult { + hasSpecificUsername: any; + id: number; + isActive: any; + name: string; +} + +export interface IJsonContainsQuery { + params: JsonContainsParams; + result: IJsonContainsResult; +} + +export type JsonContainsPathParams = []; + +export interface IJsonContainsPathResult { + hasAddress: any; + hasBoth: any; + hasNonexistent: any; + hasUsername: any; + id: number; + name: string; +} + +export interface IJsonContainsPathQuery { + params: JsonContainsPathParams; + result: IJsonContainsPathResult; +} + +export type JsonObjectBuildParams = []; + +export interface IJsonObjectBuildResult { + id: number; + name: string; + userSummary: { id: number; name: string; username: any; email: any }; +} + +export interface IJsonObjectBuildQuery { + params: JsonObjectBuildParams; + result: IJsonObjectBuildResult; +} + +export type JsonSetParams = []; + +export interface IJsonSetResult { + id: number; + name: string; + originalData: object; + updatedAge: any; + updatedCity: any; +} + +export interface IJsonSetQuery { + params: JsonSetParams; + result: IJsonSetResult; +} + +export type JsonInsertParams = []; + +export interface IJsonInsertResult { + id: number; + name: string; + originalData: object; + withPhone: any; +} + +export interface IJsonInsertQuery { + params: JsonInsertParams; + result: IJsonInsertResult; +} + +export type JsonReplaceParams = []; + +export interface IJsonReplaceResult { + id: number; + name: string; + originalData: object; + withNewUsername: any; +} + +export interface IJsonReplaceQuery { + params: JsonReplaceParams; + result: IJsonReplaceResult; +} + +export type JsonRemoveParams = []; + +export interface IJsonRemoveResult { + id: number; + name: string; + originalData: object; + withoutAge: any; +} + +export interface IJsonRemoveQuery { + params: JsonRemoveParams; + result: IJsonRemoveResult; +} + +export type JsonMergePatchParams = []; + +export interface IJsonMergePatchResult { + id: number; + mergedData: any; + name: string; + originalData: object; +} + +export interface IJsonMergePatchQuery { + params: JsonMergePatchParams; + result: IJsonMergePatchResult; +} + +export type JsonMergePreserveParams = []; + +export interface IJsonMergePreserveResult { + id: number; + mergedData: any; + name: string; + originalData: object; +} + +export interface IJsonMergePreserveQuery { + params: JsonMergePreserveParams; + result: IJsonMergePreserveResult; +} + +export type JsonSearchParams = []; + +export interface IJsonSearchResult { + emailPath: any; + id: number; + name: string; + usernamePath: any; +} + +export interface IJsonSearchQuery { + params: JsonSearchParams; + result: IJsonSearchResult; +} + +export type JsonDepthParams = []; + +export interface IJsonDepthResult { + dataDepth: any; + id: number; + name: string; +} + +export interface IJsonDepthQuery { + params: JsonDepthParams; + result: IJsonDepthResult; +} + +export type JsonValidParams = []; + +export interface IJsonValidResult { + id: number; + isValidJson: any; + name: string; +} + +export interface IJsonValidQuery { + params: JsonValidParams; + result: IJsonValidResult; +} + diff --git a/tests/demo_json/mysql/json_object_functions.ts b/tests/demo_json/mysql/json_object_functions.ts new file mode 100644 index 00000000..20ba1f32 --- /dev/null +++ b/tests/demo_json/mysql/json_object_functions.ts @@ -0,0 +1,204 @@ +import { sql } from 'sqlx-ts' + + +// Test JSON_KEYS - get all keys from object +const jsonObjectKeys = sql` +-- @db: db_mysql +-- @name: json object keys +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + JSON_KEYS(data) AS object_keys +FROM json_test_data +WHERE json_test_data.name = 'user_profile' +` + +// Test JSON_KEYS with path +const jsonObjectKeysPath = sql` +-- @db: db_mysql +-- @name: json object keys path +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + JSON_KEYS(data, '$.address') AS address_keys +FROM json_test_data +WHERE json_test_data.name = 'user_with_address' +` + +// Test JSON_TYPE - get type of JSON value +const jsonTypeof = sql` +-- @db: db_mysql +-- @name: json typeof +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + JSON_TYPE(JSON_EXTRACT(data, '$.username')) AS username_type, + JSON_TYPE(JSON_EXTRACT(data, '$.age')) AS age_type, + JSON_TYPE(JSON_EXTRACT(data, '$.active')) AS active_type, + JSON_TYPE(JSON_EXTRACT(data, '$.items')) AS items_type, + JSON_TYPE(JSON_EXTRACT(data, '$.tags')) AS tags_type +FROM json_test_data +WHERE json_test_data.name IN ('user_profile', 'shopping_cart', 'tags') +` + +// Test JSON_CONTAINS - check if JSON contains value +const jsonContains = sql` +-- @db: db_mysql +-- @name: json contains +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + JSON_CONTAINS(data, JSON_QUOTE('john_doe'), '$.username') AS has_specific_username, + JSON_CONTAINS(data, 'true', '$.active') AS is_active +FROM json_test_data +WHERE json_test_data.name = 'user_profile' +` + +// Test JSON_CONTAINS_PATH - check if path exists +const jsonContainsPath = sql` +-- @db: db_mysql +-- @name: json contains path +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + JSON_CONTAINS_PATH(data, 'one', '$.username') AS has_username, + JSON_CONTAINS_PATH(data, 'one', '$.address') AS has_address, + JSON_CONTAINS_PATH(data, 'one', '$.nonexistent') AS has_nonexistent, + JSON_CONTAINS_PATH(data, 'all', '$.username', '$.email') AS has_both +FROM json_test_data +WHERE json_test_data.name IN ('user_profile', 'user_with_address') +` + +// Test JSON_OBJECT - build JSON objects +const jsonObjectBuild = sql` +-- @db: db_mysql +-- @name: json object build +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + JSON_OBJECT( + 'id', json_test_data.id, + 'name', json_test_data.name, + 'username', JSON_EXTRACT(data, '$.username'), + 'email', JSON_EXTRACT(data, '$.email') + ) AS user_summary +FROM json_test_data +WHERE json_test_data.name = 'user_profile' +` + +// Test JSON_SET - update value in JSON +const jsonSet = sql` +-- @db: db_mysql +-- @name: json set +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + data AS original_data, + JSON_SET(data, '$.age', 31) AS updated_age, + JSON_SET(data, '$.address.city', 'New York') AS updated_city +FROM json_test_data +WHERE json_test_data.name IN ('user_profile', 'user_with_address') +LIMIT 2 +` + +// Test JSON_INSERT - insert value into JSON +const jsonInsert = sql` +-- @db: db_mysql +-- @name: json insert +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + data AS original_data, + JSON_INSERT(data, '$.phone', '555-1234') AS with_phone +FROM json_test_data +WHERE json_test_data.name = 'user_profile' +` + +// Test JSON_REPLACE - replace existing value +const jsonReplace = sql` +-- @db: db_mysql +-- @name: json replace +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + data AS original_data, + JSON_REPLACE(data, '$.username', 'new_username') AS with_new_username +FROM json_test_data +WHERE json_test_data.name = 'user_profile' +` + +// Test JSON_REMOVE - remove keys from JSON +const jsonRemove = sql` +-- @db: db_mysql +-- @name: json remove +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + data AS original_data, + JSON_REMOVE(data, '$.age') AS without_age +FROM json_test_data +WHERE json_test_data.name = 'user_profile' +` + +// Test JSON_MERGE_PATCH - merge JSON objects +const jsonMergePatch = sql` +-- @db: db_mysql +-- @name: json merge patch +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + data AS original_data, + JSON_MERGE_PATCH(data, JSON_OBJECT('verified', true, 'lastLogin', '2024-01-15')) AS merged_data +FROM json_test_data +WHERE json_test_data.name = 'user_profile' +` + +// Test JSON_MERGE_PRESERVE - merge preserving all values +const jsonMergePreserve = sql` +-- @db: db_mysql +-- @name: json merge preserve +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + data AS original_data, + JSON_MERGE_PRESERVE(data, JSON_OBJECT('newField', 'newValue')) AS merged_data +FROM json_test_data +WHERE json_test_data.name = 'user_profile' +` + +// Test JSON_SEARCH - find values in JSON +const jsonSearch = sql` +-- @db: db_mysql +-- @name: json search +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + JSON_SEARCH(data, 'one', 'john_doe') AS username_path, + JSON_SEARCH(data, 'one', 'john@example.com') AS email_path +FROM json_test_data +WHERE json_test_data.name = 'user_profile' +` + +// Test JSON_DEPTH - get depth of JSON +const jsonDepth = sql` +-- @db: db_mysql +-- @name: json depth +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + JSON_DEPTH(data) AS data_depth +FROM json_test_data +WHERE json_test_data.name IN ('user_profile', 'nested_config') +` + +// Test JSON_VALID - validate JSON +const jsonValid = sql` +-- @db: db_mysql +-- @name: json valid +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + JSON_VALID(data) AS is_valid_json +FROM json_test_data +LIMIT 3 +` diff --git a/tests/demo_json/mysql/json_operations.queries.ts b/tests/demo_json/mysql/json_operations.queries.ts new file mode 100644 index 00000000..2392c7e4 --- /dev/null +++ b/tests/demo_json/mysql/json_operations.queries.ts @@ -0,0 +1,12 @@ +export type JsonOperatorsSelectParams = []; + +export interface IJsonOperatorsSelectResult { + extractedName: any; + id: number; + name: string; +} + +export interface IJsonOperatorsSelectQuery { + params: JsonOperatorsSelectParams; + result: IJsonOperatorsSelectResult; +} diff --git a/tests/demo_json/mysql/json_operations.snapshot.ts b/tests/demo_json/mysql/json_operations.snapshot.ts new file mode 100644 index 00000000..2392c7e4 --- /dev/null +++ b/tests/demo_json/mysql/json_operations.snapshot.ts @@ -0,0 +1,12 @@ +export type JsonOperatorsSelectParams = []; + +export interface IJsonOperatorsSelectResult { + extractedName: any; + id: number; + name: string; +} + +export interface IJsonOperatorsSelectQuery { + params: JsonOperatorsSelectParams; + result: IJsonOperatorsSelectResult; +} diff --git a/tests/demo_json/mysql/json_operations.ts b/tests/demo_json/mysql/json_operations.ts new file mode 100644 index 00000000..080c2003 --- /dev/null +++ b/tests/demo_json/mysql/json_operations.ts @@ -0,0 +1,16 @@ +import { sql } from 'sqlx-ts' + +// JSON operators in SELECT - extract values +const jsonOperatorsSelect = sql` +-- @db: db_mysql +-- @name: json operators select +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + JSON_UNQUOTE(JSON_EXTRACT( + JSON_OBJECT('id', json_test_data.id, 'name', json_test_data.name), + '$.name' + )) AS extracted_name +FROM json_test_data +WHERE json_test_data.name = 'user_profile' +` diff --git a/tests/demo_json/mysql/json_reserved_keywords.queries.ts b/tests/demo_json/mysql/json_reserved_keywords.queries.ts new file mode 100644 index 00000000..ae94faa2 --- /dev/null +++ b/tests/demo_json/mysql/json_reserved_keywords.queries.ts @@ -0,0 +1,37 @@ +export type JsonReservedKeywordsParams = []; + +export interface IJsonReservedKeywordsResult { + id: number; + name: string; + reservedKeywordsObject: { "class": string; "interface": string; "type": string; "const": string; "let": string; "function": string; "return": boolean; "import": string; "export": string; "async": string }; +} + +export interface IJsonReservedKeywordsQuery { + params: JsonReservedKeywordsParams; + result: IJsonReservedKeywordsResult; +} + +export type JsonInvalidIdentifiersParams = []; + +export interface IJsonInvalidIdentifiersResult { + id: number; + invalidIdentifiersObject: { "field-name": string; "field name": string; "123field": string; "user@email": string; "field.nested": string }; + name: string; +} + +export interface IJsonInvalidIdentifiersQuery { + params: JsonInvalidIdentifiersParams; + result: IJsonInvalidIdentifiersResult; +} + +export type JsonMixedIdentifiersParams = []; + +export interface IJsonMixedIdentifiersResult { + id: number; + mixedIdentifiersObject: { validName: string; "invalid-name": number; _underscore: string; $dollar: string; "class": string; "123start": string }; +} + +export interface IJsonMixedIdentifiersQuery { + params: JsonMixedIdentifiersParams; + result: IJsonMixedIdentifiersResult; +} diff --git a/tests/demo_json/mysql/json_reserved_keywords.snapshot.ts b/tests/demo_json/mysql/json_reserved_keywords.snapshot.ts new file mode 100644 index 00000000..ee739293 --- /dev/null +++ b/tests/demo_json/mysql/json_reserved_keywords.snapshot.ts @@ -0,0 +1,38 @@ +export type JsonReservedKeywordsParams = []; + +export interface IJsonReservedKeywordsResult { + id: number; + name: string; + reservedKeywordsObject: { "class": string; "interface": string; "type": string; "const": string; "let": string; "function": string; "return": boolean; "import": string; "export": string; "async": string }; +} + +export interface IJsonReservedKeywordsQuery { + params: JsonReservedKeywordsParams; + result: IJsonReservedKeywordsResult; +} + +export type JsonInvalidIdentifiersParams = []; + +export interface IJsonInvalidIdentifiersResult { + id: number; + invalidIdentifiersObject: { "field-name": string; "field name": string; "123field": string; "user@email": string; "field.nested": string }; + name: string; +} + +export interface IJsonInvalidIdentifiersQuery { + params: JsonInvalidIdentifiersParams; + result: IJsonInvalidIdentifiersResult; +} + +export type JsonMixedIdentifiersParams = []; + +export interface IJsonMixedIdentifiersResult { + id: number; + mixedIdentifiersObject: { validName: string; "invalid-name": number; _underscore: string; $dollar: string; "class": string; "123start": string }; +} + +export interface IJsonMixedIdentifiersQuery { + params: JsonMixedIdentifiersParams; + result: IJsonMixedIdentifiersResult; +} + diff --git a/tests/demo_json/mysql/json_reserved_keywords.ts b/tests/demo_json/mysql/json_reserved_keywords.ts new file mode 100644 index 00000000..ff97c1dd --- /dev/null +++ b/tests/demo_json/mysql/json_reserved_keywords.ts @@ -0,0 +1,63 @@ +import { sql } from 'sqlx-ts' + +// Test JSON_OBJECT with TypeScript reserved keywords +const jsonReservedKeywords = sql` +-- @db: db_mysql +-- @name: json reserved keywords +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + JSON_OBJECT( + 'class', 'User', + 'interface', 'IUser', + 'type', 'object', + 'const', 'constant', + 'let', 'variable', + 'function', 'method', + 'return', true, + 'import', 'module', + 'export', 'default', + 'async', 'promise' + ) AS reserved_keywords_object +FROM json_test_data +WHERE json_test_data.name = 'user_profile' +LIMIT 1 +` + +// Test JSON_OBJECT with invalid TypeScript identifiers +const jsonInvalidIdentifiers = sql` +-- @db: db_mysql +-- @name: json invalid identifiers +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + JSON_OBJECT( + 'field-name', 'hyphenated', + 'field name', 'with space', + '123field', 'starts with number', + 'user@email', 'special chars', + 'field.nested', 'dotted name' + ) AS invalid_identifiers_object +FROM json_test_data +WHERE json_test_data.name = 'user_profile' +LIMIT 1 +` + +// Test mixed valid and invalid identifiers +const jsonMixedIdentifiers = sql` +-- @db: db_mysql +-- @name: json mixed identifiers +SELECT + json_test_data.id AS id, + JSON_OBJECT( + 'validName', json_test_data.name, + 'invalid-name', json_test_data.id, + '_underscore', 'valid', + '$dollar', 'valid', + 'class', 'reserved', + '123start', 'invalid' + ) AS mixed_identifiers_object +FROM json_test_data +WHERE json_test_data.name = 'user_profile' +LIMIT 1 +` diff --git a/tests/demo/postgres/array_operations.queries.ts b/tests/demo_json/postgres/array_operations.queries.ts similarity index 100% rename from tests/demo/postgres/array_operations.queries.ts rename to tests/demo_json/postgres/array_operations.queries.ts diff --git a/tests/demo/postgres/array_operations.snapshot.ts b/tests/demo_json/postgres/array_operations.snapshot.ts similarity index 100% rename from tests/demo/postgres/array_operations.snapshot.ts rename to tests/demo_json/postgres/array_operations.snapshot.ts diff --git a/tests/demo/postgres/array_operations.ts b/tests/demo_json/postgres/array_operations.ts similarity index 100% rename from tests/demo/postgres/array_operations.ts rename to tests/demo_json/postgres/array_operations.ts diff --git a/tests/demo_json/postgres/json_access_operators.queries.ts b/tests/demo_json/postgres/json_access_operators.queries.ts new file mode 100644 index 00000000..2e1dde7c --- /dev/null +++ b/tests/demo_json/postgres/json_access_operators.queries.ts @@ -0,0 +1,129 @@ +export type JsonFieldAccessParams = []; + +export interface IJsonFieldAccessResult { + activeJson: string; + ageJson: string; + id: number; + name: string; + usernameJson: string; +} + +export interface IJsonFieldAccessQuery { + params: JsonFieldAccessParams; + result: IJsonFieldAccessResult; +} + +export type JsonFieldAccessTextParams = []; + +export interface IJsonFieldAccessTextResult { + active: boolean; + age: number; + email: string; + id: number; + name: string; + username: string; +} + +export interface IJsonFieldAccessTextQuery { + params: JsonFieldAccessTextParams; + result: IJsonFieldAccessTextResult; +} + +export type JsonNestedAccessParams = []; + +export interface IJsonNestedAccessResult { + addressJson: string; + city: string; + cityJson: string; + id: number; + name: string; + street: string; + streetJson: string; + zipCode: string; +} + +export interface IJsonNestedAccessQuery { + params: JsonNestedAccessParams; + result: IJsonNestedAccessResult; +} + +export type JsonArrayAccessParams = []; + +export interface IJsonArrayAccessResult { + firstItemJson: number; + firstItemName: string; + firstItemPrice: string; + id: number; + itemsJson: string; + name: string; + secondItemJson: number; +} + +export interface IJsonArrayAccessQuery { + params: JsonArrayAccessParams; + result: IJsonArrayAccessResult; +} + +export type JsonPathAccessParams = []; + +export interface IJsonPathAccessResult { + firstItemJson: string; + firstItemName: string; + firstItemRarity: string; + id: number; + level: string; + levelJson: string; + name: string; +} + +export interface IJsonPathAccessQuery { + params: JsonPathAccessParams; + result: IJsonPathAccessResult; +} + +export type JsonDeepPathAccessParams = []; + +export interface IJsonDeepPathAccessResult { + darkMode: string; + dbHost: string; + dbHostJson: string; + dbPort: string; + emailNotifications: string; + id: number; + name: string; +} + +export interface IJsonDeepPathAccessQuery { + params: JsonDeepPathAccessParams; + result: IJsonDeepPathAccessResult; +} + +export type JsonFilterByFieldParams = []; + +export interface IJsonFilterByFieldResult { + email: string; + id: number; + name: string; + username: string; +} + +export interface IJsonFilterByFieldQuery { + params: JsonFilterByFieldParams; + result: IJsonFilterByFieldResult; +} + +export type JsonNullHandlingParams = []; + +export interface IJsonNullHandlingResult { + firstComment: string; + firstReviewer: string; + id: number; + secondComment: string; + thirdComment: string; + thirdReviewer: string; +} + +export interface IJsonNullHandlingQuery { + params: JsonNullHandlingParams; + result: IJsonNullHandlingResult; +} diff --git a/tests/demo_json/postgres/json_access_operators.snapshot.ts b/tests/demo_json/postgres/json_access_operators.snapshot.ts new file mode 100644 index 00000000..5897fcfb --- /dev/null +++ b/tests/demo_json/postgres/json_access_operators.snapshot.ts @@ -0,0 +1,130 @@ +export type JsonFieldAccessParams = []; + +export interface IJsonFieldAccessResult { + activeJson: string; + ageJson: string; + id: number; + name: string; + usernameJson: string; +} + +export interface IJsonFieldAccessQuery { + params: JsonFieldAccessParams; + result: IJsonFieldAccessResult; +} + +export type JsonFieldAccessTextParams = []; + +export interface IJsonFieldAccessTextResult { + active: boolean; + age: number; + email: string; + id: number; + name: string; + username: string; +} + +export interface IJsonFieldAccessTextQuery { + params: JsonFieldAccessTextParams; + result: IJsonFieldAccessTextResult; +} + +export type JsonNestedAccessParams = []; + +export interface IJsonNestedAccessResult { + addressJson: string; + city: string; + cityJson: string; + id: number; + name: string; + street: string; + streetJson: string; + zipCode: string; +} + +export interface IJsonNestedAccessQuery { + params: JsonNestedAccessParams; + result: IJsonNestedAccessResult; +} + +export type JsonArrayAccessParams = []; + +export interface IJsonArrayAccessResult { + firstItemJson: number; + firstItemName: string; + firstItemPrice: string; + id: number; + itemsJson: string; + name: string; + secondItemJson: number; +} + +export interface IJsonArrayAccessQuery { + params: JsonArrayAccessParams; + result: IJsonArrayAccessResult; +} + +export type JsonPathAccessParams = []; + +export interface IJsonPathAccessResult { + firstItemJson: string; + firstItemName: string; + firstItemRarity: string; + id: number; + level: string; + levelJson: string; + name: string; +} + +export interface IJsonPathAccessQuery { + params: JsonPathAccessParams; + result: IJsonPathAccessResult; +} + +export type JsonDeepPathAccessParams = []; + +export interface IJsonDeepPathAccessResult { + darkMode: string; + dbHost: string; + dbHostJson: string; + dbPort: string; + emailNotifications: string; + id: number; + name: string; +} + +export interface IJsonDeepPathAccessQuery { + params: JsonDeepPathAccessParams; + result: IJsonDeepPathAccessResult; +} + +export type JsonFilterByFieldParams = []; + +export interface IJsonFilterByFieldResult { + email: string; + id: number; + name: string; + username: string; +} + +export interface IJsonFilterByFieldQuery { + params: JsonFilterByFieldParams; + result: IJsonFilterByFieldResult; +} + +export type JsonNullHandlingParams = []; + +export interface IJsonNullHandlingResult { + firstComment: string; + firstReviewer: string; + id: number; + secondComment: string; + thirdComment: string; + thirdReviewer: string; +} + +export interface IJsonNullHandlingQuery { + params: JsonNullHandlingParams; + result: IJsonNullHandlingResult; +} + diff --git a/tests/demo_json/postgres/json_access_operators.ts b/tests/demo_json/postgres/json_access_operators.ts new file mode 100644 index 00000000..79aab8a2 --- /dev/null +++ b/tests/demo_json/postgres/json_access_operators.ts @@ -0,0 +1,116 @@ +import { sql } from 'sqlx-ts' + +// Test -> operator (get JSON object field as JSON) +const jsonFieldAccess = sql` +-- @name: json field access +SELECT + id, + name, + data -> 'username' AS username_json, + data -> 'age' AS age_json, + data -> 'active' AS active_json +FROM json_test_data +WHERE name = 'user_profile' +` + +// Test ->> operator (get JSON object field as text) +const jsonFieldAccessText = sql` +-- @name: json field access text +SELECT + id, + name, + data ->> 'username' AS username, + data ->> 'email' AS email, + (data ->> 'age')::integer AS age, + (data ->> 'active')::boolean AS active +FROM json_test_data +WHERE name = 'user_profile' +` + +// Test nested field access with -> and ->> +const jsonNestedAccess = sql` +-- @name: json nested access +SELECT + id, + name, + data -> 'address' AS address_json, + data -> 'address' -> 'city' AS city_json, + data -> 'address' ->> 'city' AS city, + data -> 'address' ->> 'zipCode' AS zip_code, + data #> '{address, street}' AS street_json, + data #>> '{address, street}' AS street +FROM json_test_data +WHERE name = 'user_with_address' +` + +// Test array element access by index +const jsonArrayAccess = sql` +-- @name: json array access +SELECT + id, + name, + data -> 'items' AS items_json, + data -> 'items' -> 0 AS first_item_json, + data -> 'items' -> 1 AS second_item_json, + data -> 'items' -> 0 ->> 'name' AS first_item_name, + data -> 'items' -> 0 ->> 'price' AS first_item_price +FROM json_test_data +WHERE name = 'shopping_cart' +` + +// Test #> operator (get JSON object at path) +const jsonPathAccess = sql` +-- @name: json path access +SELECT + id, + name, + data #> '{stats, level}' AS level_json, + data #> '{stats, inventory, 0, item}' AS first_item_json, + data #>> '{stats, level}' AS level, + data #>> '{stats, inventory, 0, item}' AS first_item_name, + data #>> '{stats, inventory, 0, rarity}' AS first_item_rarity +FROM json_test_data +WHERE name = 'game_stats' +` + +// Test deep nested path access +const jsonDeepPathAccess = sql` +-- @name: json deep path access +SELECT + id, + name, + data #> '{app, settings, database, host}' AS db_host_json, + data #>> '{app, settings, database, host}' AS db_host, + data #>> '{app, settings, database, port}' AS db_port, + data #>> '{app, settings, features, darkMode}' AS dark_mode, + data #>> '{app, settings, features, notifications, email}' AS email_notifications +FROM json_test_data +WHERE name = 'nested_config' +` + +// Test mixed operators in WHERE clause +const jsonFilterByField = sql` +-- @name: json filter by field +SELECT + id, + name, + data ->> 'username' AS username, + data ->> 'email' AS email +FROM json_test_data +WHERE data ->> 'active' = 'true' + AND (data ->> 'age')::integer > 25 +` + +// Test null handling in JSON +const jsonNullHandling = sql` +-- @name: json null handling +SELECT + id, + data -> 'reviews' -> 0 ->> 'comment' AS first_comment, + data -> 'reviews' -> 1 ->> 'comment' AS second_comment, + data -> 'reviews' -> 2 ->> 'comment' AS third_comment, + data -> 'reviews' -> 0 ->> 'reviewer' AS first_reviewer, + data -> 'reviews' -> 2 ->> 'reviewer' AS third_reviewer +FROM json_test_data +WHERE name = 'product_reviews' +` diff --git a/tests/demo_json/postgres/json_array_functions.queries.ts b/tests/demo_json/postgres/json_array_functions.queries.ts new file mode 100644 index 00000000..cd1e5fed --- /dev/null +++ b/tests/demo_json/postgres/json_array_functions.queries.ts @@ -0,0 +1,26 @@ +export type JsonbArrayLengthParams = []; + +export interface IJsonbArrayLengthResult { + itemsCount: any; + jsonTestDataId: number; + jsonTestDataName: string; + tagsCount: any; +} + +export interface IJsonbArrayLengthQuery { + params: JsonbArrayLengthParams; + result: IJsonbArrayLengthResult; +} + +export type JsonbArrayContainsParams = []; + +export interface IJsonbArrayContainsResult { + jsonTestDataId: number; + jsonTestDataName: string; + tags: string; +} + +export interface IJsonbArrayContainsQuery { + params: JsonbArrayContainsParams; + result: IJsonbArrayContainsResult; +} diff --git a/tests/demo_json/postgres/json_array_functions.snapshot.ts b/tests/demo_json/postgres/json_array_functions.snapshot.ts new file mode 100644 index 00000000..52fb189e --- /dev/null +++ b/tests/demo_json/postgres/json_array_functions.snapshot.ts @@ -0,0 +1,27 @@ +export type JsonbArrayLengthParams = []; + +export interface IJsonbArrayLengthResult { + itemsCount: any; + jsonTestDataId: number; + jsonTestDataName: string; + tagsCount: any; +} + +export interface IJsonbArrayLengthQuery { + params: JsonbArrayLengthParams; + result: IJsonbArrayLengthResult; +} + +export type JsonbArrayContainsParams = []; + +export interface IJsonbArrayContainsResult { + jsonTestDataId: number; + jsonTestDataName: string; + tags: string; +} + +export interface IJsonbArrayContainsQuery { + params: JsonbArrayContainsParams; + result: IJsonbArrayContainsResult; +} + diff --git a/tests/demo_json/postgres/json_array_functions.ts b/tests/demo_json/postgres/json_array_functions.ts new file mode 100644 index 00000000..1a27feb4 --- /dev/null +++ b/tests/demo_json/postgres/json_array_functions.ts @@ -0,0 +1,25 @@ +import { sql } from 'sqlx-ts' + +// Test jsonb_array_length - get array length +const jsonbArrayLength = sql` +-- @name: jsonb array length +SELECT + json_test_data.id, + json_test_data.name, + jsonb_array_length(data -> 'items') AS items_count, + jsonb_array_length(data -> 'tags') AS tags_count +FROM json_test_data +WHERE json_test_data.name IN ('shopping_cart', 'tags') +` + +// Test array contains using @> operator +const jsonbArrayContains = sql` +-- @name: jsonb array contains +SELECT + json_test_data.id, + json_test_data.name, + data -> 'tags' AS tags +FROM json_test_data +WHERE data -> 'tags' @> '["postgresql"]'::jsonb +` + diff --git a/tests/demo_json/postgres/json_comprehensive.queries.ts b/tests/demo_json/postgres/json_comprehensive.queries.ts new file mode 100644 index 00000000..8c2e6431 --- /dev/null +++ b/tests/demo_json/postgres/json_comprehensive.queries.ts @@ -0,0 +1,136 @@ +export type JsonAccessOperatorsParams = []; + +export interface IJsonAccessOperatorsResult { + age: number; + email: string; + id: number; + name: string; + username: string; +} + +export interface IJsonAccessOperatorsQuery { + params: JsonAccessOperatorsParams; + result: IJsonAccessOperatorsResult; +} + +export type JsonNestedAccessParams = []; + +export interface IJsonNestedAccessResult { + city: string; + id: number; + name: string; + zipCode: string; +} + +export interface IJsonNestedAccessQuery { + params: JsonNestedAccessParams; + result: IJsonNestedAccessResult; +} + +export type JsonArrayIndexParams = []; + +export interface IJsonArrayIndexResult { + firstItemName: string; + firstItemPrice: number; + id: number; + name: string; +} + +export interface IJsonArrayIndexQuery { + params: JsonArrayIndexParams; + result: IJsonArrayIndexResult; +} + +export type JsonArrayLengthParams = []; + +export interface IJsonArrayLengthResult { + id: number; + name: string; + tagsCount: any; +} + +export interface IJsonArrayLengthQuery { + params: JsonArrayLengthParams; + result: IJsonArrayLengthResult; +} + +export type JsonTypeofParams = []; + +export interface IJsonTypeofResult { + ageType: any; + id: number; + tagsType: any; + usernameType: any; +} + +export interface IJsonTypeofQuery { + params: JsonTypeofParams; + result: IJsonTypeofResult; +} + +export type JsonKeyExistsParams = []; + +export interface IJsonKeyExistsResult { + hasAddress: string; + hasUsername: string; + id: number; + name: string; +} + +export interface IJsonKeyExistsQuery { + params: JsonKeyExistsParams; + result: IJsonKeyExistsResult; +} + +export type JsonContainsParams = []; + +export interface IJsonContainsResult { + id: number; + isActive: string; + name: string; +} + +export interface IJsonContainsQuery { + params: JsonContainsParams; + result: IJsonContainsResult; +} + +export type JsonBuildObjectTypedParams = []; + +export interface IJsonBuildObjectTypedResult { + id: number; + name: string; + userSummary: { id: number; name: string; username: any; email: any }; +} + +export interface IJsonBuildObjectTypedQuery { + params: JsonBuildObjectTypedParams; + result: IJsonBuildObjectTypedResult; +} + +export type JsonFilterParams = []; + +export interface IJsonFilterResult { + id: number; + name: string; + username: string; +} + +export interface IJsonFilterQuery { + params: JsonFilterParams; + result: IJsonFilterResult; +} + +export type JsonDeepPathParams = []; + +export interface IJsonDeepPathResult { + appName: string; + dbHost: string; + dbPort: number; + id: number; +} + +export interface IJsonDeepPathQuery { + params: JsonDeepPathParams; + result: IJsonDeepPathResult; +} diff --git a/tests/demo_json/postgres/json_comprehensive.snapshot.ts b/tests/demo_json/postgres/json_comprehensive.snapshot.ts new file mode 100644 index 00000000..1945a372 --- /dev/null +++ b/tests/demo_json/postgres/json_comprehensive.snapshot.ts @@ -0,0 +1,137 @@ +export type JsonAccessOperatorsParams = []; + +export interface IJsonAccessOperatorsResult { + age: number; + email: string; + id: number; + name: string; + username: string; +} + +export interface IJsonAccessOperatorsQuery { + params: JsonAccessOperatorsParams; + result: IJsonAccessOperatorsResult; +} + +export type JsonNestedAccessParams = []; + +export interface IJsonNestedAccessResult { + city: string; + id: number; + name: string; + zipCode: string; +} + +export interface IJsonNestedAccessQuery { + params: JsonNestedAccessParams; + result: IJsonNestedAccessResult; +} + +export type JsonArrayIndexParams = []; + +export interface IJsonArrayIndexResult { + firstItemName: string; + firstItemPrice: number; + id: number; + name: string; +} + +export interface IJsonArrayIndexQuery { + params: JsonArrayIndexParams; + result: IJsonArrayIndexResult; +} + +export type JsonArrayLengthParams = []; + +export interface IJsonArrayLengthResult { + id: number; + name: string; + tagsCount: any; +} + +export interface IJsonArrayLengthQuery { + params: JsonArrayLengthParams; + result: IJsonArrayLengthResult; +} + +export type JsonTypeofParams = []; + +export interface IJsonTypeofResult { + ageType: any; + id: number; + tagsType: any; + usernameType: any; +} + +export interface IJsonTypeofQuery { + params: JsonTypeofParams; + result: IJsonTypeofResult; +} + +export type JsonKeyExistsParams = []; + +export interface IJsonKeyExistsResult { + hasAddress: string; + hasUsername: string; + id: number; + name: string; +} + +export interface IJsonKeyExistsQuery { + params: JsonKeyExistsParams; + result: IJsonKeyExistsResult; +} + +export type JsonContainsParams = []; + +export interface IJsonContainsResult { + id: number; + isActive: string; + name: string; +} + +export interface IJsonContainsQuery { + params: JsonContainsParams; + result: IJsonContainsResult; +} + +export type JsonBuildObjectTypedParams = []; + +export interface IJsonBuildObjectTypedResult { + id: number; + name: string; + userSummary: { id: number; name: string; username: any; email: any }; +} + +export interface IJsonBuildObjectTypedQuery { + params: JsonBuildObjectTypedParams; + result: IJsonBuildObjectTypedResult; +} + +export type JsonFilterParams = []; + +export interface IJsonFilterResult { + id: number; + name: string; + username: string; +} + +export interface IJsonFilterQuery { + params: JsonFilterParams; + result: IJsonFilterResult; +} + +export type JsonDeepPathParams = []; + +export interface IJsonDeepPathResult { + appName: string; + dbHost: string; + dbPort: number; + id: number; +} + +export interface IJsonDeepPathQuery { + params: JsonDeepPathParams; + result: IJsonDeepPathResult; +} + diff --git a/tests/demo_json/postgres/json_comprehensive.ts b/tests/demo_json/postgres/json_comprehensive.ts new file mode 100644 index 00000000..207ec0fb --- /dev/null +++ b/tests/demo_json/postgres/json_comprehensive.ts @@ -0,0 +1,124 @@ +import { sql } from 'sqlx-ts' + +// Test 1: JSON access operators -> and ->> +const jsonAccessOperators = sql` +-- @name: json access operators +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + data ->> 'username' AS username, + data ->> 'email' AS email, + (data ->> 'age')::integer AS age +FROM json_test_data +WHERE json_test_data.name = 'user_profile' +` + +// Test 2: Nested JSON access with #> and #>> +const jsonNestedAccess = sql` +-- @name: json nested access +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + data #>> '{address, city}' AS city, + data #>> '{address, zipCode}' AS zip_code +FROM json_test_data +WHERE json_test_data.name = 'user_with_address' +` + +// Test 3: JSON array access by index +const jsonArrayIndex = sql` +-- @name: json array index +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + data -> 'items' -> 0 ->> 'name' AS first_item_name, + (data -> 'items' -> 0 ->> 'price')::numeric AS first_item_price +FROM json_test_data +WHERE json_test_data.name = 'shopping_cart' +` + +// Test 4: JSON array length +const jsonArrayLength = sql` +-- @name: json array length +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + jsonb_array_length(data -> 'tags') AS tags_count +FROM json_test_data +WHERE json_test_data.name = 'tags' +` + +// Test 5: JSON typeof +const jsonTypeof = sql` +-- @name: json typeof +SELECT + json_test_data.id AS id, + jsonb_typeof(data -> 'username') AS username_type, + jsonb_typeof(data -> 'age') AS age_type, + jsonb_typeof(data -> 'tags') AS tags_type +FROM json_test_data +WHERE json_test_data.name IN ('user_profile', 'tags') +` + +// Test 6: JSON key existence with ? +const jsonKeyExists = sql` +-- @name: json key exists +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + (data ? 'username')::text AS has_username, + (data ? 'address')::text AS has_address +FROM json_test_data +WHERE json_test_data.name IN ('user_profile', 'user_with_address') +` + +// Test 7: JSON containment with @> +const jsonContains = sql` +-- @name: json contains +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + (data @> '{"active": true}'::jsonb)::text AS is_active +FROM json_test_data +WHERE json_test_data.name = 'user_profile' +` + +// Test 8: JSON build object with typed fields +const jsonBuildObjectTyped = sql` +-- @name: json build object typed +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + jsonb_build_object( + 'id', json_test_data.id, + 'name', json_test_data.name, + 'username', data ->> 'username', + 'email', data ->> 'email' + ) AS user_summary +FROM json_test_data +WHERE json_test_data.name = 'user_profile' +` + +// Test 9: Filter using JSON operators +const jsonFilter = sql` +-- @name: json filter +SELECT + json_test_data.id AS id, + json_test_data.name AS name, + data ->> 'username' AS username +FROM json_test_data +WHERE (data ->> 'active')::boolean = true + AND (data ->> 'age')::integer > 25 +` + +// Test 10: JSON path queries +const jsonDeepPath = sql` +-- @name: json deep path +SELECT + json_test_data.id AS id, + data #>> '{app, name}' AS app_name, + data #>> '{app, settings, database, host}' AS db_host, + (data #>> '{app, settings, database, port}')::integer AS db_port +FROM json_test_data +WHERE json_test_data.name = 'nested_config' +` diff --git a/tests/demo_json/postgres/json_object_functions.queries.ts b/tests/demo_json/postgres/json_object_functions.queries.ts new file mode 100644 index 00000000..fb2fc6d6 --- /dev/null +++ b/tests/demo_json/postgres/json_object_functions.queries.ts @@ -0,0 +1,142 @@ +export type JsonbObjectKeysParams = []; + +export interface IJsonbObjectKeysResult { + jsonTestDataId: number; + jsonTestDataName: string; + objectKey: any; +} + +export interface IJsonbObjectKeysQuery { + params: JsonbObjectKeysParams; + result: IJsonbObjectKeysResult; +} + +export type JsonbTypeofParams = []; + +export interface IJsonbTypeofResult { + activeType: any; + ageType: any; + itemsType: any; + jsonTestDataId: number; + jsonTestDataName: string; + tagsType: any; + usernameType: any; +} + +export interface IJsonbTypeofQuery { + params: JsonbTypeofParams; + result: IJsonbTypeofResult; +} + +export type JsonbStripNullsParams = []; + +export interface IJsonbStripNullsResult { + jsonTestDataId: number; + jsonTestDataName: string; + reviewWithNulls: number; + reviewWithoutNulls: any; +} + +export interface IJsonbStripNullsQuery { + params: JsonbStripNullsParams; + result: IJsonbStripNullsResult; +} + +export type JsonbKeyExistsParams = []; + +export interface IJsonbKeyExistsResult { + hasAddress: string; + hasNonexistent: string; + hasUsername: string; + jsonTestDataId: number; + jsonTestDataName: string; +} + +export interface IJsonbKeyExistsQuery { + params: JsonbKeyExistsParams; + result: IJsonbKeyExistsResult; +} + +export type JsonbAnyKeyExistsParams = []; + +export interface IJsonbAnyKeyExistsResult { + hasAnyContact: any; + jsonTestDataId: number; + jsonTestDataName: string; +} + +export interface IJsonbAnyKeyExistsQuery { + params: JsonbAnyKeyExistsParams; + result: IJsonbAnyKeyExistsResult; +} + +export type JsonbAllKeysExistParams = []; + +export interface IJsonbAllKeysExistResult { + hasAllRequired: any; + hasAllWithPhone: any; + jsonTestDataId: number; + jsonTestDataName: string; +} + +export interface IJsonbAllKeysExistQuery { + params: JsonbAllKeysExistParams; + result: IJsonbAllKeysExistResult; +} + +export type JsonbContainsParams = []; + +export interface IJsonbContainsResult { + hasSpecificUsername: object; + isActive: object; + jsonTestDataId: number; + jsonTestDataName: string; +} + +export interface IJsonbContainsQuery { + params: JsonbContainsParams; + result: IJsonbContainsResult; +} + +export type JsonbContainedByParams = []; + +export interface IJsonbContainedByResult { + jsonTestDataId: number; + jsonTestDataName: string; + subsetInData: object; + usernameInData: object; +} + +export interface IJsonbContainedByQuery { + params: JsonbContainedByParams; + result: IJsonbContainedByResult; +} + +export type JsonbSetParams = []; + +export interface IJsonbSetResult { + jsonTestDataId: number; + jsonTestDataName: string; + originalData: object; + updatedAge: any; + updatedCity: any; +} + +export interface IJsonbSetQuery { + params: JsonbSetParams; + result: IJsonbSetResult; +} + +export type JsonbInsertParams = []; + +export interface IJsonbInsertResult { + jsonTestDataId: number; + jsonTestDataName: string; + originalData: object; + withPhone: any; +} + +export interface IJsonbInsertQuery { + params: JsonbInsertParams; + result: IJsonbInsertResult; +} diff --git a/tests/demo_json/postgres/json_object_functions.snapshot.ts b/tests/demo_json/postgres/json_object_functions.snapshot.ts new file mode 100644 index 00000000..5679e9f9 --- /dev/null +++ b/tests/demo_json/postgres/json_object_functions.snapshot.ts @@ -0,0 +1,143 @@ +export type JsonbObjectKeysParams = []; + +export interface IJsonbObjectKeysResult { + jsonTestDataId: number; + jsonTestDataName: string; + objectKey: any; +} + +export interface IJsonbObjectKeysQuery { + params: JsonbObjectKeysParams; + result: IJsonbObjectKeysResult; +} + +export type JsonbTypeofParams = []; + +export interface IJsonbTypeofResult { + activeType: any; + ageType: any; + itemsType: any; + jsonTestDataId: number; + jsonTestDataName: string; + tagsType: any; + usernameType: any; +} + +export interface IJsonbTypeofQuery { + params: JsonbTypeofParams; + result: IJsonbTypeofResult; +} + +export type JsonbStripNullsParams = []; + +export interface IJsonbStripNullsResult { + jsonTestDataId: number; + jsonTestDataName: string; + reviewWithNulls: number; + reviewWithoutNulls: any; +} + +export interface IJsonbStripNullsQuery { + params: JsonbStripNullsParams; + result: IJsonbStripNullsResult; +} + +export type JsonbKeyExistsParams = []; + +export interface IJsonbKeyExistsResult { + hasAddress: string; + hasNonexistent: string; + hasUsername: string; + jsonTestDataId: number; + jsonTestDataName: string; +} + +export interface IJsonbKeyExistsQuery { + params: JsonbKeyExistsParams; + result: IJsonbKeyExistsResult; +} + +export type JsonbAnyKeyExistsParams = []; + +export interface IJsonbAnyKeyExistsResult { + hasAnyContact: any; + jsonTestDataId: number; + jsonTestDataName: string; +} + +export interface IJsonbAnyKeyExistsQuery { + params: JsonbAnyKeyExistsParams; + result: IJsonbAnyKeyExistsResult; +} + +export type JsonbAllKeysExistParams = []; + +export interface IJsonbAllKeysExistResult { + hasAllRequired: any; + hasAllWithPhone: any; + jsonTestDataId: number; + jsonTestDataName: string; +} + +export interface IJsonbAllKeysExistQuery { + params: JsonbAllKeysExistParams; + result: IJsonbAllKeysExistResult; +} + +export type JsonbContainsParams = []; + +export interface IJsonbContainsResult { + hasSpecificUsername: object; + isActive: object; + jsonTestDataId: number; + jsonTestDataName: string; +} + +export interface IJsonbContainsQuery { + params: JsonbContainsParams; + result: IJsonbContainsResult; +} + +export type JsonbContainedByParams = []; + +export interface IJsonbContainedByResult { + jsonTestDataId: number; + jsonTestDataName: string; + subsetInData: object; + usernameInData: object; +} + +export interface IJsonbContainedByQuery { + params: JsonbContainedByParams; + result: IJsonbContainedByResult; +} + +export type JsonbSetParams = []; + +export interface IJsonbSetResult { + jsonTestDataId: number; + jsonTestDataName: string; + originalData: object; + updatedAge: any; + updatedCity: any; +} + +export interface IJsonbSetQuery { + params: JsonbSetParams; + result: IJsonbSetResult; +} + +export type JsonbInsertParams = []; + +export interface IJsonbInsertResult { + jsonTestDataId: number; + jsonTestDataName: string; + originalData: object; + withPhone: any; +} + +export interface IJsonbInsertQuery { + params: JsonbInsertParams; + result: IJsonbInsertResult; +} + diff --git a/tests/demo_json/postgres/json_object_functions.ts b/tests/demo_json/postgres/json_object_functions.ts new file mode 100644 index 00000000..9a9d1fd2 --- /dev/null +++ b/tests/demo_json/postgres/json_object_functions.ts @@ -0,0 +1,126 @@ +import { sql } from 'sqlx-ts' + +// Test jsonb_object_keys - get all keys from object +const jsonbObjectKeys = sql` +-- @name: jsonb object keys +SELECT + json_test_data.id, + json_test_data.name, + jsonb_object_keys(json_test_data.data) AS object_key +FROM json_test_data +WHERE json_test_data.name = 'user_profile' +` + +// Test jsonb_typeof - get type of JSON value +const jsonbTypeof = sql` +-- @name: jsonb typeof +SELECT + json_test_data.id, + json_test_data.name, + jsonb_typeof(json_test_data.data -> 'username') AS username_type, + jsonb_typeof(json_test_data.data -> 'age') AS age_type, + jsonb_typeof(json_test_data.data -> 'active') AS active_type, + jsonb_typeof(json_test_data.data -> 'items') AS items_type, + jsonb_typeof(json_test_data.data -> 'tags') AS tags_type +FROM json_test_data +WHERE json_test_data.name IN ('user_profile', 'shopping_cart', 'tags') +` + +// Test jsonb_strip_nulls - remove null values +const jsonbStripNulls = sql` +-- @name: jsonb strip nulls +SELECT + json_test_data.id, + json_test_data.name, + json_test_data.data -> 'reviews' -> 1 AS review_with_nulls, + jsonb_strip_nulls(json_test_data.data -> 'reviews' -> 1) AS review_without_nulls +FROM json_test_data +WHERE json_test_data.name = 'product_reviews' +` + +// Test ? operator - key exists +const jsonbKeyExists = sql` +-- @name: jsonb key exists +SELECT + json_test_data.id, + json_test_data.name, + json_test_data.data ? 'username' AS has_username, + json_test_data.data ? 'address' AS has_address, + json_test_data.data ? 'nonexistent' AS has_nonexistent +FROM json_test_data +WHERE json_test_data.name IN ('user_profile', 'user_with_address') +` + +// Test ?| operator - any key exists +const jsonbAnyKeyExists = sql` +-- @name: jsonb any key exists +SELECT + json_test_data.id, + json_test_data.name, + json_test_data.data ?| array['username', 'email', 'phone'] AS has_any_contact +FROM json_test_data +WHERE json_test_data.name IN ('user_profile', 'user_with_address') +` + +// Test ?& operator - all keys exist +const jsonbAllKeysExist = sql` +-- @name: jsonb all keys exist +SELECT + json_test_data.id, + json_test_data.name, + json_test_data.data ?& array['username', 'email'] AS has_all_required, + json_test_data.data ?& array['username', 'email', 'phone'] AS has_all_with_phone +FROM json_test_data +WHERE json_test_data.name IN ('user_profile', 'user_with_address') +` + +// Test @> operator - contains (left contains right) +const jsonbContains = sql` +-- @name: jsonb contains +SELECT + json_test_data.id, + json_test_data.name, + json_test_data.data @> '{"username": "john_doe"}'::jsonb AS has_specific_username, + json_test_data.data @> '{"active": true}'::jsonb AS is_active +FROM json_test_data +WHERE json_test_data.name = 'user_profile' +` + +// Test <@ operator - is contained by (left is contained in right) +const jsonbContainedBy = sql` +-- @name: jsonb contained by +SELECT + json_test_data.id, + json_test_data.name, + '{"username": "john_doe"}'::jsonb <@ json_test_data.data AS username_in_data, + '{"username": "john_doe", "age": 30}'::jsonb <@ json_test_data.data AS subset_in_data +FROM json_test_data +WHERE json_test_data.name = 'user_profile' +` + +// Test jsonb_set - update value in JSON +const jsonbSet = sql` +-- @name: jsonb set +SELECT + json_test_data.id, + json_test_data.name, + json_test_data.data AS original_data, + jsonb_set(json_test_data.data, '{age}', '31') AS updated_age, + jsonb_set(json_test_data.data, '{address, city}', '"New York"') AS updated_city +FROM json_test_data +WHERE json_test_data.name IN ('user_profile', 'user_with_address') +LIMIT 2 +` + +// Test jsonb_insert - insert value into JSON +const jsonbInsert = sql` +-- @name: jsonb insert +SELECT + json_test_data.id, + json_test_data.name, + json_test_data.data AS original_data, + jsonb_insert(json_test_data.data, '{phone}', '"555-1234"') AS with_phone +FROM json_test_data +WHERE json_test_data.name = 'user_profile' +` + diff --git a/tests/demo_json/postgres/json_reserved_keywords.queries.ts b/tests/demo_json/postgres/json_reserved_keywords.queries.ts new file mode 100644 index 00000000..0751cc5c --- /dev/null +++ b/tests/demo_json/postgres/json_reserved_keywords.queries.ts @@ -0,0 +1,48 @@ +export type JsonbReservedKeywordsParams = []; + +export interface IJsonbReservedKeywordsResult { + jsonTestDataId: number; + jsonTestDataName: string; + reservedKeywordsObject: { "class": string; "interface": string; "type": string; "const": string; "let": string; "function": string; "return": boolean; "import": string; "export": string; "async": string }; +} + +export interface IJsonbReservedKeywordsQuery { + params: JsonbReservedKeywordsParams; + result: IJsonbReservedKeywordsResult; +} + +export type JsonbInvalidIdentifiersParams = []; + +export interface IJsonbInvalidIdentifiersResult { + invalidIdentifiersObject: { "field-name": string; "field name": string; "123field": string; "user@email": string; "field.nested": string }; + jsonTestDataId: number; + jsonTestDataName: string; +} + +export interface IJsonbInvalidIdentifiersQuery { + params: JsonbInvalidIdentifiersParams; + result: IJsonbInvalidIdentifiersResult; +} + +export type JsonbAggReservedKeywordsParams = []; + +export interface IJsonbAggReservedKeywordsResult { + aggregatedReservedKeywords: Array<{ "class": string; "interface": number; "default": boolean }>; +} + +export interface IJsonbAggReservedKeywordsQuery { + params: JsonbAggReservedKeywordsParams; + result: IJsonbAggReservedKeywordsResult; +} + +export type JsonbMixedIdentifiersParams = []; + +export interface IJsonbMixedIdentifiersResult { + jsonTestDataId: number; + mixedIdentifiersObject: { validName: string; "invalid-name": number; _underscore: string; $dollar: string; "class": string; "123start": string }; +} + +export interface IJsonbMixedIdentifiersQuery { + params: JsonbMixedIdentifiersParams; + result: IJsonbMixedIdentifiersResult; +} diff --git a/tests/demo_json/postgres/json_reserved_keywords.snapshot.ts b/tests/demo_json/postgres/json_reserved_keywords.snapshot.ts new file mode 100644 index 00000000..0ad81552 --- /dev/null +++ b/tests/demo_json/postgres/json_reserved_keywords.snapshot.ts @@ -0,0 +1,49 @@ +export type JsonbReservedKeywordsParams = []; + +export interface IJsonbReservedKeywordsResult { + jsonTestDataId: number; + jsonTestDataName: string; + reservedKeywordsObject: { "class": string; "interface": string; "type": string; "const": string; "let": string; "function": string; "return": boolean; "import": string; "export": string; "async": string }; +} + +export interface IJsonbReservedKeywordsQuery { + params: JsonbReservedKeywordsParams; + result: IJsonbReservedKeywordsResult; +} + +export type JsonbInvalidIdentifiersParams = []; + +export interface IJsonbInvalidIdentifiersResult { + invalidIdentifiersObject: { "field-name": string; "field name": string; "123field": string; "user@email": string; "field.nested": string }; + jsonTestDataId: number; + jsonTestDataName: string; +} + +export interface IJsonbInvalidIdentifiersQuery { + params: JsonbInvalidIdentifiersParams; + result: IJsonbInvalidIdentifiersResult; +} + +export type JsonbAggReservedKeywordsParams = []; + +export interface IJsonbAggReservedKeywordsResult { + aggregatedReservedKeywords: Array<{ "class": string; "interface": number; "default": boolean }>; +} + +export interface IJsonbAggReservedKeywordsQuery { + params: JsonbAggReservedKeywordsParams; + result: IJsonbAggReservedKeywordsResult; +} + +export type JsonbMixedIdentifiersParams = []; + +export interface IJsonbMixedIdentifiersResult { + jsonTestDataId: number; + mixedIdentifiersObject: { validName: string; "invalid-name": number; _underscore: string; $dollar: string; "class": string; "123start": string }; +} + +export interface IJsonbMixedIdentifiersQuery { + params: JsonbMixedIdentifiersParams; + result: IJsonbMixedIdentifiersResult; +} + diff --git a/tests/demo_json/postgres/json_reserved_keywords.ts b/tests/demo_json/postgres/json_reserved_keywords.ts new file mode 100644 index 00000000..4961e9de --- /dev/null +++ b/tests/demo_json/postgres/json_reserved_keywords.ts @@ -0,0 +1,75 @@ +import { sql } from 'sqlx-ts' + +// Test jsonb_build_object with TypeScript reserved keywords +const jsonbReservedKeywords = sql` +-- @name: jsonb reserved keywords +SELECT + json_test_data.id, + json_test_data.name, + jsonb_build_object( + 'class', 'User', + 'interface', 'IUser', + 'type', 'object', + 'const', 'constant', + 'let', 'variable', + 'function', 'method', + 'return', true, + 'import', 'module', + 'export', 'default', + 'async', 'promise' + ) AS reserved_keywords_object +FROM json_test_data +WHERE json_test_data.name = 'user_profile' +LIMIT 1 +` + +// Test jsonb_build_object with invalid TypeScript identifiers +const jsonbInvalidIdentifiers = sql` +-- @name: jsonb invalid identifiers +SELECT + json_test_data.id, + json_test_data.name, + jsonb_build_object( + 'field-name', 'hyphenated', + 'field name', 'with space', + '123field', 'starts with number', + 'user@email', 'special chars', + 'field.nested', 'dotted name' + ) AS invalid_identifiers_object +FROM json_test_data +WHERE json_test_data.name = 'user_profile' +LIMIT 1 +` + +// Test jsonb_agg with reserved keywords +const jsonbAggReservedKeywords = sql` +-- @name: jsonb agg reserved keywords +SELECT + jsonb_agg( + jsonb_build_object( + 'class', json_test_data.name, + 'interface', json_test_data.id, + 'default', true + ) + ) AS aggregated_reserved_keywords +FROM json_test_data +LIMIT 3 +` + +// Test mixed valid and invalid identifiers +const jsonbMixedIdentifiers = sql` +-- @name: jsonb mixed identifiers +SELECT + json_test_data.id, + jsonb_build_object( + 'validName', json_test_data.name, + 'invalid-name', json_test_data.id, + '_underscore', 'valid', + '$dollar', 'valid', + 'class', 'reserved', + '123start', 'invalid' + ) AS mixed_identifiers_object +FROM json_test_data +WHERE json_test_data.name = 'user_profile' +LIMIT 1 +` diff --git a/tests/demo/postgres/jsonb_operations.queries.ts b/tests/demo_json/postgres/jsonb_operations.queries.ts similarity index 86% rename from tests/demo/postgres/jsonb_operations.queries.ts rename to tests/demo_json/postgres/jsonb_operations.queries.ts index 42ab19cd..01832bba 100644 --- a/tests/demo/postgres/jsonb_operations.queries.ts +++ b/tests/demo_json/postgres/jsonb_operations.queries.ts @@ -2,7 +2,7 @@ export type JsonbBuildObjectBasicParams = []; export interface IJsonbBuildObjectBasicResult { id: number; - itemJson: any; + itemJson: { id: number; name: string; rarity: string | null }; } export interface IJsonbBuildObjectBasicQuery { @@ -13,7 +13,7 @@ export interface IJsonbBuildObjectBasicQuery { export type JsonbAggregationParams = []; export interface IJsonbAggregationResult { - items: any; + items: Array<{ id: number; name: string }>; rarity: string | null; } diff --git a/tests/demo/postgres/jsonb_operations.snapshot.ts b/tests/demo_json/postgres/jsonb_operations.snapshot.ts similarity index 86% rename from tests/demo/postgres/jsonb_operations.snapshot.ts rename to tests/demo_json/postgres/jsonb_operations.snapshot.ts index ac767890..6375de99 100644 --- a/tests/demo/postgres/jsonb_operations.snapshot.ts +++ b/tests/demo_json/postgres/jsonb_operations.snapshot.ts @@ -2,7 +2,7 @@ export type JsonbBuildObjectBasicParams = []; export interface IJsonbBuildObjectBasicResult { id: number; - itemJson: any; + itemJson: { id: number; name: string; rarity: string | null }; } export interface IJsonbBuildObjectBasicQuery { @@ -13,7 +13,7 @@ export interface IJsonbBuildObjectBasicQuery { export type JsonbAggregationParams = []; export interface IJsonbAggregationResult { - items: any; + items: Array<{ id: number; name: string }>; rarity: string | null; } diff --git a/tests/demo/postgres/jsonb_operations.ts b/tests/demo_json/postgres/jsonb_operations.ts similarity index 100% rename from tests/demo/postgres/jsonb_operations.ts rename to tests/demo_json/postgres/jsonb_operations.ts diff --git a/tests/demo/postgres/upsert.queries.ts b/tests/demo_json/postgres/upsert.queries.ts similarity index 100% rename from tests/demo/postgres/upsert.queries.ts rename to tests/demo_json/postgres/upsert.queries.ts diff --git a/tests/demo/postgres/upsert.snapshot.ts b/tests/demo_json/postgres/upsert.snapshot.ts similarity index 100% rename from tests/demo/postgres/upsert.snapshot.ts rename to tests/demo_json/postgres/upsert.snapshot.ts diff --git a/tests/demo/postgres/upsert.ts b/tests/demo_json/postgres/upsert.ts similarity index 100% rename from tests/demo/postgres/upsert.ts rename to tests/demo_json/postgres/upsert.ts