From b4dd57e25bee5996c01168fa31d348cedb1967ba Mon Sep 17 00:00:00 2001 From: mk3008 Date: Thu, 23 May 2024 21:49:49 +0900 Subject: [PATCH 1/6] Support for subqueries --- .../MethodCallExpressionExtension.cs | 2 +- src/Carbunql.TypeSafe/FluentSelectQuery.cs | 100 +++++++- src/Carbunql.TypeSafe/ITableRowDefinition.cs | 3 +- src/Carbunql.TypeSafe/Sql.cs | 48 +++- .../Annotations/ColumnDefinitionFactory.cs | 2 +- .../TableDefinitionClauseFactory.cs | 50 +--- src/Carbunql/Annotations/TableInfoFactory.cs | 31 +++ src/Carbunql/TableInfo.cs | 15 ++ test/Carbunql.TypeSafe.Test/CTETest.cs | 227 ++++++++++++++++++ test/Carbunql.TypeSafe.Test/JoinTest.cs | 8 +- .../Carbunql.TypeSafe.Test/SingleTableTest.cs | 4 +- test/Carbunql.TypeSafe.Test/WhereTest.cs | 2 +- 12 files changed, 428 insertions(+), 64 deletions(-) create mode 100644 src/Carbunql/Annotations/TableInfoFactory.cs create mode 100644 src/Carbunql/TableInfo.cs create mode 100644 test/Carbunql.TypeSafe.Test/CTETest.cs diff --git a/src/Carbunql.TypeSafe/Extensions/MethodCallExpressionExtension.cs b/src/Carbunql.TypeSafe/Extensions/MethodCallExpressionExtension.cs index a124ee77..f03a5edb 100644 --- a/src/Carbunql.TypeSafe/Extensions/MethodCallExpressionExtension.cs +++ b/src/Carbunql.TypeSafe/Extensions/MethodCallExpressionExtension.cs @@ -161,7 +161,7 @@ private static string CreateSqlCommand(this MethodCallExpression mce } throw new ArgumentException("Invalid arguments count for RowNumber."); - case nameof(Sql.RowNumberOrderbyBy): + case nameof(Sql.RowNumberOrderBy): if (mce.Arguments.First() is NewExpression argOrderbyBy) { var argOrderbyByText = string.Join(",", argOrderbyBy.Arguments.Select(x => mainConverter(x, addParameter))); diff --git a/src/Carbunql.TypeSafe/FluentSelectQuery.cs b/src/Carbunql.TypeSafe/FluentSelectQuery.cs index 9e90fa32..8f5bcbd6 100644 --- a/src/Carbunql.TypeSafe/FluentSelectQuery.cs +++ b/src/Carbunql.TypeSafe/FluentSelectQuery.cs @@ -1,4 +1,5 @@ using Carbunql.Analysis.Parser; +using Carbunql.Annotations; using Carbunql.Building; using Carbunql.TypeSafe.Extensions; using Carbunql.Values; @@ -49,7 +50,18 @@ public FluentSelectQuery InnerJoin(Expression> tableExpression, Expre var condition = ToValue(conditionExpression.Body, prmManager.AddParaemter); - this.FromClause!.InnerJoin(table.TableDefinition).As(tableAlias).On(_ => ValueParser.Parse(condition)); + if (table.CreateTableQuery.Query != null) + { + this.FromClause!.InnerJoin(table.CreateTableQuery.Query).As(tableAlias).On(_ => ValueParser.Parse(condition)); + } + else if (table.CreateTableQuery.DefinitionClause != null) + { + this.FromClause!.InnerJoin(table.CreateTableQuery.DefinitionClause).As(tableAlias).On(_ => ValueParser.Parse(condition)); + } + else + { + throw new NotSupportedException(); + } return this; } @@ -71,7 +83,18 @@ public FluentSelectQuery LeftJoin(Expression> tableExpression, Expres var condition = ToValue(conditionExpression.Body, prmManager.AddParaemter); - this.FromClause!.LeftJoin(table.TableDefinition).As(tableAlias).On(_ => ValueParser.Parse(condition)); + if (table.CreateTableQuery.Query != null) + { + this.FromClause!.LeftJoin(table.CreateTableQuery.Query).As(tableAlias).On(_ => ValueParser.Parse(condition)); + } + else if (table.CreateTableQuery.DefinitionClause != null) + { + this.FromClause!.LeftJoin(table.CreateTableQuery.DefinitionClause).As(tableAlias).On(_ => ValueParser.Parse(condition)); + } + else + { + throw new NotSupportedException(); + } return this; } @@ -93,7 +116,18 @@ public FluentSelectQuery RightJoin(Expression> tableExpression, Expre var condition = ToValue(conditionExpression.Body, prmManager.AddParaemter); - this.FromClause!.RightJoin(table.TableDefinition).As(tableAlias).On(_ => ValueParser.Parse(condition)); + if (table.CreateTableQuery.Query != null) + { + this.FromClause!.RightJoin(table.CreateTableQuery.Query).As(tableAlias).On(_ => ValueParser.Parse(condition)); + } + else if (table.CreateTableQuery.DefinitionClause != null) + { + this.FromClause!.RightJoin(table.CreateTableQuery.DefinitionClause).As(tableAlias).On(_ => ValueParser.Parse(condition)); + } + else + { + throw new NotSupportedException(); + } return this; } @@ -107,7 +141,18 @@ public FluentSelectQuery CrossJoin(Expression> tableExpression) where var compiledExpression = tableExpression.Compile(); var table = compiledExpression(); - this.FromClause!.CrossJoin(table.TableDefinition).As(tableAlias); + if (table.CreateTableQuery.Query != null) + { + this.FromClause!.CrossJoin(table.CreateTableQuery.Query).As(tableAlias); + } + else if (table.CreateTableQuery.DefinitionClause != null) + { + this.FromClause!.CrossJoin(table.CreateTableQuery.DefinitionClause).As(tableAlias); + } + else + { + throw new NotSupportedException(); + } return this; } @@ -189,4 +234,51 @@ private string RemoveRootBracketOrDefault(string value) } return value; } + + /// + /// Compiles a FluentSelectQuery for a specified table row definition type. + /// + /// The type of the table row definition. + /// A compiled FluentSelectQuery of type T. + /// + /// Thrown when the select clause does not include all required columns of the table row definition type. + /// + public FluentSelectQuery Compile() where T : ITableRowDefinition, new() + { + var q = new FluentSelectQuery(); + + // Copy clauses and parameters to the new query object + q.WithClause = WithClause; + q.SelectClause = SelectClause; + q.FromClause = FromClause; + q.WhereClause = WhereClause; + q.GroupClause = GroupClause; + q.HavingClause = HavingClause; + q.WindowClause = WindowClause; + q.OperatableQueries = OperatableQueries; + q.OrderClause = OrderClause; + q.LimitClause = LimitClause; + q.Parameters = Parameters; + + var clause = TableDefinitionClauseFactory.Create(); + + if (q.SelectClause != null) + { + // Check if all properties of T are specified in the select clause + var aliases = q.GetSelectableItems().Select(x => x.Alias).ToHashSet(); + var missingColumns = clause.ColumnNames.Where(item => !aliases.Contains(item)).ToList(); + + if (missingColumns.Any()) + { + // If there are missing columns, include all of them in the error message + throw new InvalidProgramException($"'{typeof(T).Name}' is not compatible. The following columns are missing: {string.Join(", ", missingColumns)}"); + } + } + + return q; + } +} + +public class FluentSelectQuery : FluentSelectQuery where T : ITableRowDefinition, new() +{ } diff --git a/src/Carbunql.TypeSafe/ITableRowDefinition.cs b/src/Carbunql.TypeSafe/ITableRowDefinition.cs index 462e0601..18165165 100644 --- a/src/Carbunql.TypeSafe/ITableRowDefinition.cs +++ b/src/Carbunql.TypeSafe/ITableRowDefinition.cs @@ -1,10 +1,9 @@ using Carbunql.Annotations; -using Carbunql.Clauses; namespace Carbunql.TypeSafe; public interface ITableRowDefinition { [IgnoreMapping] - TableDefinitionClause TableDefinition { get; set; } + CreateTableQuery CreateTableQuery { get; set; } } diff --git a/src/Carbunql.TypeSafe/Sql.cs b/src/Carbunql.TypeSafe/Sql.cs index ea3cc77a..deae7e53 100644 --- a/src/Carbunql.TypeSafe/Sql.cs +++ b/src/Carbunql.TypeSafe/Sql.cs @@ -13,10 +13,36 @@ namespace Carbunql.TypeSafe; /// (SelectQuery query) where T : ITableRowDefinition, new() + { + var instance = new T(); + + var info = TableInfoFactory.Create(typeof(T)); + + instance.CreateTableQuery = query.ToCreateTableQuery("_" + info.Table, isTemporary: true); + return instance; + } + + public static T DefineTable(FluentSelectQuery query) where T : ITableRowDefinition, new() + { + var instance = new T(); + + var info = TableInfoFactory.Create(typeof(T)); + + instance.CreateTableQuery = query.ToCreateTableQuery("_" + info.Table, isTemporary: true); + return instance; + } + + public static T DefineTable(Func> builder) where T : ITableRowDefinition, new() + { + return DefineTable(builder.Invoke()); + } + public static T DefineTable() where T : ITableRowDefinition, new() { var instance = new T(); - instance.TableDefinition = TableDefinitionClauseFactory.Create(); + var clause = TableDefinitionClauseFactory.Create(); + instance.CreateTableQuery = new CreateTableQuery(clause); return instance; } @@ -30,7 +56,14 @@ public static FluentSelectQuery From(Expression> expression) where T var compiledExpression = expression.Compile(); var result = compiledExpression(); - sq.From(result.TableDefinition).As(alias); + if (result.CreateTableQuery.Query != null) + { + sq.From(result.CreateTableQuery.Query).As(alias); + } + else if (result.CreateTableQuery.DefinitionClause != null) + { + sq.From(result.CreateTableQuery.DefinitionClause).As(alias); + } return sq; } @@ -64,5 +97,14 @@ public static string Raw(string command) public static string RowNumberPartitionBy(object partition) => string.Empty; - public static string RowNumberOrderbyBy(object order) => string.Empty; + public static string RowNumberOrderBy(object order) => string.Empty; +} + +public struct CTEDefinition +{ + public string Name { get; set; } + + public Type RowType { get; set; } + + public string Query { get; set; } } diff --git a/src/Carbunql/Annotations/ColumnDefinitionFactory.cs b/src/Carbunql/Annotations/ColumnDefinitionFactory.cs index 726d0784..b9681b01 100644 --- a/src/Carbunql/Annotations/ColumnDefinitionFactory.cs +++ b/src/Carbunql/Annotations/ColumnDefinitionFactory.cs @@ -65,7 +65,7 @@ public static List CreateFromRelationPropeties(ITable t) internal static List CreateAsParentProperties(ITable t, PropertyInfo prop) { var parentType = prop.PropertyType; - var parentclause = TableDefinitionClauseFactory.CreateTableDefinitionClause(parentType); + var parentclause = TableDefinitionClauseFactory.Create(parentType); var parentPkeyMaps = PrimaryKeyConstraintFactory.Create(parentType).PrimaryKeyMaps; if (!parentPkeyMaps.Any()) throw new InvalidProgramException(); diff --git a/src/Carbunql/Annotations/TableDefinitionClauseFactory.cs b/src/Carbunql/Annotations/TableDefinitionClauseFactory.cs index 1303fbfd..b2724d77 100644 --- a/src/Carbunql/Annotations/TableDefinitionClauseFactory.cs +++ b/src/Carbunql/Annotations/TableDefinitionClauseFactory.cs @@ -14,7 +14,7 @@ public static class TableDefinitionClauseFactory /// The table definition clause for the specified type. public static TableDefinitionClause Create() { - var clause = CreateTableDefinitionClause(typeof(T)); + var clause = Create(typeof(T)); ColumnDefinitionFactory.Create(clause).ToList().ForEach(x => clause.Add(x)); @@ -23,53 +23,11 @@ public static TableDefinitionClause Create() return clause; } - /// - /// Creates a table definition clause for the specified type and schema/table names. - /// - /// The type of class for which to create the table definition. - /// The schema name. - /// The table name. - /// The table definition clause for the specified type and names. - internal static TableDefinitionClause CreateTableDefinitionClause(Type type) - { - var atr = (TableAttribute?)Attribute.GetCustomAttribute(type, typeof(TableAttribute)); - - TableDefinitionClause clause; - if (atr != null) - { - clause = CreateTableDefinitionClause(type, atr.Schema, atr.Table); - } - else - { - clause = CreateTableDefinitionClause(type, string.Empty, string.Empty); - } - - return clause; - } - - /// - /// Creates a table definition clause with the specified type, schema, and table names. - /// - /// The type of class for which to create the table definition. - /// The schema name. - /// The table name. - /// The table definition clause with the specified type, schema, and table names. - private static TableDefinitionClause CreateTableDefinitionClause(Type type, string schema, string table) + internal static TableDefinitionClause Create(Type type) { - if (string.IsNullOrEmpty(schema)) - { - schema = DbmsConfiguration.ConvertToDefaultSchemaNameLogic(type); - } - if (string.IsNullOrEmpty(table)) - { - table = DbmsConfiguration.ConvertToDefaultTableNameLogic(type); - } - if (string.IsNullOrEmpty(table)) - { - table = DbmsConfiguration.ConvertToDefaultTableNameLogic(type); - } + var info = TableInfoFactory.Create(type); - var clause = new TableDefinitionClause(schema, table); + var clause = new TableDefinitionClause(info.Schema, info.Table); return clause; } diff --git a/src/Carbunql/Annotations/TableInfoFactory.cs b/src/Carbunql/Annotations/TableInfoFactory.cs new file mode 100644 index 00000000..d667bb84 --- /dev/null +++ b/src/Carbunql/Annotations/TableInfoFactory.cs @@ -0,0 +1,31 @@ +namespace Carbunql.Annotations; + +public static class TableInfoFactory +{ + public static TableInfo Create(Type type) + { + var atr = (TableAttribute?)Attribute.GetCustomAttribute(type, typeof(TableAttribute)); + + string schema; + string table; + + if (string.IsNullOrEmpty(atr?.Schema)) + { + schema = DbmsConfiguration.ConvertToDefaultSchemaNameLogic(type); + } + else + { + schema = atr.Schema; + } + if (string.IsNullOrEmpty(atr?.Table)) + { + table = DbmsConfiguration.ConvertToDefaultTableNameLogic(type); + } + else + { + table = atr.Table; + } + + return new TableInfo(table) { Schema = schema }; + } +} \ No newline at end of file diff --git a/src/Carbunql/TableInfo.cs b/src/Carbunql/TableInfo.cs new file mode 100644 index 00000000..97d6e8ee --- /dev/null +++ b/src/Carbunql/TableInfo.cs @@ -0,0 +1,15 @@ +using Carbunql.Definitions; + +namespace Carbunql; + +public class TableInfo : ITable +{ + public TableInfo(string tableName) + { + Table = tableName; + } + + public string Schema { get; set; } = string.Empty; + + public string Table { get; set; } +} diff --git a/test/Carbunql.TypeSafe.Test/CTETest.cs b/test/Carbunql.TypeSafe.Test/CTETest.cs new file mode 100644 index 00000000..c51565a8 --- /dev/null +++ b/test/Carbunql.TypeSafe.Test/CTETest.cs @@ -0,0 +1,227 @@ +using Carbunql.Clauses; +using System.Runtime.InteropServices; +using Xunit.Abstractions; + +namespace Carbunql.TypeSafe.Test; + +public class CTETest +{ + public CTETest(ITestOutputHelper output) + { + Output = output; + } + + private ITestOutputHelper Output { get; } + + private FluentSelectQuery SelectOrderById_NoType(int id) + { + var o = Sql.DefineTable(); + var query = Sql.From(() => o) + .Where(() => o.store_id == id); + return query; + } + + private FluentSelectQuery SelectOrderById(int id) + { + var o = Sql.DefineTable(); + var query = Sql.From(() => o) + .Where(() => o.store_id == id); + return query.Compile(); + } + + private FluentSelectQuery SelectOrder() + { + var o = Sql.DefineTable(); + var query = Sql.From(() => o); + return query.Compile(); + } + + [Fact] + public void SubQuery_NoType() + { + var o = Sql.DefineTable(SelectOrderById_NoType(1)); + + var query = Sql.From(() => o) + .Select(() => new { o.store_id }); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"/* + :id = 1 +*/ +SELECT + o.store_id +FROM + ( + SELECT + * + FROM + order AS o + WHERE + o.store_id = :id + ) AS o"; + + Assert.Equal(expect, actual, true, true, true); + } + + [Fact] + public void SubQuery() + { + var o = Sql.DefineTable(SelectOrderById(1)); + + var query = Sql.From(() => o) + .Select(() => new { o.store_id }); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"/* + :id = 1 +*/ +SELECT + o.store_id +FROM + ( + SELECT + * + FROM + order AS o + WHERE + o.store_id = :id + ) AS o"; + + Assert.Equal(expect, actual, true, true, true); + } + + [Fact] + public void SubQuery_Injection() + { + var o = Sql.DefineTable(() => + { + var x = Sql.DefineTable(SelectOrder()); + return Sql.From(() => x).Where(() => x.store_id == 1).Compile(); + }); + + var query = Sql.From(() => o) + .Select(() => new { o.store_id }); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"SELECT + o.store_id +FROM + ( + SELECT + * + FROM + ( + SELECT + * + FROM + order AS o + ) AS x + WHERE + x.store_id = 1 + ) AS o"; + + Assert.Equal(expect, actual, true, true, true); + } + + [Fact] + public void Compile_SelectAll() + { + var o = Sql.DefineTable(); + + var query = Sql.From(() => o).Compile(); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"SELECT + * +FROM + order AS o"; + + Assert.Equal(expect, actual, true, true, true); + } + + [Fact] + public void Compile_Excess() + { + var o = Sql.DefineTable(); + + var query = Sql.From(() => o).Select(() => new { o.order_id, o.store_id, o.order_date, o.customer_name, memo = "test" }).Compile(); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"/* + :p0 = 'test' +*/ +SELECT + o.order_id, + o.store_id, + o.order_date, + o.customer_name, + :p0 AS memo +FROM + order AS o"; + + Assert.Equal(expect, actual, true, true, true); + } + + [Fact] + public void Compile_Undersized() + { + var o = Sql.DefineTable(); + + var query = Sql.From(() => o).Select(() => new { o.order_id }); + + var ex = Assert.Throws(() => query.Compile()); + Output.WriteLine(ex.Message); + + Assert.Equal("'order' is not compatible. The following columns are missing: order_date, customer_name, store_id", ex.Message); + } + + public record product(int product_id, string name, decimal price) : ITableRowDefinition + { + // no arguments constructor. + // Since it is used as a definition, it has no particular meaning as a value. + public product() : this(0, "", 0) { } + + // interface property + CreateTableQuery ITableRowDefinition.CreateTableQuery { get; set; } = null!; + } + + public record store(int store_id, string name, string location) : ITableRowDefinition + { + // no arguments constructor. + // Since it is used as a definition, it has no particular meaning as a value. + public store() : this(0, "", "") { } + + // interface property + CreateTableQuery ITableRowDefinition.CreateTableQuery { get; set; } = null!; + } + + public record order(int order_id, DateTime order_date, string customer_name, int store_id, List order_details) : ITableRowDefinition + { + // no arguments constructor. + // Since it is used as a definition, it has no particular meaning as a value. + public order() : this(0, DateTime.Now, "", 0, new List()) { } + + // interface property + CreateTableQuery ITableRowDefinition.CreateTableQuery { get; set; } = null!; + } + + public record order_detail(int order_detail_id, int order_id, int product_id, int quantity, decimal price) : ITableRowDefinition + { + // no arguments constructor. + // Since it is used as a definition, it has no particular meaning as a value. + public order_detail() : this(0, 0, 0, 0, 0) { } + + // interface property + CreateTableQuery ITableRowDefinition.CreateTableQuery { get; set; } = null!; + } +} diff --git a/test/Carbunql.TypeSafe.Test/JoinTest.cs b/test/Carbunql.TypeSafe.Test/JoinTest.cs index 54d7dda6..cc7d5cd1 100644 --- a/test/Carbunql.TypeSafe.Test/JoinTest.cs +++ b/test/Carbunql.TypeSafe.Test/JoinTest.cs @@ -88,7 +88,7 @@ public record product(int product_id, string name, decimal price) : ITableRowDef public product() : this(0, "", 0) { } // interface property - TableDefinitionClause ITableRowDefinition.TableDefinition { get; set; } = null!; + CreateTableQuery ITableRowDefinition.CreateTableQuery { get; set; } = null!; } public record store(int store_id, string name, string location) : ITableRowDefinition @@ -98,7 +98,7 @@ public record store(int store_id, string name, string location) : ITableRowDefin public store() : this(0, "", "") { } // interface property - TableDefinitionClause ITableRowDefinition.TableDefinition { get; set; } = null!; + CreateTableQuery ITableRowDefinition.CreateTableQuery { get; set; } = null!; } public record order(int order_id, DateTime order_date, string customer_name, int store_id, List order_details) : ITableRowDefinition @@ -108,7 +108,7 @@ public record order(int order_id, DateTime order_date, string customer_name, int public order() : this(0, DateTime.Now, "", 0, new List()) { } // interface property - TableDefinitionClause ITableRowDefinition.TableDefinition { get; set; } = null!; + CreateTableQuery ITableRowDefinition.CreateTableQuery { get; set; } = null!; } public record order_detail(int order_detail_id, int order_id, int product_id, int quantity, decimal price) : ITableRowDefinition @@ -118,6 +118,6 @@ public record order_detail(int order_detail_id, int order_id, int product_id, in public order_detail() : this(0, 0, 0, 0, 0) { } // interface property - TableDefinitionClause ITableRowDefinition.TableDefinition { get; set; } = null!; + CreateTableQuery ITableRowDefinition.CreateTableQuery { get; set; } = null!; } } diff --git a/test/Carbunql.TypeSafe.Test/SingleTableTest.cs b/test/Carbunql.TypeSafe.Test/SingleTableTest.cs index 50598a2d..9683c44e 100644 --- a/test/Carbunql.TypeSafe.Test/SingleTableTest.cs +++ b/test/Carbunql.TypeSafe.Test/SingleTableTest.cs @@ -399,7 +399,7 @@ public void ReservedCommand() row_num = Sql.RowNumber(), row_num_partiton_order = Sql.RowNumber(new { a.product_name, a.unit_price }, new { a.quantity, a.sale_id }), row_num_partition = Sql.RowNumberPartitionBy(new { a.product_name, a.unit_price }), - row_num_order = Sql.RowNumberOrderbyBy(new { a.product_name, a.unit_price }) + row_num_order = Sql.RowNumberOrderBy(new { a.product_name, a.unit_price }) }); var actual = query.ToText(); @@ -702,6 +702,6 @@ DateTime created_at public sale() : this(0, "", 0, 0, DateTime.Now) { } // interface property - TableDefinitionClause ITableRowDefinition.TableDefinition { get; set; } = null!; + CreateTableQuery ITableRowDefinition.CreateTableQuery { get; set; } = null!; } } \ No newline at end of file diff --git a/test/Carbunql.TypeSafe.Test/WhereTest.cs b/test/Carbunql.TypeSafe.Test/WhereTest.cs index 69ac87cf..a8e01148 100644 --- a/test/Carbunql.TypeSafe.Test/WhereTest.cs +++ b/test/Carbunql.TypeSafe.Test/WhereTest.cs @@ -265,6 +265,6 @@ decimal unit_price public sale() : this(0, "", 0, 0) { } // interface property - TableDefinitionClause ITableRowDefinition.TableDefinition { get; set; } = null!; + CreateTableQuery ITableRowDefinition.CreateTableQuery { get; set; } = null!; } } \ No newline at end of file From 6461692bbbf1e4c11616f5b78aa1c8af5f5d61ad Mon Sep 17 00:00:00 2001 From: mk3008 Date: Thu, 23 May 2024 22:38:05 +0900 Subject: [PATCH 2/6] Subquery feature enhancements Type checking feature added. --- src/Carbunql.TypeSafe/FluentSelectQuery.cs | 30 ++++++++++++++++++- .../{CTETest.cs => SubQueryTest.cs} | 17 +++++++++-- 2 files changed, 44 insertions(+), 3 deletions(-) rename test/Carbunql.TypeSafe.Test/{CTETest.cs => SubQueryTest.cs} (92%) diff --git a/src/Carbunql.TypeSafe/FluentSelectQuery.cs b/src/Carbunql.TypeSafe/FluentSelectQuery.cs index 8f5bcbd6..36db4899 100644 --- a/src/Carbunql.TypeSafe/FluentSelectQuery.cs +++ b/src/Carbunql.TypeSafe/FluentSelectQuery.cs @@ -1,6 +1,9 @@ using Carbunql.Analysis.Parser; using Carbunql.Annotations; using Carbunql.Building; +using Carbunql.Clauses; +using Carbunql.Definitions; +using Carbunql.Tables; using Carbunql.TypeSafe.Extensions; using Carbunql.Values; using System.Linq.Expressions; @@ -262,6 +265,13 @@ private string RemoveRootBracketOrDefault(string value) var clause = TableDefinitionClauseFactory.Create(); + TypeValidate(q, clause); + + return q; + } + + private static void TypeValidate(SelectQuery q, TableDefinitionClause clause) + { if (q.SelectClause != null) { // Check if all properties of T are specified in the select clause @@ -273,9 +283,27 @@ private string RemoveRootBracketOrDefault(string value) // If there are missing columns, include all of them in the error message throw new InvalidProgramException($"'{typeof(T).Name}' is not compatible. The following columns are missing: {string.Join(", ", missingColumns)}"); } + return; } + else if (q.FromClause != null) + { + var actual = q.FromClause.Root.Table.GetTableFullName(); + var expect = clause.GetTableFullName(); - return q; + if (q.FromClause.Root.Table is VirtualTable v && v.Query is SelectQuery vq) + { + TypeValidate(vq, clause); + } + else if (!actual.Equals(expect)) + { + throw new InvalidProgramException($"'{typeof(T).Name}' is not compatible. Expect: {expect}, Actual: {actual}"); + } + return; + } + else + { + throw new InvalidProgramException($"'{typeof(T).Name}' is not compatible. FromClause is null."); + } } } diff --git a/test/Carbunql.TypeSafe.Test/CTETest.cs b/test/Carbunql.TypeSafe.Test/SubQueryTest.cs similarity index 92% rename from test/Carbunql.TypeSafe.Test/CTETest.cs rename to test/Carbunql.TypeSafe.Test/SubQueryTest.cs index c51565a8..7f2a321e 100644 --- a/test/Carbunql.TypeSafe.Test/CTETest.cs +++ b/test/Carbunql.TypeSafe.Test/SubQueryTest.cs @@ -4,9 +4,9 @@ namespace Carbunql.TypeSafe.Test; -public class CTETest +public class SubQueryTest { - public CTETest(ITestOutputHelper output) + public SubQueryTest(ITestOutputHelper output) { Output = output; } @@ -147,6 +147,19 @@ public void Compile_SelectAll() Assert.Equal(expect, actual, true, true, true); } + [Fact] + public void Compile_SelectAll_Exception() + { + var o = Sql.DefineTable(); + + var query = Sql.From(() => o); + + var ex = Assert.Throws(() => query.Compile()); + Output.WriteLine(ex.Message); + + Assert.Equal("'order_detail' is not compatible. Expect: order_detail, Actual: order", ex.Message); + } + [Fact] public void Compile_Excess() { From 776d28c3455ab054e33999bd5b5003644e435910 Mon Sep 17 00:00:00 2001 From: mk3008 Date: Fri, 24 May 2024 07:42:17 +0900 Subject: [PATCH 3/6] Improved the Compile method. Error messages are now easier to understand. Added an auto-correction option. --- src/Carbunql.TypeSafe/FluentSelectQuery.cs | 35 ++++++++++++++++++--- test/Carbunql.TypeSafe.Test/SubQueryTest.cs | 25 +++++++++++++-- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/src/Carbunql.TypeSafe/FluentSelectQuery.cs b/src/Carbunql.TypeSafe/FluentSelectQuery.cs index 36db4899..cecefa19 100644 --- a/src/Carbunql.TypeSafe/FluentSelectQuery.cs +++ b/src/Carbunql.TypeSafe/FluentSelectQuery.cs @@ -246,7 +246,7 @@ private string RemoveRootBracketOrDefault(string value) /// /// Thrown when the select clause does not include all required columns of the table row definition type. /// - public FluentSelectQuery Compile() where T : ITableRowDefinition, new() + public FluentSelectQuery Compile(bool force = false) where T : ITableRowDefinition, new() { var q = new FluentSelectQuery(); @@ -265,11 +265,38 @@ private string RemoveRootBracketOrDefault(string value) var clause = TableDefinitionClauseFactory.Create(); + if (force) + { + CorrectSelectClause(q, clause); + } + TypeValidate(q, clause); return q; } + private static void CorrectSelectClause(SelectQuery q, TableDefinitionClause clause) + { + if (q.SelectClause != null) + { + // Check if all properties of T are specified in the select clause + var aliases = q.GetSelectableItems().Select(x => x.Alias).ToHashSet(); + var missingColumns = clause.OfType().Where(x => !aliases.Contains(x.ColumnName)); + + // 不足する列を自動的に足す + foreach (var item in missingColumns) + { + q.Select($"cast(null as {item.ColumnType.ToText()})").As(item.ColumnName); + } + return; + } + else + { + //補正をせず終了 + return; + } + } + private static void TypeValidate(SelectQuery q, TableDefinitionClause clause) { if (q.SelectClause != null) @@ -281,7 +308,7 @@ private static void TypeValidate(SelectQuery q, TableDefinitionClause clause) if (missingColumns.Any()) { // If there are missing columns, include all of them in the error message - throw new InvalidProgramException($"'{typeof(T).Name}' is not compatible. The following columns are missing: {string.Join(", ", missingColumns)}"); + throw new InvalidProgramException($"The select query is not compatible with '{typeof(T).Name}'. The following columns are missing: {string.Join(", ", missingColumns)}"); } return; } @@ -296,13 +323,13 @@ private static void TypeValidate(SelectQuery q, TableDefinitionClause clause) } else if (!actual.Equals(expect)) { - throw new InvalidProgramException($"'{typeof(T).Name}' is not compatible. Expect: {expect}, Actual: {actual}"); + throw new InvalidProgramException($"The select query is not compatible with '{typeof(T).Name}'. Expect: {expect}, Actual: {actual}"); } return; } else { - throw new InvalidProgramException($"'{typeof(T).Name}' is not compatible. FromClause is null."); + throw new InvalidProgramException($"The select query is not compatible with '{typeof(T).Name}'. FromClause is null."); } } } diff --git a/test/Carbunql.TypeSafe.Test/SubQueryTest.cs b/test/Carbunql.TypeSafe.Test/SubQueryTest.cs index 7f2a321e..95604fe1 100644 --- a/test/Carbunql.TypeSafe.Test/SubQueryTest.cs +++ b/test/Carbunql.TypeSafe.Test/SubQueryTest.cs @@ -157,7 +157,7 @@ public void Compile_SelectAll_Exception() var ex = Assert.Throws(() => query.Compile()); Output.WriteLine(ex.Message); - Assert.Equal("'order_detail' is not compatible. Expect: order_detail, Actual: order", ex.Message); + Assert.Equal("The select query is not compatible with 'order_detail'. Expect: order_detail, Actual: order", ex.Message); } [Fact] @@ -195,7 +195,28 @@ public void Compile_Undersized() var ex = Assert.Throws(() => query.Compile()); Output.WriteLine(ex.Message); - Assert.Equal("'order' is not compatible. The following columns are missing: order_date, customer_name, store_id", ex.Message); + Assert.Equal("The select query is not compatible with 'order'. The following columns are missing: order_date, customer_name, store_id", ex.Message); + } + + [Fact] + public void Compile_ForceCorrect() + { + var o = Sql.DefineTable(); + + var query = Sql.From(() => o).Select(() => new { o.order_id }).Compile(true); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"SELECT + o.order_id, + CAST(null AS timestamp) AS order_date, + CAST(null AS text) AS customer_name, + CAST(null AS integer) AS store_id +FROM + order AS o"; + + Assert.Equal(expect, actual, true, true, true); } public record product(int product_id, string name, decimal price) : ITableRowDefinition From 5455d50aa84666eea27361522e67bb607a65815e Mon Sep 17 00:00:00 2001 From: mk3008 Date: Fri, 24 May 2024 07:44:40 +0900 Subject: [PATCH 4/6] Organizing test classes Moved the compilation tests to a separate class. --- test/Carbunql.TypeSafe.Test/CompileTest.cs | 143 ++++++++++++++++++++ test/Carbunql.TypeSafe.Test/SubQueryTest.cs | 90 ------------ 2 files changed, 143 insertions(+), 90 deletions(-) create mode 100644 test/Carbunql.TypeSafe.Test/CompileTest.cs diff --git a/test/Carbunql.TypeSafe.Test/CompileTest.cs b/test/Carbunql.TypeSafe.Test/CompileTest.cs new file mode 100644 index 00000000..cfff1d50 --- /dev/null +++ b/test/Carbunql.TypeSafe.Test/CompileTest.cs @@ -0,0 +1,143 @@ +using Xunit.Abstractions; + +namespace Carbunql.TypeSafe.Test; + +public class CompileTest +{ + public CompileTest(ITestOutputHelper output) + { + Output = output; + } + + private ITestOutputHelper Output { get; } + + [Fact] + public void Compile_SelectAll() + { + var o = Sql.DefineTable(); + + var query = Sql.From(() => o).Compile(); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"SELECT + * +FROM + order AS o"; + + Assert.Equal(expect, actual, true, true, true); + } + + [Fact] + public void Compile_SelectAll_Exception() + { + var o = Sql.DefineTable(); + + var query = Sql.From(() => o); + + var ex = Assert.Throws(() => query.Compile()); + Output.WriteLine(ex.Message); + + Assert.Equal("The select query is not compatible with 'order_detail'. Expect: order_detail, Actual: order", ex.Message); + } + + [Fact] + public void Compile_Excess() + { + var o = Sql.DefineTable(); + + var query = Sql.From(() => o).Select(() => new { o.order_id, o.store_id, o.order_date, o.customer_name, memo = "test" }).Compile(); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"/* + :p0 = 'test' +*/ +SELECT + o.order_id, + o.store_id, + o.order_date, + o.customer_name, + :p0 AS memo +FROM + order AS o"; + + Assert.Equal(expect, actual, true, true, true); + } + + [Fact] + public void Compile_Undersized() + { + var o = Sql.DefineTable(); + + var query = Sql.From(() => o).Select(() => new { o.order_id }); + + var ex = Assert.Throws(() => query.Compile()); + Output.WriteLine(ex.Message); + + Assert.Equal("The select query is not compatible with 'order'. The following columns are missing: order_date, customer_name, store_id", ex.Message); + } + + [Fact] + public void Compile_ForceCorrect() + { + var o = Sql.DefineTable(); + + var query = Sql.From(() => o).Select(() => new { o.order_id }).Compile(true); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"SELECT + o.order_id, + CAST(null AS timestamp) AS order_date, + CAST(null AS text) AS customer_name, + CAST(null AS integer) AS store_id +FROM + order AS o"; + + Assert.Equal(expect, actual, true, true, true); + } + + public record product(int product_id, string name, decimal price) : ITableRowDefinition + { + // no arguments constructor. + // Since it is used as a definition, it has no particular meaning as a value. + public product() : this(0, "", 0) { } + + // interface property + CreateTableQuery ITableRowDefinition.CreateTableQuery { get; set; } = null!; + } + + public record store(int store_id, string name, string location) : ITableRowDefinition + { + // no arguments constructor. + // Since it is used as a definition, it has no particular meaning as a value. + public store() : this(0, "", "") { } + + // interface property + CreateTableQuery ITableRowDefinition.CreateTableQuery { get; set; } = null!; + } + + public record order(int order_id, DateTime order_date, string customer_name, int store_id, List order_details) : ITableRowDefinition + { + // no arguments constructor. + // Since it is used as a definition, it has no particular meaning as a value. + public order() : this(0, DateTime.Now, "", 0, new List()) { } + + // interface property + CreateTableQuery ITableRowDefinition.CreateTableQuery { get; set; } = null!; + } + + public record order_detail(int order_detail_id, int order_id, int product_id, int quantity, decimal price) : ITableRowDefinition + { + // no arguments constructor. + // Since it is used as a definition, it has no particular meaning as a value. + public order_detail() : this(0, 0, 0, 0, 0) { } + + // interface property + CreateTableQuery ITableRowDefinition.CreateTableQuery { get; set; } = null!; + } +} diff --git a/test/Carbunql.TypeSafe.Test/SubQueryTest.cs b/test/Carbunql.TypeSafe.Test/SubQueryTest.cs index 95604fe1..719d26ef 100644 --- a/test/Carbunql.TypeSafe.Test/SubQueryTest.cs +++ b/test/Carbunql.TypeSafe.Test/SubQueryTest.cs @@ -129,96 +129,6 @@ order AS o Assert.Equal(expect, actual, true, true, true); } - [Fact] - public void Compile_SelectAll() - { - var o = Sql.DefineTable(); - - var query = Sql.From(() => o).Compile(); - - var actual = query.ToText(); - Output.WriteLine(actual); - - var expect = @"SELECT - * -FROM - order AS o"; - - Assert.Equal(expect, actual, true, true, true); - } - - [Fact] - public void Compile_SelectAll_Exception() - { - var o = Sql.DefineTable(); - - var query = Sql.From(() => o); - - var ex = Assert.Throws(() => query.Compile()); - Output.WriteLine(ex.Message); - - Assert.Equal("The select query is not compatible with 'order_detail'. Expect: order_detail, Actual: order", ex.Message); - } - - [Fact] - public void Compile_Excess() - { - var o = Sql.DefineTable(); - - var query = Sql.From(() => o).Select(() => new { o.order_id, o.store_id, o.order_date, o.customer_name, memo = "test" }).Compile(); - - var actual = query.ToText(); - Output.WriteLine(actual); - - var expect = @"/* - :p0 = 'test' -*/ -SELECT - o.order_id, - o.store_id, - o.order_date, - o.customer_name, - :p0 AS memo -FROM - order AS o"; - - Assert.Equal(expect, actual, true, true, true); - } - - [Fact] - public void Compile_Undersized() - { - var o = Sql.DefineTable(); - - var query = Sql.From(() => o).Select(() => new { o.order_id }); - - var ex = Assert.Throws(() => query.Compile()); - Output.WriteLine(ex.Message); - - Assert.Equal("The select query is not compatible with 'order'. The following columns are missing: order_date, customer_name, store_id", ex.Message); - } - - [Fact] - public void Compile_ForceCorrect() - { - var o = Sql.DefineTable(); - - var query = Sql.From(() => o).Select(() => new { o.order_id }).Compile(true); - - var actual = query.ToText(); - Output.WriteLine(actual); - - var expect = @"SELECT - o.order_id, - CAST(null AS timestamp) AS order_date, - CAST(null AS text) AS customer_name, - CAST(null AS integer) AS store_id -FROM - order AS o"; - - Assert.Equal(expect, actual, true, true, true); - } - public record product(int product_id, string name, decimal price) : ITableRowDefinition { // no arguments constructor. From e9418297961506ff67b703583ac2dbfb069e6844 Mon Sep 17 00:00:00 2001 From: mk3008 Date: Sun, 26 May 2024 22:04:34 +0900 Subject: [PATCH 5/6] Supports CTE --- src/Carbunql.TypeSafe/CTEDatasoure.cs | 37 +++++ src/Carbunql.TypeSafe/CTEDefinition.cs | 10 ++ src/Carbunql.TypeSafe/FluentSelectQuery.cs | 86 +++------- src/Carbunql.TypeSafe/IDatasource.cs | 10 ++ src/Carbunql.TypeSafe/ITableRowDefinition.cs | 2 +- .../PhysicalTableDatasource.cs | 30 ++++ src/Carbunql.TypeSafe/QueryDatasource.cs | 27 +++ src/Carbunql.TypeSafe/Sql.cs | 90 ++++++---- src/Carbunql/Tables/PhysicalTable.cs | 7 + src/Carbunql/Values/Interval.cs | 1 - src/Carbunql/Values/OperatableValue.cs | 2 +- test/Carbunql.TypeSafe.Test/CTETest.cs | 156 ++++++++++++++++++ test/Carbunql.TypeSafe.Test/CompileTest.cs | 8 +- test/Carbunql.TypeSafe.Test/JoinTest.cs | 11 +- .../Carbunql.TypeSafe.Test/SingleTableTest.cs | 3 +- test/Carbunql.TypeSafe.Test/SubQueryTest.cs | 20 +-- test/Carbunql.TypeSafe.Test/WhereTest.cs | 5 +- 17 files changed, 378 insertions(+), 127 deletions(-) create mode 100644 src/Carbunql.TypeSafe/CTEDatasoure.cs create mode 100644 src/Carbunql.TypeSafe/CTEDefinition.cs create mode 100644 src/Carbunql.TypeSafe/IDatasource.cs create mode 100644 src/Carbunql.TypeSafe/PhysicalTableDatasource.cs create mode 100644 src/Carbunql.TypeSafe/QueryDatasource.cs create mode 100644 test/Carbunql.TypeSafe.Test/CTETest.cs diff --git a/src/Carbunql.TypeSafe/CTEDatasoure.cs b/src/Carbunql.TypeSafe/CTEDatasoure.cs new file mode 100644 index 00000000..a24213fd --- /dev/null +++ b/src/Carbunql.TypeSafe/CTEDatasoure.cs @@ -0,0 +1,37 @@ +using Carbunql.Analysis.Parser; +using Carbunql.Building; + +namespace Carbunql.TypeSafe; + +public class CTEDatasoure(string name, SelectQuery query) : IDatasource +{ + public string Name { get; set; } = name; + + public Materialized Materialized { get; set; } = Materialized.Undefined; + + public SelectQuery Query { get; init; } = query; + + public bool IsCTE => true; + + public SelectQuery BuildFromClause(SelectQuery query, string alias) + { + var cte = query.With(Query).As(Name); + cte.Materialized = Materialized; + + query.From(cte).As(alias); + return query; + } + + public SelectQuery BuildJoinClause(SelectQuery query, string join, string alias, string condition) + { + var cte = query.With(Query).As(Name); + cte.Materialized = Materialized; + + var r = query.FromClause!.Join(cte, join).As(alias); + if (!string.IsNullOrEmpty(condition)) + { + r.On(_ => ValueParser.Parse(condition)); + } + return query; + } +} diff --git a/src/Carbunql.TypeSafe/CTEDefinition.cs b/src/Carbunql.TypeSafe/CTEDefinition.cs new file mode 100644 index 00000000..5c3f0db1 --- /dev/null +++ b/src/Carbunql.TypeSafe/CTEDefinition.cs @@ -0,0 +1,10 @@ +namespace Carbunql.TypeSafe; + +public struct CTEDefinition +{ + public string Name { get; set; } + + public Type RowType { get; set; } + + public string Query { get; set; } +} diff --git a/src/Carbunql.TypeSafe/FluentSelectQuery.cs b/src/Carbunql.TypeSafe/FluentSelectQuery.cs index cecefa19..16d9a77f 100644 --- a/src/Carbunql.TypeSafe/FluentSelectQuery.cs +++ b/src/Carbunql.TypeSafe/FluentSelectQuery.cs @@ -48,23 +48,10 @@ public FluentSelectQuery InnerJoin(Expression> tableExpression, Expre var compiledExpression = tableExpression.Compile(); var table = compiledExpression(); - var prmManager = new ParameterManager(GetParameters(), AddParameter); - var condition = ToValue(conditionExpression.Body, prmManager.AddParaemter); - if (table.CreateTableQuery.Query != null) - { - this.FromClause!.InnerJoin(table.CreateTableQuery.Query).As(tableAlias).On(_ => ValueParser.Parse(condition)); - } - else if (table.CreateTableQuery.DefinitionClause != null) - { - this.FromClause!.InnerJoin(table.CreateTableQuery.DefinitionClause).As(tableAlias).On(_ => ValueParser.Parse(condition)); - } - else - { - throw new NotSupportedException(); - } + table.Datasource.BuildJoinClause(this, "inner join", tableAlias, condition); return this; } @@ -81,23 +68,10 @@ public FluentSelectQuery LeftJoin(Expression> tableExpression, Expres var compiledExpression = tableExpression.Compile(); var table = compiledExpression(); - var prmManager = new ParameterManager(GetParameters(), AddParameter); - var condition = ToValue(conditionExpression.Body, prmManager.AddParaemter); - if (table.CreateTableQuery.Query != null) - { - this.FromClause!.LeftJoin(table.CreateTableQuery.Query).As(tableAlias).On(_ => ValueParser.Parse(condition)); - } - else if (table.CreateTableQuery.DefinitionClause != null) - { - this.FromClause!.LeftJoin(table.CreateTableQuery.DefinitionClause).As(tableAlias).On(_ => ValueParser.Parse(condition)); - } - else - { - throw new NotSupportedException(); - } + table.Datasource.BuildJoinClause(this, "left join", tableAlias, condition); return this; } @@ -114,23 +88,10 @@ public FluentSelectQuery RightJoin(Expression> tableExpression, Expre var compiledExpression = tableExpression.Compile(); var table = compiledExpression(); - var prmManager = new ParameterManager(GetParameters(), AddParameter); - var condition = ToValue(conditionExpression.Body, prmManager.AddParaemter); - if (table.CreateTableQuery.Query != null) - { - this.FromClause!.RightJoin(table.CreateTableQuery.Query).As(tableAlias).On(_ => ValueParser.Parse(condition)); - } - else if (table.CreateTableQuery.DefinitionClause != null) - { - this.FromClause!.RightJoin(table.CreateTableQuery.DefinitionClause).As(tableAlias).On(_ => ValueParser.Parse(condition)); - } - else - { - throw new NotSupportedException(); - } + table.Datasource.BuildJoinClause(this, "right join", tableAlias, condition); return this; } @@ -144,18 +105,7 @@ public FluentSelectQuery CrossJoin(Expression> tableExpression) where var compiledExpression = tableExpression.Compile(); var table = compiledExpression(); - if (table.CreateTableQuery.Query != null) - { - this.FromClause!.CrossJoin(table.CreateTableQuery.Query).As(tableAlias); - } - else if (table.CreateTableQuery.DefinitionClause != null) - { - this.FromClause!.CrossJoin(table.CreateTableQuery.DefinitionClause).As(tableAlias); - } - else - { - throw new NotSupportedException(); - } + table.Datasource.BuildJoinClause(this, "cross join", tableAlias); return this; } @@ -277,24 +227,22 @@ private string RemoveRootBracketOrDefault(string value) private static void CorrectSelectClause(SelectQuery q, TableDefinitionClause clause) { - if (q.SelectClause != null) + if (q.SelectClause == null) { - // Check if all properties of T are specified in the select clause - var aliases = q.GetSelectableItems().Select(x => x.Alias).ToHashSet(); - var missingColumns = clause.OfType().Where(x => !aliases.Contains(x.ColumnName)); - - // 不足する列を自動的に足す - foreach (var item in missingColumns) - { - q.Select($"cast(null as {item.ColumnType.ToText()})").As(item.ColumnName); - } + // End without making corrections return; } - else + + // Check if all properties of T are specified in the select clause + var aliases = q.GetSelectableItems().Select(x => x.Alias).ToHashSet(); + var missingColumns = clause.OfType().Where(x => !aliases.Contains(x.ColumnName)); + + // Automatically add missing columns + foreach (var item in missingColumns) { - //補正をせず終了 - return; + q.Select($"cast(null as {item.ColumnType.ToText()})").As(item.ColumnName); } + return; } private static void TypeValidate(SelectQuery q, TableDefinitionClause clause) @@ -336,4 +284,8 @@ private static void TypeValidate(SelectQuery q, TableDefinitionClause clause) public class FluentSelectQuery : FluentSelectQuery where T : ITableRowDefinition, new() { + public T ToTable() + { + throw new InvalidOperationException(); + } } diff --git a/src/Carbunql.TypeSafe/IDatasource.cs b/src/Carbunql.TypeSafe/IDatasource.cs new file mode 100644 index 00000000..e49aca40 --- /dev/null +++ b/src/Carbunql.TypeSafe/IDatasource.cs @@ -0,0 +1,10 @@ +namespace Carbunql.TypeSafe; + +public interface IDatasource +{ + bool IsCTE { get; } + + SelectQuery BuildFromClause(SelectQuery query, string alias); + + SelectQuery BuildJoinClause(SelectQuery query, string join, string alias, string condition = ""); +} diff --git a/src/Carbunql.TypeSafe/ITableRowDefinition.cs b/src/Carbunql.TypeSafe/ITableRowDefinition.cs index 18165165..be2aa017 100644 --- a/src/Carbunql.TypeSafe/ITableRowDefinition.cs +++ b/src/Carbunql.TypeSafe/ITableRowDefinition.cs @@ -5,5 +5,5 @@ namespace Carbunql.TypeSafe; public interface ITableRowDefinition { [IgnoreMapping] - CreateTableQuery CreateTableQuery { get; set; } + IDatasource Datasource { get; set; } } diff --git a/src/Carbunql.TypeSafe/PhysicalTableDatasource.cs b/src/Carbunql.TypeSafe/PhysicalTableDatasource.cs new file mode 100644 index 00000000..5eadfbe4 --- /dev/null +++ b/src/Carbunql.TypeSafe/PhysicalTableDatasource.cs @@ -0,0 +1,30 @@ +using Carbunql.Analysis.Parser; +using Carbunql.Building; +using Carbunql.Clauses; +using Carbunql.Definitions; +using Carbunql.Tables; + +namespace Carbunql.TypeSafe; + +public class PhysicalTableDatasource(ITable tb) : IDatasource +{ + public SelectableTable Table { get; set; } = new PhysicalTable(tb).ToSelectable(); + + public bool IsCTE => false; + + public SelectQuery BuildFromClause(SelectQuery query, string alias) + { + query.From(Table).As(alias); + return query; + } + + public SelectQuery BuildJoinClause(SelectQuery query, string join, string alias, string condition) + { + var r = query.FromClause!.Join(Table, join).As(alias); + if (!string.IsNullOrEmpty(condition)) + { + r.On(_ => ValueParser.Parse(condition)); + } + return query; + } +} diff --git a/src/Carbunql.TypeSafe/QueryDatasource.cs b/src/Carbunql.TypeSafe/QueryDatasource.cs new file mode 100644 index 00000000..2c5a3a05 --- /dev/null +++ b/src/Carbunql.TypeSafe/QueryDatasource.cs @@ -0,0 +1,27 @@ +using Carbunql.Analysis.Parser; +using Carbunql.Building; + +namespace Carbunql.TypeSafe; + +public class QueryDatasource(SelectQuery query) : IDatasource +{ + public SelectQuery Query { get; init; } = query; + + public bool IsCTE => false; + + public SelectQuery BuildFromClause(SelectQuery query, string alias) + { + query.From(Query).As(alias); + return query; + } + + public SelectQuery BuildJoinClause(SelectQuery query, string join, string alias, string condition) + { + var r = query.FromClause!.Join(Query.ToSelectableTable(), join).As(alias); + if (!string.IsNullOrEmpty(condition)) + { + r.On(_ => ValueParser.Parse(condition)); + } + return query; + } +} diff --git a/src/Carbunql.TypeSafe/Sql.cs b/src/Carbunql.TypeSafe/Sql.cs index deae7e53..67a8ed18 100644 --- a/src/Carbunql.TypeSafe/Sql.cs +++ b/src/Carbunql.TypeSafe/Sql.cs @@ -1,48 +1,92 @@ -using Carbunql.Analysis.Parser; -using Carbunql.Annotations; -using Carbunql.Building; -using System.Data; +using Carbunql.Annotations; using System.Linq.Expressions; namespace Carbunql.TypeSafe; - /// /// Only function definitions are written for use in expression trees. /// The actual situation is in ExpressionExtension.cs. /// (SelectQuery query) where T : ITableRowDefinition, new() + private static T DefineCTE(Expression>> expression, Materialized materialization) where T : ITableRowDefinition, new() + { +#if DEBUG + var analyze = ExpressionReader.Analyze(expression); + // Debug analysis for the expression +#endif + if (expression.Body is MemberExpression body) + { + // Compile and execute the expression + var compiledExpression = expression.Compile(); + var result = compiledExpression(); + + if (result is SelectQuery sq) + { + var variableName = body.Member.Name; + + var instance = new T(); + instance.Datasource = new CTEDatasoure(variableName, sq) + { + Materialized = materialization + }; + + return instance; + } + else + { + throw new NotSupportedException("The provided expression did not result in a SelectQuery."); + } + } + throw new NotSupportedException("Expression body is not a MemberExpression."); + } + + + public static T DefineCTE(Expression>> expression) where T : ITableRowDefinition, new() + { + return DefineCTE(expression, Materialized.Undefined); + } + + public static T DefineMaterializedCTE(Expression>> expression) where T : ITableRowDefinition, new() + { + return DefineCTE(expression, Materialized.Materialized); + } + + public static T DefineNotMaterializedCTE(Expression>> expression) where T : ITableRowDefinition, new() + { + return DefineCTE(expression, Materialized.NotMaterialized); + } + + public static T DefineSubQuery(SelectQuery query) where T : ITableRowDefinition, new() { var instance = new T(); - var info = TableInfoFactory.Create(typeof(T)); + //var info = TableInfoFactory.Create(typeof(T)); - instance.CreateTableQuery = query.ToCreateTableQuery("_" + info.Table, isTemporary: true); + instance.Datasource = new QueryDatasource(query); return instance; } - public static T DefineTable(FluentSelectQuery query) where T : ITableRowDefinition, new() + public static T DefineSubQuery(FluentSelectQuery query) where T : ITableRowDefinition, new() { var instance = new T(); - var info = TableInfoFactory.Create(typeof(T)); + //var info = TableInfoFactory.Create(typeof(T)); - instance.CreateTableQuery = query.ToCreateTableQuery("_" + info.Table, isTemporary: true); + instance.Datasource = new QueryDatasource(query); return instance; } - public static T DefineTable(Func> builder) where T : ITableRowDefinition, new() + public static T DefineSubQuery(Func> builder) where T : ITableRowDefinition, new() { - return DefineTable(builder.Invoke()); + return DefineSubQuery(builder.Invoke()); } public static T DefineTable() where T : ITableRowDefinition, new() { var instance = new T(); var clause = TableDefinitionClauseFactory.Create(); - instance.CreateTableQuery = new CreateTableQuery(clause); + instance.Datasource = new PhysicalTableDatasource(clause); return instance; } @@ -56,14 +100,7 @@ public static FluentSelectQuery From(Expression> expression) where T var compiledExpression = expression.Compile(); var result = compiledExpression(); - if (result.CreateTableQuery.Query != null) - { - sq.From(result.CreateTableQuery.Query).As(alias); - } - else if (result.CreateTableQuery.DefinitionClause != null) - { - sq.From(result.CreateTableQuery.DefinitionClause).As(alias); - } + result.Datasource.BuildFromClause(sq, alias); return sq; } @@ -99,12 +136,3 @@ public static string Raw(string command) public static string RowNumberOrderBy(object order) => string.Empty; } - -public struct CTEDefinition -{ - public string Name { get; set; } - - public Type RowType { get; set; } - - public string Query { get; set; } -} diff --git a/src/Carbunql/Tables/PhysicalTable.cs b/src/Carbunql/Tables/PhysicalTable.cs index 0125797c..e1d35774 100644 --- a/src/Carbunql/Tables/PhysicalTable.cs +++ b/src/Carbunql/Tables/PhysicalTable.cs @@ -1,4 +1,5 @@ using Carbunql.Clauses; +using Carbunql.Definitions; using MessagePack; using System.Collections.Immutable; @@ -30,6 +31,12 @@ public PhysicalTable(string schema, string table) Table = table; } + public PhysicalTable(ITable tb) + { + Schema = tb.Schema; + Table = tb.Table; + } + /// /// Gets or sets the schema name. /// diff --git a/src/Carbunql/Values/Interval.cs b/src/Carbunql/Values/Interval.cs index 3cefd058..68572234 100644 --- a/src/Carbunql/Values/Interval.cs +++ b/src/Carbunql/Values/Interval.cs @@ -1,5 +1,4 @@ using Carbunql.Clauses; -using Carbunql.Extensions; using Carbunql.Tables; using MessagePack; diff --git a/src/Carbunql/Values/OperatableValue.cs b/src/Carbunql/Values/OperatableValue.cs index 3114a1d5..bc2a5575 100644 --- a/src/Carbunql/Values/OperatableValue.cs +++ b/src/Carbunql/Values/OperatableValue.cs @@ -24,7 +24,7 @@ public OperatableValue(string @operator, ValueBase value) /// /// Gets or sets the operator to apply to the value. /// - public string Operator { get; init ; } + public string Operator { get; init; } /// /// Gets or sets the value to which the operator is applied. diff --git a/test/Carbunql.TypeSafe.Test/CTETest.cs b/test/Carbunql.TypeSafe.Test/CTETest.cs new file mode 100644 index 00000000..704ec805 --- /dev/null +++ b/test/Carbunql.TypeSafe.Test/CTETest.cs @@ -0,0 +1,156 @@ +using Xunit.Abstractions; + +namespace Carbunql.TypeSafe.Test; + +public class CTETest +{ + public CTETest(ITestOutputHelper output) + { + Output = output; + } + + private ITestOutputHelper Output { get; } + + private FluentSelectQuery SelectTodayOrder() + { + var o = Sql.DefineTable(); + var query = Sql.From(() => o).Where(() => o.order_date == Sql.Now); + return query.Compile(); + } + + [Fact] + public void CTE() + { + // Assign to a variable + var today_order = SelectTodayOrder(); + + // Pass the variable using an Expression + var o = Sql.DefineCTE(() => today_order); + + var query = Sql.From(() => o) + .Select(() => new { o.store_id }); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"WITH + today_order AS ( + SELECT + * + FROM + order AS o + WHERE + o.order_date = CAST(NOW() AS timestamp) + ) +SELECT + o.store_id +FROM + today_order AS o"; + + Assert.Equal(expect, actual, true, true, true); + } + + [Fact] + public void CTE_Materialized() + { + // Assign to a variable + var filtered_order = SelectTodayOrder(); + + // Pass the variable using an Expression + var o = Sql.DefineMaterializedCTE(() => filtered_order); + + var query = Sql.From(() => o) + .Select(() => new { o.store_id }); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"WITH + filtered_order AS MATERIALIZED ( + SELECT + * + FROM + order AS o + WHERE + o.order_date = CAST(NOW() AS timestamp) + ) +SELECT + o.store_id +FROM + filtered_order AS o"; + + Assert.Equal(expect, actual, true, true, true); + } + + [Fact] + public void CTE_NotMaterialized() + { + // Assign to a variable + var filtered_order = SelectTodayOrder(); + + // Pass the variable using an Expression + var o = Sql.DefineNotMaterializedCTE(() => filtered_order); + + var query = Sql.From(() => o) + .Select(() => new { o.store_id }); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"WITH + filtered_order AS NOT MATERIALIZED ( + SELECT + * + FROM + order AS o + WHERE + o.order_date = CAST(NOW() AS timestamp) + ) +SELECT + o.store_id +FROM + filtered_order AS o"; + + Assert.Equal(expect, actual, true, true, true); + } + + public record product(int product_id, string name, decimal price) : ITableRowDefinition + { + // no arguments constructor. + // Since it is used as a definition, it has no particular meaning as a value. + public product() : this(0, "", 0) { } + + // interface property + IDatasource ITableRowDefinition.Datasource { get; set; } = null!; + } + + public record store(int store_id, string name, string location) : ITableRowDefinition + { + // no arguments constructor. + // Since it is used as a definition, it has no particular meaning as a value. + public store() : this(0, "", "") { } + + // interface property + IDatasource ITableRowDefinition.Datasource { get; set; } = null!; + } + + public record order(int order_id, DateTime order_date, string customer_name, int store_id, List order_details) : ITableRowDefinition + { + // no arguments constructor. + // Since it is used as a definition, it has no particular meaning as a value. + public order() : this(0, DateTime.Now, "", 0, new List()) { } + + // interface property + IDatasource ITableRowDefinition.Datasource { get; set; } = null!; + } + + public record order_detail(int order_detail_id, int order_id, int product_id, int quantity, decimal price) : ITableRowDefinition + { + // no arguments constructor. + // Since it is used as a definition, it has no particular meaning as a value. + public order_detail() : this(0, 0, 0, 0, 0) { } + + // interface property + IDatasource ITableRowDefinition.Datasource { get; set; } = null!; + } +} diff --git a/test/Carbunql.TypeSafe.Test/CompileTest.cs b/test/Carbunql.TypeSafe.Test/CompileTest.cs index cfff1d50..01dbd602 100644 --- a/test/Carbunql.TypeSafe.Test/CompileTest.cs +++ b/test/Carbunql.TypeSafe.Test/CompileTest.cs @@ -108,7 +108,7 @@ public record product(int product_id, string name, decimal price) : ITableRowDef public product() : this(0, "", 0) { } // interface property - CreateTableQuery ITableRowDefinition.CreateTableQuery { get; set; } = null!; + IDatasource ITableRowDefinition.Datasource { get; set; } = null!; } public record store(int store_id, string name, string location) : ITableRowDefinition @@ -118,7 +118,7 @@ public record store(int store_id, string name, string location) : ITableRowDefin public store() : this(0, "", "") { } // interface property - CreateTableQuery ITableRowDefinition.CreateTableQuery { get; set; } = null!; + IDatasource ITableRowDefinition.Datasource { get; set; } = null!; } public record order(int order_id, DateTime order_date, string customer_name, int store_id, List order_details) : ITableRowDefinition @@ -128,7 +128,7 @@ public record order(int order_id, DateTime order_date, string customer_name, int public order() : this(0, DateTime.Now, "", 0, new List()) { } // interface property - CreateTableQuery ITableRowDefinition.CreateTableQuery { get; set; } = null!; + IDatasource ITableRowDefinition.Datasource { get; set; } = null!; } public record order_detail(int order_detail_id, int order_id, int product_id, int quantity, decimal price) : ITableRowDefinition @@ -138,6 +138,6 @@ public record order_detail(int order_detail_id, int order_id, int product_id, in public order_detail() : this(0, 0, 0, 0, 0) { } // interface property - CreateTableQuery ITableRowDefinition.CreateTableQuery { get; set; } = null!; + IDatasource ITableRowDefinition.Datasource { get; set; } = null!; } } diff --git a/test/Carbunql.TypeSafe.Test/JoinTest.cs b/test/Carbunql.TypeSafe.Test/JoinTest.cs index cc7d5cd1..816bc00c 100644 --- a/test/Carbunql.TypeSafe.Test/JoinTest.cs +++ b/test/Carbunql.TypeSafe.Test/JoinTest.cs @@ -1,5 +1,4 @@ -using Carbunql.Clauses; -using Xunit.Abstractions; +using Xunit.Abstractions; namespace Carbunql.TypeSafe.Test; @@ -88,7 +87,7 @@ public record product(int product_id, string name, decimal price) : ITableRowDef public product() : this(0, "", 0) { } // interface property - CreateTableQuery ITableRowDefinition.CreateTableQuery { get; set; } = null!; + IDatasource ITableRowDefinition.Datasource { get; set; } = null!; } public record store(int store_id, string name, string location) : ITableRowDefinition @@ -98,7 +97,7 @@ public record store(int store_id, string name, string location) : ITableRowDefin public store() : this(0, "", "") { } // interface property - CreateTableQuery ITableRowDefinition.CreateTableQuery { get; set; } = null!; + IDatasource ITableRowDefinition.Datasource { get; set; } = null!; } public record order(int order_id, DateTime order_date, string customer_name, int store_id, List order_details) : ITableRowDefinition @@ -108,7 +107,7 @@ public record order(int order_id, DateTime order_date, string customer_name, int public order() : this(0, DateTime.Now, "", 0, new List()) { } // interface property - CreateTableQuery ITableRowDefinition.CreateTableQuery { get; set; } = null!; + IDatasource ITableRowDefinition.Datasource { get; set; } = null!; } public record order_detail(int order_detail_id, int order_id, int product_id, int quantity, decimal price) : ITableRowDefinition @@ -118,6 +117,6 @@ public record order_detail(int order_detail_id, int order_id, int product_id, in public order_detail() : this(0, 0, 0, 0, 0) { } // interface property - CreateTableQuery ITableRowDefinition.CreateTableQuery { get; set; } = null!; + IDatasource ITableRowDefinition.Datasource { get; set; } = null!; } } diff --git a/test/Carbunql.TypeSafe.Test/SingleTableTest.cs b/test/Carbunql.TypeSafe.Test/SingleTableTest.cs index 9683c44e..1546984c 100644 --- a/test/Carbunql.TypeSafe.Test/SingleTableTest.cs +++ b/test/Carbunql.TypeSafe.Test/SingleTableTest.cs @@ -1,4 +1,3 @@ -using Carbunql.Clauses; using Carbunql.TypeSafe.Extensions; using Xunit.Abstractions; @@ -702,6 +701,6 @@ DateTime created_at public sale() : this(0, "", 0, 0, DateTime.Now) { } // interface property - CreateTableQuery ITableRowDefinition.CreateTableQuery { get; set; } = null!; + IDatasource ITableRowDefinition.Datasource { get; set; } = null!; } } \ No newline at end of file diff --git a/test/Carbunql.TypeSafe.Test/SubQueryTest.cs b/test/Carbunql.TypeSafe.Test/SubQueryTest.cs index 719d26ef..fc4aabc4 100644 --- a/test/Carbunql.TypeSafe.Test/SubQueryTest.cs +++ b/test/Carbunql.TypeSafe.Test/SubQueryTest.cs @@ -1,6 +1,4 @@ -using Carbunql.Clauses; -using System.Runtime.InteropServices; -using Xunit.Abstractions; +using Xunit.Abstractions; namespace Carbunql.TypeSafe.Test; @@ -39,7 +37,7 @@ private FluentSelectQuery SelectOrder() [Fact] public void SubQuery_NoType() { - var o = Sql.DefineTable(SelectOrderById_NoType(1)); + var o = Sql.DefineSubQuery(SelectOrderById_NoType(1)); var query = Sql.From(() => o) .Select(() => new { o.store_id }); @@ -68,7 +66,7 @@ order AS o [Fact] public void SubQuery() { - var o = Sql.DefineTable(SelectOrderById(1)); + var o = Sql.DefineSubQuery(SelectOrderById(1)); var query = Sql.From(() => o) .Select(() => new { o.store_id }); @@ -97,9 +95,9 @@ order AS o [Fact] public void SubQuery_Injection() { - var o = Sql.DefineTable(() => + var o = Sql.DefineSubQuery(() => { - var x = Sql.DefineTable(SelectOrder()); + var x = Sql.DefineSubQuery(SelectOrder()); return Sql.From(() => x).Where(() => x.store_id == 1).Compile(); }); @@ -136,7 +134,7 @@ public record product(int product_id, string name, decimal price) : ITableRowDef public product() : this(0, "", 0) { } // interface property - CreateTableQuery ITableRowDefinition.CreateTableQuery { get; set; } = null!; + IDatasource ITableRowDefinition.Datasource { get; set; } = null!; } public record store(int store_id, string name, string location) : ITableRowDefinition @@ -146,7 +144,7 @@ public record store(int store_id, string name, string location) : ITableRowDefin public store() : this(0, "", "") { } // interface property - CreateTableQuery ITableRowDefinition.CreateTableQuery { get; set; } = null!; + IDatasource ITableRowDefinition.Datasource { get; set; } = null!; } public record order(int order_id, DateTime order_date, string customer_name, int store_id, List order_details) : ITableRowDefinition @@ -156,7 +154,7 @@ public record order(int order_id, DateTime order_date, string customer_name, int public order() : this(0, DateTime.Now, "", 0, new List()) { } // interface property - CreateTableQuery ITableRowDefinition.CreateTableQuery { get; set; } = null!; + IDatasource ITableRowDefinition.Datasource { get; set; } = null!; } public record order_detail(int order_detail_id, int order_id, int product_id, int quantity, decimal price) : ITableRowDefinition @@ -166,6 +164,6 @@ public record order_detail(int order_detail_id, int order_id, int product_id, in public order_detail() : this(0, 0, 0, 0, 0) { } // interface property - CreateTableQuery ITableRowDefinition.CreateTableQuery { get; set; } = null!; + IDatasource ITableRowDefinition.Datasource { get; set; } = null!; } } diff --git a/test/Carbunql.TypeSafe.Test/WhereTest.cs b/test/Carbunql.TypeSafe.Test/WhereTest.cs index a8e01148..8fb49f02 100644 --- a/test/Carbunql.TypeSafe.Test/WhereTest.cs +++ b/test/Carbunql.TypeSafe.Test/WhereTest.cs @@ -1,5 +1,4 @@ -using Carbunql.Clauses; -using Xunit.Abstractions; +using Xunit.Abstractions; namespace Carbunql.TypeSafe.Test; @@ -265,6 +264,6 @@ decimal unit_price public sale() : this(0, "", 0, 0) { } // interface property - CreateTableQuery ITableRowDefinition.CreateTableQuery { get; set; } = null!; + IDatasource ITableRowDefinition.Datasource { get; set; } = null!; } } \ No newline at end of file From 06362bd38ff4d1080e166d85180e20567e32b1c1 Mon Sep 17 00:00:00 2001 From: mk3008 Date: Mon, 27 May 2024 22:23:43 +0900 Subject: [PATCH 6/6] Now supports CTE. The function name for Define processing has been unified to DefineDataSet. The name of IDatasource has been changed to IDataSet. The name of ITableRowDefinition has been changed to IDataRow. The CTETable class has been added (may be removed). --- .../{CTEDatasoure.cs => CTEDataSet.cs} | 7 +- src/Carbunql.TypeSafe/CTETable.cs | 42 +++++++++ .../Extensions/MemberExpressionExtension.cs | 2 +- src/Carbunql.TypeSafe/FluentSelectQuery.cs | 41 +++++++-- .../{ITableRowDefinition.cs => IDataRow.cs} | 4 +- .../{IDatasource.cs => IDataSet.cs} | 4 +- ...eDatasource.cs => PhysicalTableDataSet.cs} | 4 +- .../{QueryDatasource.cs => QueryDataSet.cs} | 4 +- src/Carbunql.TypeSafe/Sql.cs | 74 +++++++++++---- test/Carbunql.TypeSafe.Test/CTETest.cs | 39 +++++--- test/Carbunql.TypeSafe.Test/CompileTest.cs | 31 ++++--- test/Carbunql.TypeSafe.Test/JoinTest.cs | 32 +++---- .../Carbunql.TypeSafe.Test/SingleTableTest.cs | 40 ++++---- test/Carbunql.TypeSafe.Test/SubQueryTest.cs | 91 ++++++++++--------- test/Carbunql.TypeSafe.Test/WhereTest.cs | 24 ++--- 15 files changed, 275 insertions(+), 164 deletions(-) rename src/Carbunql.TypeSafe/{CTEDatasoure.cs => CTEDataSet.cs} (80%) create mode 100644 src/Carbunql.TypeSafe/CTETable.cs rename src/Carbunql.TypeSafe/{ITableRowDefinition.cs => IDataRow.cs} (52%) rename src/Carbunql.TypeSafe/{IDatasource.cs => IDataSet.cs} (75%) rename src/Carbunql.TypeSafe/{PhysicalTableDatasource.cs => PhysicalTableDataSet.cs} (82%) rename src/Carbunql.TypeSafe/{QueryDatasource.cs => QueryDataSet.cs} (82%) diff --git a/src/Carbunql.TypeSafe/CTEDatasoure.cs b/src/Carbunql.TypeSafe/CTEDataSet.cs similarity index 80% rename from src/Carbunql.TypeSafe/CTEDatasoure.cs rename to src/Carbunql.TypeSafe/CTEDataSet.cs index a24213fd..d8bced39 100644 --- a/src/Carbunql.TypeSafe/CTEDatasoure.cs +++ b/src/Carbunql.TypeSafe/CTEDataSet.cs @@ -3,7 +3,7 @@ namespace Carbunql.TypeSafe; -public class CTEDatasoure(string name, SelectQuery query) : IDatasource +public class CTEDataSet(string name, SelectQuery query) : IDataSet { public string Name { get; set; } = name; @@ -11,14 +11,13 @@ public class CTEDatasoure(string name, SelectQuery query) : IDatasource public SelectQuery Query { get; init; } = query; - public bool IsCTE => true; + public List Columns { get; init; } = query.GetColumnNames().ToList(); public SelectQuery BuildFromClause(SelectQuery query, string alias) { var cte = query.With(Query).As(Name); cte.Materialized = Materialized; - - query.From(cte).As(alias); + query.From(new CTETable(cte).ToSelectable()).As(alias); return query; } diff --git a/src/Carbunql.TypeSafe/CTETable.cs b/src/Carbunql.TypeSafe/CTETable.cs new file mode 100644 index 00000000..2834ecc2 --- /dev/null +++ b/src/Carbunql.TypeSafe/CTETable.cs @@ -0,0 +1,42 @@ +using Carbunql.Clauses; +using Carbunql.Tables; + +namespace Carbunql.TypeSafe; + +public class CTETable(CommonTable ct) : TableBase +{ + public CommonTable CommonTable { get; init; } = ct; + + /// + public override IEnumerable GetTokens(Token? parent) + { + yield return new Token(this, parent, CommonTable.Alias); + } + + /// + public override IEnumerable GetParameters() + { + yield break; + } + + /// + public override IList GetColumnNames() => CommonTable.GetColumnNames().ToList(); + + /// + public override IEnumerable GetPhysicalTables() + { + yield break; + } + + /// + public override IEnumerable GetCommonTables() + { + yield break; + } + + /// + public override IEnumerable GetInternalQueries() + { + yield break; + } +} diff --git a/src/Carbunql.TypeSafe/Extensions/MemberExpressionExtension.cs b/src/Carbunql.TypeSafe/Extensions/MemberExpressionExtension.cs index 62f0e5f7..76e36ccc 100644 --- a/src/Carbunql.TypeSafe/Extensions/MemberExpressionExtension.cs +++ b/src/Carbunql.TypeSafe/Extensions/MemberExpressionExtension.cs @@ -15,7 +15,7 @@ internal static string ToValue(this MemberExpression mem // ex. Sql.Now, Sql.CurrentTimestamp return CreateSqlCommand(mem); } - if (mem.Expression is MemberExpression && typeof(ITableRowDefinition).IsAssignableFrom(tp)) + if (mem.Expression is MemberExpression && typeof(IDataRow).IsAssignableFrom(tp)) { //column var table = ((MemberExpression)mem.Expression).Member.Name; diff --git a/src/Carbunql.TypeSafe/FluentSelectQuery.cs b/src/Carbunql.TypeSafe/FluentSelectQuery.cs index 16d9a77f..187c96c7 100644 --- a/src/Carbunql.TypeSafe/FluentSelectQuery.cs +++ b/src/Carbunql.TypeSafe/FluentSelectQuery.cs @@ -36,7 +36,7 @@ public FluentSelectQuery Select(Expression> expression) where T : cla return this; } - public FluentSelectQuery InnerJoin(Expression> tableExpression, Expression> conditionExpression) where T : ITableRowDefinition + public FluentSelectQuery InnerJoin(Expression> tableExpression, Expression> conditionExpression) where T : IDataRow { #if DEBUG var analyzed = ExpressionReader.Analyze(conditionExpression); @@ -51,12 +51,12 @@ public FluentSelectQuery InnerJoin(Expression> tableExpression, Expre var prmManager = new ParameterManager(GetParameters(), AddParameter); var condition = ToValue(conditionExpression.Body, prmManager.AddParaemter); - table.Datasource.BuildJoinClause(this, "inner join", tableAlias, condition); + table.DataSet.BuildJoinClause(this, "inner join", tableAlias, condition); return this; } - public FluentSelectQuery LeftJoin(Expression> tableExpression, Expression> conditionExpression) where T : ITableRowDefinition + public FluentSelectQuery LeftJoin(Expression> tableExpression, Expression> conditionExpression) where T : IDataRow { #if DEBUG var analyzed = ExpressionReader.Analyze(conditionExpression); @@ -71,12 +71,12 @@ public FluentSelectQuery LeftJoin(Expression> tableExpression, Expres var prmManager = new ParameterManager(GetParameters(), AddParameter); var condition = ToValue(conditionExpression.Body, prmManager.AddParaemter); - table.Datasource.BuildJoinClause(this, "left join", tableAlias, condition); + table.DataSet.BuildJoinClause(this, "left join", tableAlias, condition); return this; } - public FluentSelectQuery RightJoin(Expression> tableExpression, Expression> conditionExpression) where T : ITableRowDefinition + public FluentSelectQuery RightJoin(Expression> tableExpression, Expression> conditionExpression) where T : IDataRow { #if DEBUG var analyzed = ExpressionReader.Analyze(conditionExpression); @@ -91,12 +91,12 @@ public FluentSelectQuery RightJoin(Expression> tableExpression, Expre var prmManager = new ParameterManager(GetParameters(), AddParameter); var condition = ToValue(conditionExpression.Body, prmManager.AddParaemter); - table.Datasource.BuildJoinClause(this, "right join", tableAlias, condition); + table.DataSet.BuildJoinClause(this, "right join", tableAlias, condition); return this; } - public FluentSelectQuery CrossJoin(Expression> tableExpression) where T : ITableRowDefinition + public FluentSelectQuery CrossJoin(Expression> tableExpression) where T : IDataRow { var tableAlias = ((MemberExpression)tableExpression.Body).Member.Name; @@ -105,7 +105,7 @@ public FluentSelectQuery CrossJoin(Expression> tableExpression) where var compiledExpression = tableExpression.Compile(); var table = compiledExpression(); - table.Datasource.BuildJoinClause(this, "cross join", tableAlias); + table.DataSet.BuildJoinClause(this, "cross join", tableAlias); return this; } @@ -196,7 +196,7 @@ private string RemoveRootBracketOrDefault(string value) /// /// Thrown when the select clause does not include all required columns of the table row definition type. /// - public FluentSelectQuery Compile(bool force = false) where T : ITableRowDefinition, new() + public FluentSelectQuery Compile(bool force = false) where T : IDataRow, new() { var q = new FluentSelectQuery(); @@ -222,6 +222,14 @@ private string RemoveRootBracketOrDefault(string value) TypeValidate(q, clause); + if (SelectClause == null) + { + foreach (var item in clause.OfType()) + { + q.Select(q.FromClause!.Root, item.ColumnName); + } + }; + return q; } @@ -269,6 +277,19 @@ private static void TypeValidate(SelectQuery q, TableDefinitionClause clause) { TypeValidate(vq, clause); } + else if (q.FromClause.Root.Table is CTETable ct) + { + // Check if all properties of T are specified in the select clause + var aliases = ct.GetColumnNames().ToHashSet(); + var missingColumns = clause.ColumnNames.Where(item => !aliases.Contains(item)).ToList(); + + if (missingColumns.Any()) + { + // If there are missing columns, include all of them in the error message + throw new InvalidProgramException($"The select query is not compatible with '{typeof(T).Name}'. The following columns are missing: {string.Join(", ", missingColumns)}"); + } + return; + } else if (!actual.Equals(expect)) { throw new InvalidProgramException($"The select query is not compatible with '{typeof(T).Name}'. Expect: {expect}, Actual: {actual}"); @@ -282,7 +303,7 @@ private static void TypeValidate(SelectQuery q, TableDefinitionClause clause) } } -public class FluentSelectQuery : FluentSelectQuery where T : ITableRowDefinition, new() +public class FluentSelectQuery : FluentSelectQuery where T : IDataRow, new() { public T ToTable() { diff --git a/src/Carbunql.TypeSafe/ITableRowDefinition.cs b/src/Carbunql.TypeSafe/IDataRow.cs similarity index 52% rename from src/Carbunql.TypeSafe/ITableRowDefinition.cs rename to src/Carbunql.TypeSafe/IDataRow.cs index be2aa017..e8e44880 100644 --- a/src/Carbunql.TypeSafe/ITableRowDefinition.cs +++ b/src/Carbunql.TypeSafe/IDataRow.cs @@ -2,8 +2,8 @@ namespace Carbunql.TypeSafe; -public interface ITableRowDefinition +public interface IDataRow { [IgnoreMapping] - IDatasource Datasource { get; set; } + IDataSet DataSet { get; set; } } diff --git a/src/Carbunql.TypeSafe/IDatasource.cs b/src/Carbunql.TypeSafe/IDataSet.cs similarity index 75% rename from src/Carbunql.TypeSafe/IDatasource.cs rename to src/Carbunql.TypeSafe/IDataSet.cs index e49aca40..85880a41 100644 --- a/src/Carbunql.TypeSafe/IDatasource.cs +++ b/src/Carbunql.TypeSafe/IDataSet.cs @@ -1,8 +1,8 @@ namespace Carbunql.TypeSafe; -public interface IDatasource +public interface IDataSet { - bool IsCTE { get; } + public List Columns { get; } SelectQuery BuildFromClause(SelectQuery query, string alias); diff --git a/src/Carbunql.TypeSafe/PhysicalTableDatasource.cs b/src/Carbunql.TypeSafe/PhysicalTableDataSet.cs similarity index 82% rename from src/Carbunql.TypeSafe/PhysicalTableDatasource.cs rename to src/Carbunql.TypeSafe/PhysicalTableDataSet.cs index 5eadfbe4..3c1ad35a 100644 --- a/src/Carbunql.TypeSafe/PhysicalTableDatasource.cs +++ b/src/Carbunql.TypeSafe/PhysicalTableDataSet.cs @@ -6,11 +6,11 @@ namespace Carbunql.TypeSafe; -public class PhysicalTableDatasource(ITable tb) : IDatasource +public class PhysicalTableDataSet(ITable tb, IEnumerable columns) : IDataSet { public SelectableTable Table { get; set; } = new PhysicalTable(tb).ToSelectable(); - public bool IsCTE => false; + public List Columns { get; init; } = columns.ToList(); public SelectQuery BuildFromClause(SelectQuery query, string alias) { diff --git a/src/Carbunql.TypeSafe/QueryDatasource.cs b/src/Carbunql.TypeSafe/QueryDataSet.cs similarity index 82% rename from src/Carbunql.TypeSafe/QueryDatasource.cs rename to src/Carbunql.TypeSafe/QueryDataSet.cs index 2c5a3a05..76b26572 100644 --- a/src/Carbunql.TypeSafe/QueryDatasource.cs +++ b/src/Carbunql.TypeSafe/QueryDataSet.cs @@ -3,11 +3,11 @@ namespace Carbunql.TypeSafe; -public class QueryDatasource(SelectQuery query) : IDatasource +public class QueryDataSet(SelectQuery query) : IDataSet { public SelectQuery Query { get; init; } = query; - public bool IsCTE => false; + public List Columns { get; init; } = query.GetColumnNames().ToList(); public SelectQuery BuildFromClause(SelectQuery query, string alias) { diff --git a/src/Carbunql.TypeSafe/Sql.cs b/src/Carbunql.TypeSafe/Sql.cs index 67a8ed18..d97b7c94 100644 --- a/src/Carbunql.TypeSafe/Sql.cs +++ b/src/Carbunql.TypeSafe/Sql.cs @@ -1,4 +1,5 @@ using Carbunql.Annotations; +using System.ComponentModel.Design; using System.Linq.Expressions; namespace Carbunql.TypeSafe; @@ -9,7 +10,7 @@ namespace Carbunql.TypeSafe; /// (Expression>> expression, Materialized materialization) where T : ITableRowDefinition, new() + private static T DefineDataSet(Expression>> expression, Materialized materialization) where T : IDataRow, new() { #if DEBUG var analyze = ExpressionReader.Analyze(expression); @@ -26,7 +27,7 @@ public static class Sql var variableName = body.Member.Name; var instance = new T(); - instance.Datasource = new CTEDatasoure(variableName, sq) + instance.DataSet = new CTEDataSet(variableName, sq) { Materialized = materialization }; @@ -38,59 +39,94 @@ public static class Sql throw new NotSupportedException("The provided expression did not result in a SelectQuery."); } } + else if (expression.Body is MethodCallExpression me) + { + var compiledExpression = expression.Compile(); + var result = compiledExpression(); + if (result is SelectQuery sq) + { + var instance = new T(); + instance.DataSet = new QueryDataSet(sq); + return instance; + } + else + { + throw new NotSupportedException("The provided expression did not result in a SelectQuery."); + } + } throw new NotSupportedException("Expression body is not a MemberExpression."); } + public static T2 DefineDataSet(Expression>> expression, Func, FluentSelectQuery> editor) where T1 : IDataRow, new() where T2 : IDataRow, new() + { +#if DEBUG + var analyze = ExpressionReader.Analyze(expression); + // Debug analysis for the expression +#endif + var compiledExpression = expression.Compile(); + var result = compiledExpression(); + if (result is FluentSelectQuery sq) + { + var edited = editor(sq); + + var instance = new T2(); - public static T DefineCTE(Expression>> expression) where T : ITableRowDefinition, new() + instance.DataSet = new QueryDataSet(edited); + return instance; + } + else + { + throw new NotSupportedException("The provided expression did not result in a SelectQuery."); + } + } + + public static T DefineDataSet(Expression>> expression) where T : IDataRow, new() { - return DefineCTE(expression, Materialized.Undefined); + return DefineDataSet(expression, Materialized.Undefined); } - public static T DefineMaterializedCTE(Expression>> expression) where T : ITableRowDefinition, new() + public static T DefineMaterializedDataSet(Expression>> expression) where T : IDataRow, new() { - return DefineCTE(expression, Materialized.Materialized); + return DefineDataSet(expression, Materialized.Materialized); } - public static T DefineNotMaterializedCTE(Expression>> expression) where T : ITableRowDefinition, new() + public static T DefineNotMaterializedDataSet(Expression>> expression) where T : IDataRow, new() { - return DefineCTE(expression, Materialized.NotMaterialized); + return DefineDataSet(expression, Materialized.NotMaterialized); } - public static T DefineSubQuery(SelectQuery query) where T : ITableRowDefinition, new() + public static T DefineSubQuery(SelectQuery query) where T : IDataRow, new() { var instance = new T(); //var info = TableInfoFactory.Create(typeof(T)); - instance.Datasource = new QueryDatasource(query); + instance.DataSet = new QueryDataSet(query); return instance; } - public static T DefineSubQuery(FluentSelectQuery query) where T : ITableRowDefinition, new() + public static T DefineSubQuery(FluentSelectQuery query) where T : IDataRow, new() { var instance = new T(); - //var info = TableInfoFactory.Create(typeof(T)); - - instance.Datasource = new QueryDatasource(query); + instance.DataSet = new QueryDataSet(query); return instance; } - public static T DefineSubQuery(Func> builder) where T : ITableRowDefinition, new() + public static T DefineSubQuery(Func> builder) where T : IDataRow, new() { return DefineSubQuery(builder.Invoke()); } - public static T DefineTable() where T : ITableRowDefinition, new() + public static T DefineDataSet() where T : IDataRow, new() { var instance = new T(); var clause = TableDefinitionClauseFactory.Create(); - instance.Datasource = new PhysicalTableDatasource(clause); + instance.DataSet = new PhysicalTableDataSet(clause, clause.GetColumnNames()); return instance; } - public static FluentSelectQuery From(Expression> expression) where T : ITableRowDefinition + public static FluentSelectQuery From(Expression> expression) where T : IDataRow { var sq = new FluentSelectQuery(); @@ -100,7 +136,7 @@ public static FluentSelectQuery From(Expression> expression) where T var compiledExpression = expression.Compile(); var result = compiledExpression(); - result.Datasource.BuildFromClause(sq, alias); + result.DataSet.BuildFromClause(sq, alias); return sq; } diff --git a/test/Carbunql.TypeSafe.Test/CTETest.cs b/test/Carbunql.TypeSafe.Test/CTETest.cs index 704ec805..c9a62f12 100644 --- a/test/Carbunql.TypeSafe.Test/CTETest.cs +++ b/test/Carbunql.TypeSafe.Test/CTETest.cs @@ -13,7 +13,7 @@ public CTETest(ITestOutputHelper output) private FluentSelectQuery SelectTodayOrder() { - var o = Sql.DefineTable(); + var o = Sql.DefineDataSet(); var query = Sql.From(() => o).Where(() => o.order_date == Sql.Now); return query.Compile(); } @@ -25,7 +25,7 @@ public void CTE() var today_order = SelectTodayOrder(); // Pass the variable using an Expression - var o = Sql.DefineCTE(() => today_order); + var o = Sql.DefineDataSet(() => today_order); var query = Sql.From(() => o) .Select(() => new { o.store_id }); @@ -36,7 +36,10 @@ public void CTE() var expect = @"WITH today_order AS ( SELECT - * + o.order_id, + o.order_date, + o.customer_name, + o.store_id FROM order AS o WHERE @@ -57,7 +60,7 @@ public void CTE_Materialized() var filtered_order = SelectTodayOrder(); // Pass the variable using an Expression - var o = Sql.DefineMaterializedCTE(() => filtered_order); + var o = Sql.DefineMaterializedDataSet(() => filtered_order); var query = Sql.From(() => o) .Select(() => new { o.store_id }); @@ -68,7 +71,10 @@ public void CTE_Materialized() var expect = @"WITH filtered_order AS MATERIALIZED ( SELECT - * + o.order_id, + o.order_date, + o.customer_name, + o.store_id FROM order AS o WHERE @@ -89,7 +95,7 @@ public void CTE_NotMaterialized() var filtered_order = SelectTodayOrder(); // Pass the variable using an Expression - var o = Sql.DefineNotMaterializedCTE(() => filtered_order); + var o = Sql.DefineNotMaterializedDataSet(() => filtered_order); var query = Sql.From(() => o) .Select(() => new { o.store_id }); @@ -100,7 +106,10 @@ public void CTE_NotMaterialized() var expect = @"WITH filtered_order AS NOT MATERIALIZED ( SELECT - * + o.order_id, + o.order_date, + o.customer_name, + o.store_id FROM order AS o WHERE @@ -114,43 +123,43 @@ order AS o Assert.Equal(expect, actual, true, true, true); } - public record product(int product_id, string name, decimal price) : ITableRowDefinition + public record product(int product_id, string name, decimal price) : IDataRow { // no arguments constructor. // Since it is used as a definition, it has no particular meaning as a value. public product() : this(0, "", 0) { } // interface property - IDatasource ITableRowDefinition.Datasource { get; set; } = null!; + IDataSet IDataRow.DataSet { get; set; } = null!; } - public record store(int store_id, string name, string location) : ITableRowDefinition + public record store(int store_id, string name, string location) : IDataRow { // no arguments constructor. // Since it is used as a definition, it has no particular meaning as a value. public store() : this(0, "", "") { } // interface property - IDatasource ITableRowDefinition.Datasource { get; set; } = null!; + IDataSet IDataRow.DataSet { get; set; } = null!; } - public record order(int order_id, DateTime order_date, string customer_name, int store_id, List order_details) : ITableRowDefinition + public record order(int order_id, DateTime order_date, string customer_name, int store_id, List order_details) : IDataRow { // no arguments constructor. // Since it is used as a definition, it has no particular meaning as a value. public order() : this(0, DateTime.Now, "", 0, new List()) { } // interface property - IDatasource ITableRowDefinition.Datasource { get; set; } = null!; + IDataSet IDataRow.DataSet { get; set; } = null!; } - public record order_detail(int order_detail_id, int order_id, int product_id, int quantity, decimal price) : ITableRowDefinition + public record order_detail(int order_detail_id, int order_id, int product_id, int quantity, decimal price) : IDataRow { // no arguments constructor. // Since it is used as a definition, it has no particular meaning as a value. public order_detail() : this(0, 0, 0, 0, 0) { } // interface property - IDatasource ITableRowDefinition.Datasource { get; set; } = null!; + IDataSet IDataRow.DataSet { get; set; } = null!; } } diff --git a/test/Carbunql.TypeSafe.Test/CompileTest.cs b/test/Carbunql.TypeSafe.Test/CompileTest.cs index 01dbd602..33141f1d 100644 --- a/test/Carbunql.TypeSafe.Test/CompileTest.cs +++ b/test/Carbunql.TypeSafe.Test/CompileTest.cs @@ -14,7 +14,7 @@ public CompileTest(ITestOutputHelper output) [Fact] public void Compile_SelectAll() { - var o = Sql.DefineTable(); + var o = Sql.DefineDataSet(); var query = Sql.From(() => o).Compile(); @@ -22,7 +22,10 @@ public void Compile_SelectAll() Output.WriteLine(actual); var expect = @"SELECT - * + o.order_id, + o.order_date, + o.customer_name, + o.store_id FROM order AS o"; @@ -32,7 +35,7 @@ public void Compile_SelectAll() [Fact] public void Compile_SelectAll_Exception() { - var o = Sql.DefineTable(); + var o = Sql.DefineDataSet(); var query = Sql.From(() => o); @@ -45,7 +48,7 @@ public void Compile_SelectAll_Exception() [Fact] public void Compile_Excess() { - var o = Sql.DefineTable(); + var o = Sql.DefineDataSet(); var query = Sql.From(() => o).Select(() => new { o.order_id, o.store_id, o.order_date, o.customer_name, memo = "test" }).Compile(); @@ -70,7 +73,7 @@ public void Compile_Excess() [Fact] public void Compile_Undersized() { - var o = Sql.DefineTable(); + var o = Sql.DefineDataSet(); var query = Sql.From(() => o).Select(() => new { o.order_id }); @@ -83,7 +86,7 @@ public void Compile_Undersized() [Fact] public void Compile_ForceCorrect() { - var o = Sql.DefineTable(); + var o = Sql.DefineDataSet(); var query = Sql.From(() => o).Select(() => new { o.order_id }).Compile(true); @@ -101,43 +104,43 @@ public void Compile_ForceCorrect() Assert.Equal(expect, actual, true, true, true); } - public record product(int product_id, string name, decimal price) : ITableRowDefinition + public record product(int product_id, string name, decimal price) : IDataRow { // no arguments constructor. // Since it is used as a definition, it has no particular meaning as a value. public product() : this(0, "", 0) { } // interface property - IDatasource ITableRowDefinition.Datasource { get; set; } = null!; + IDataSet IDataRow.DataSet { get; set; } = null!; } - public record store(int store_id, string name, string location) : ITableRowDefinition + public record store(int store_id, string name, string location) : IDataRow { // no arguments constructor. // Since it is used as a definition, it has no particular meaning as a value. public store() : this(0, "", "") { } // interface property - IDatasource ITableRowDefinition.Datasource { get; set; } = null!; + IDataSet IDataRow.DataSet { get; set; } = null!; } - public record order(int order_id, DateTime order_date, string customer_name, int store_id, List order_details) : ITableRowDefinition + public record order(int order_id, DateTime order_date, string customer_name, int store_id, List order_details) : IDataRow { // no arguments constructor. // Since it is used as a definition, it has no particular meaning as a value. public order() : this(0, DateTime.Now, "", 0, new List()) { } // interface property - IDatasource ITableRowDefinition.Datasource { get; set; } = null!; + IDataSet IDataRow.DataSet { get; set; } = null!; } - public record order_detail(int order_detail_id, int order_id, int product_id, int quantity, decimal price) : ITableRowDefinition + public record order_detail(int order_detail_id, int order_id, int product_id, int quantity, decimal price) : IDataRow { // no arguments constructor. // Since it is used as a definition, it has no particular meaning as a value. public order_detail() : this(0, 0, 0, 0, 0) { } // interface property - IDatasource ITableRowDefinition.Datasource { get; set; } = null!; + IDataSet IDataRow.DataSet { get; set; } = null!; } } diff --git a/test/Carbunql.TypeSafe.Test/JoinTest.cs b/test/Carbunql.TypeSafe.Test/JoinTest.cs index 816bc00c..d898cc95 100644 --- a/test/Carbunql.TypeSafe.Test/JoinTest.cs +++ b/test/Carbunql.TypeSafe.Test/JoinTest.cs @@ -14,10 +14,10 @@ public JoinTest(ITestOutputHelper output) [Fact] public void InnerJoin() { - var od = Sql.DefineTable(); - var o = Sql.DefineTable(); - var p = Sql.DefineTable(); - var s = Sql.DefineTable(); + var od = Sql.DefineDataSet(); + var o = Sql.DefineDataSet(); + var p = Sql.DefineDataSet(); + var s = Sql.DefineDataSet(); var query = Sql.From(() => od) .InnerJoin(() => o, () => o.order_id == od.order_id) @@ -41,8 +41,8 @@ order_detail AS od [Fact] public void LeftJoin() { - var o = Sql.DefineTable(); - var od = Sql.DefineTable(); + var o = Sql.DefineDataSet(); + var od = Sql.DefineDataSet(); var query = Sql.From(() => o) .LeftJoin(() => od, () => o.order_id == od.order_id); @@ -62,8 +62,8 @@ order AS o [Fact] public void CrossJoin() { - var p = Sql.DefineTable(); - var s = Sql.DefineTable(); + var p = Sql.DefineDataSet(); + var s = Sql.DefineDataSet(); var query = Sql.From(() => p) .CrossJoin(() => s); @@ -80,43 +80,43 @@ product AS p Assert.Equal(expect, actual, true, true, true); } - public record product(int product_id, string name, decimal price) : ITableRowDefinition + public record product(int product_id, string name, decimal price) : IDataRow { // no arguments constructor. // Since it is used as a definition, it has no particular meaning as a value. public product() : this(0, "", 0) { } // interface property - IDatasource ITableRowDefinition.Datasource { get; set; } = null!; + IDataSet IDataRow.DataSet { get; set; } = null!; } - public record store(int store_id, string name, string location) : ITableRowDefinition + public record store(int store_id, string name, string location) : IDataRow { // no arguments constructor. // Since it is used as a definition, it has no particular meaning as a value. public store() : this(0, "", "") { } // interface property - IDatasource ITableRowDefinition.Datasource { get; set; } = null!; + IDataSet IDataRow.DataSet { get; set; } = null!; } - public record order(int order_id, DateTime order_date, string customer_name, int store_id, List order_details) : ITableRowDefinition + public record order(int order_id, DateTime order_date, string customer_name, int store_id, List order_details) : IDataRow { // no arguments constructor. // Since it is used as a definition, it has no particular meaning as a value. public order() : this(0, DateTime.Now, "", 0, new List()) { } // interface property - IDatasource ITableRowDefinition.Datasource { get; set; } = null!; + IDataSet IDataRow.DataSet { get; set; } = null!; } - public record order_detail(int order_detail_id, int order_id, int product_id, int quantity, decimal price) : ITableRowDefinition + public record order_detail(int order_detail_id, int order_id, int product_id, int quantity, decimal price) : IDataRow { // no arguments constructor. // Since it is used as a definition, it has no particular meaning as a value. public order_detail() : this(0, 0, 0, 0, 0) { } // interface property - IDatasource ITableRowDefinition.Datasource { get; set; } = null!; + IDataSet IDataRow.DataSet { get; set; } = null!; } } diff --git a/test/Carbunql.TypeSafe.Test/SingleTableTest.cs b/test/Carbunql.TypeSafe.Test/SingleTableTest.cs index 1546984c..9e0ae4cc 100644 --- a/test/Carbunql.TypeSafe.Test/SingleTableTest.cs +++ b/test/Carbunql.TypeSafe.Test/SingleTableTest.cs @@ -15,7 +15,7 @@ public SingleTableTest(ITestOutputHelper output) [Fact] public void SelectAllTest() { - var a = Sql.DefineTable(); + var a = Sql.DefineDataSet(); var query = Sql.From(() => a); @@ -33,7 +33,7 @@ public void SelectAllTest() [Fact] public void SelectTest() { - var a = Sql.DefineTable(); + var a = Sql.DefineDataSet(); var query = Sql.From(() => a) .Select(() => new @@ -55,7 +55,7 @@ public void SelectTest() [Fact] public void AliasTest() { - var a = Sql.DefineTable(); + var a = Sql.DefineDataSet(); var query = Sql.From(() => a) .Select(() => new @@ -81,7 +81,7 @@ public void AliasTest() [Fact] public void LiteralTest() { - var a = Sql.DefineTable(); + var a = Sql.DefineDataSet(); var query = Sql.From(() => a) .Select(() => new @@ -117,7 +117,7 @@ public void LiteralTest() [Fact] public void BracketTest() { - var a = Sql.DefineTable(); + var a = Sql.DefineDataSet(); var query = Sql.From(() => a) .Select(() => new @@ -141,7 +141,7 @@ public void BracketTest() [Fact] public void VariableTest() { - var a = Sql.DefineTable(); + var a = Sql.DefineDataSet(); var id = 1; var value = (long)10; @@ -192,7 +192,7 @@ public void VariableTest() [Fact] public void CSharpFunction_Coalesce() { - var a = Sql.DefineTable(); + var a = Sql.DefineDataSet(); var query = Sql.From(() => a) .Select(() => new @@ -216,7 +216,7 @@ public void CSharpFunction_Coalesce() [Fact] public void CSharpFunction_Operator() { - var a = Sql.DefineTable(); + var a = Sql.DefineDataSet(); var query = Sql.From(() => a) .Select(() => new @@ -248,7 +248,7 @@ public void CSharpFunction_Operator() [Fact] public void DatetimeTest_SqlCommand() { - var a = Sql.DefineTable(); + var a = Sql.DefineDataSet(); var d = new DateTime(2000, 10, 20); @@ -289,7 +289,7 @@ public void DatetimeTest_SqlCommand() [Fact] public void DatetimeTest_SqlExtension() { - var a = Sql.DefineTable(); + var a = Sql.DefineDataSet(); var d = new DateTime(2000, 10, 20); @@ -332,7 +332,7 @@ public void DatetimeTest_SqlExtension() [Fact] public void CSharpFunction_Math() { - var a = Sql.DefineTable(); + var a = Sql.DefineDataSet(); var query = Sql.From(() => a) .Select(() => new @@ -366,7 +366,7 @@ public void CSharpFunction_Math() [Fact] public void RawCommand() { - var a = Sql.DefineTable(); + var a = Sql.DefineDataSet(); var query = Sql.From(() => a) .Select(() => new @@ -388,7 +388,7 @@ current_timestamp AS rawcommand [Fact] public void ReservedCommand() { - var a = Sql.DefineTable(); + var a = Sql.DefineDataSet(); var query = Sql.From(() => a) .Select(() => new @@ -435,7 +435,7 @@ ORDER BY [Fact] public void Trinomial() { - var a = Sql.DefineTable(); + var a = Sql.DefineDataSet(); var query = Sql.From(() => a) .Select(() => new @@ -485,7 +485,7 @@ END AS v_le [Fact] public void Trinomial_When() { - var a = Sql.DefineTable(); + var a = Sql.DefineDataSet(); var query = Sql.From(() => a) .Select(() => new @@ -515,7 +515,7 @@ END AS v_nest [Fact] public void Trinomial_Nest() { - var a = Sql.DefineTable(); + var a = Sql.DefineDataSet(); var query = Sql.From(() => a) .Select(() => new @@ -563,7 +563,7 @@ END AS v_nest [Fact] public void TrimTest() { - var a = Sql.DefineTable(); + var a = Sql.DefineDataSet(); var query = Sql.From(() => a) .Select(() => new @@ -589,7 +589,7 @@ public void TrimTest() [Fact] public void ToStringTest() { - var a = Sql.DefineTable(); + var a = Sql.DefineDataSet(); var query = Sql.From(() => a) .Select(() => new @@ -694,13 +694,13 @@ public record sale( int quantity, decimal unit_price, DateTime created_at - ) : ITableRowDefinition + ) : IDataRow { // no arguments constructor. // Since it is used as a definition, it has no particular meaning as a value. public sale() : this(0, "", 0, 0, DateTime.Now) { } // interface property - IDatasource ITableRowDefinition.Datasource { get; set; } = null!; + IDataSet IDataRow.DataSet { get; set; } = null!; } } \ No newline at end of file diff --git a/test/Carbunql.TypeSafe.Test/SubQueryTest.cs b/test/Carbunql.TypeSafe.Test/SubQueryTest.cs index fc4aabc4..e2f04025 100644 --- a/test/Carbunql.TypeSafe.Test/SubQueryTest.cs +++ b/test/Carbunql.TypeSafe.Test/SubQueryTest.cs @@ -11,33 +11,25 @@ public SubQueryTest(ITestOutputHelper output) private ITestOutputHelper Output { get; } - private FluentSelectQuery SelectOrderById_NoType(int id) + private FluentSelectQuery SelectOrderByStoreId(int store_id) { - var o = Sql.DefineTable(); + var o = Sql.DefineDataSet(); var query = Sql.From(() => o) - .Where(() => o.store_id == id); - return query; - } - - private FluentSelectQuery SelectOrderById(int id) - { - var o = Sql.DefineTable(); - var query = Sql.From(() => o) - .Where(() => o.store_id == id); + .Where(() => o.store_id == store_id); return query.Compile(); } private FluentSelectQuery SelectOrder() { - var o = Sql.DefineTable(); + var o = Sql.DefineDataSet(); var query = Sql.From(() => o); return query.Compile(); } [Fact] - public void SubQuery_NoType() + public void SubQuery() { - var o = Sql.DefineSubQuery(SelectOrderById_NoType(1)); + var o = Sql.DefineDataSet(() => SelectOrder()); var query = Sql.From(() => o) .Select(() => new { o.store_id }); @@ -45,28 +37,26 @@ public void SubQuery_NoType() var actual = query.ToText(); Output.WriteLine(actual); - var expect = @"/* - :id = 1 -*/ -SELECT + var expect = @"SELECT o.store_id FROM ( SELECT - * + o.order_id, + o.order_date, + o.customer_name, + o.store_id FROM order AS o - WHERE - o.store_id = :id ) AS o"; Assert.Equal(expect, actual, true, true, true); } [Fact] - public void SubQuery() + public void SubQuery_Parameter() { - var o = Sql.DefineSubQuery(SelectOrderById(1)); + var o = Sql.DefineDataSet(() => SelectOrderByStoreId(1)); var query = Sql.From(() => o) .Select(() => new { o.store_id }); @@ -75,29 +65,32 @@ public void SubQuery() Output.WriteLine(actual); var expect = @"/* - :id = 1 + :store_id = 1 */ SELECT o.store_id FROM ( SELECT - * + o.order_id, + o.order_date, + o.customer_name, + o.store_id FROM order AS o WHERE - o.store_id = :id + o.store_id = :store_id ) AS o"; Assert.Equal(expect, actual, true, true, true); } [Fact] - public void SubQuery_Injection() + public void SubQuery_Edit() { - var o = Sql.DefineSubQuery(() => + var o = Sql.DefineDataSet(() => SelectOrder(), all_order => { - var x = Sql.DefineSubQuery(SelectOrder()); + var x = Sql.DefineDataSet(() => all_order); return Sql.From(() => x).Where(() => x.store_id == 1).Compile(); }); @@ -107,19 +100,27 @@ public void SubQuery_Injection() var actual = query.ToText(); Output.WriteLine(actual); - var expect = @"SELECT + var expect = @"WITH + all_order AS ( + SELECT + o.order_id, + o.order_date, + o.customer_name, + o.store_id + FROM + order AS o + ) +SELECT o.store_id FROM ( SELECT - * + x.order_id, + x.order_date, + x.customer_name, + x.store_id FROM - ( - SELECT - * - FROM - order AS o - ) AS x + all_order AS x WHERE x.store_id = 1 ) AS o"; @@ -127,43 +128,43 @@ order AS o Assert.Equal(expect, actual, true, true, true); } - public record product(int product_id, string name, decimal price) : ITableRowDefinition + public record product(int product_id, string name, decimal price) : IDataRow { // no arguments constructor. // Since it is used as a definition, it has no particular meaning as a value. public product() : this(0, "", 0) { } // interface property - IDatasource ITableRowDefinition.Datasource { get; set; } = null!; + IDataSet IDataRow.DataSet { get; set; } = null!; } - public record store(int store_id, string name, string location) : ITableRowDefinition + public record store(int store_id, string name, string location) : IDataRow { // no arguments constructor. // Since it is used as a definition, it has no particular meaning as a value. public store() : this(0, "", "") { } // interface property - IDatasource ITableRowDefinition.Datasource { get; set; } = null!; + IDataSet IDataRow.DataSet { get; set; } = null!; } - public record order(int order_id, DateTime order_date, string customer_name, int store_id, List order_details) : ITableRowDefinition + public record order(int order_id, DateTime order_date, string customer_name, int store_id, List order_details) : IDataRow { // no arguments constructor. // Since it is used as a definition, it has no particular meaning as a value. public order() : this(0, DateTime.Now, "", 0, new List()) { } // interface property - IDatasource ITableRowDefinition.Datasource { get; set; } = null!; + IDataSet IDataRow.DataSet { get; set; } = null!; } - public record order_detail(int order_detail_id, int order_id, int product_id, int quantity, decimal price) : ITableRowDefinition + public record order_detail(int order_detail_id, int order_id, int product_id, int quantity, decimal price) : IDataRow { // no arguments constructor. // Since it is used as a definition, it has no particular meaning as a value. public order_detail() : this(0, 0, 0, 0, 0) { } // interface property - IDatasource ITableRowDefinition.Datasource { get; set; } = null!; + IDataSet IDataRow.DataSet { get; set; } = null!; } } diff --git a/test/Carbunql.TypeSafe.Test/WhereTest.cs b/test/Carbunql.TypeSafe.Test/WhereTest.cs index 8fb49f02..45da2090 100644 --- a/test/Carbunql.TypeSafe.Test/WhereTest.cs +++ b/test/Carbunql.TypeSafe.Test/WhereTest.cs @@ -14,7 +14,7 @@ public WhereTest(ITestOutputHelper output) [Fact] public void WhereStaticValue() { - var a = Sql.DefineTable(); + var a = Sql.DefineDataSet(); var query = Sql.From(() => a) .Where(() => a.sale_id == 1); @@ -35,7 +35,7 @@ sale AS a [Fact] public void WhereStaticVariable() { - var a = Sql.DefineTable(); + var a = Sql.DefineDataSet(); var id = 1; @@ -61,7 +61,7 @@ sale AS a [Fact] public void Multiple() { - var a = Sql.DefineTable(); + var a = Sql.DefineDataSet(); var query = Sql.From(() => a) .Where(() => a.quantity == 1) @@ -84,7 +84,7 @@ sale AS a [Fact] public void AndTest() { - var a = Sql.DefineTable(); + var a = Sql.DefineDataSet(); var query = Sql.From(() => a) .Where(() => a.quantity == 1 && a.unit_price == 2); @@ -106,7 +106,7 @@ sale AS a [Fact] public void OrTest() { - var a = Sql.DefineTable(); + var a = Sql.DefineDataSet(); var query = Sql.From(() => a) .Where(() => a.quantity == 1 || a.unit_price == 2); @@ -127,7 +127,7 @@ sale AS a [Fact] public void BracketTest() { - var a = Sql.DefineTable(); + var a = Sql.DefineDataSet(); var query = Sql.From(() => a) .Where(() => @@ -152,7 +152,7 @@ sale AS a [Fact] public void LikeTest() { - var a = Sql.DefineTable(); + var a = Sql.DefineDataSet(); var query = Sql.From(() => a) .Where(() => a.product_name.StartsWith("a")) @@ -182,7 +182,7 @@ public void InTest() { var idArray = new List() { 1, 2, 3, 4 }; - var a = Sql.DefineTable(); + var a = Sql.DefineDataSet(); var query = Sql.From(() => a) .Where(() => idArray.Contains(a.sale_id!.Value)); @@ -205,7 +205,7 @@ public void AnyTest() { var idArray = new List() { 1, 2, 3, 4 }; - var a = Sql.DefineTable(); + var a = Sql.DefineDataSet(); var query = Sql.From(() => a) .Where(() => idArray.Any(x => a.sale_id!.Value == x)); @@ -231,7 +231,7 @@ public void AnyTest_Right() { var idArray = new List() { 1, 2, 3, 4 }; - var a = Sql.DefineTable(); + var a = Sql.DefineDataSet(); var query = Sql.From(() => a) .Where(() => idArray.Any(x => x == a.sale_id!.Value)); @@ -257,13 +257,13 @@ public record sale( string product_name, int quantity, decimal unit_price - ) : ITableRowDefinition + ) : IDataRow { // no arguments constructor. // Since it is used as a definition, it has no particular meaning as a value. public sale() : this(0, "", 0, 0) { } // interface property - IDatasource ITableRowDefinition.Datasource { get; set; } = null!; + IDataSet IDataRow.DataSet { get; set; } = null!; } } \ No newline at end of file