Mapping Postgres tstzrange to NpgsqlRange with linq2db
See original GitHub issueOK, this one is strange and I can’t find anything that would help me out in resolving the issue, but it’s entirely possible I’m doing something wrong.
It seems that if I map the range to NpgsqlRange<DateTimeOffset>
, I can save the data just fine but I can’t query it, and if I map it as NpgsqlRange<DateTime>
I can’t insert a new row, but I can query existing data set.
Let’s start with the first option: mapping the range to NpgsqlRange<DateTimeOffset>
property for reading & writing.
The insert goes fine, but on any query involving the property I get:
LinqToDB.Common.LinqToDBConvertException: Cannot convert value '[01/25/2021 18:37:30,01/26/2021 18:37:30]' to type 'NpgsqlTypes.NpgsqlRange`1[[System.DateTimeOffset, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]'
---> System.InvalidCastException: Object must implement IConvertible.
at System.Convert.ChangeType(Object value, Type conversionType, IFormatProvider provider)
at LinqToDB.Common.ConvertBuilder.ConvertDefault(Object value, Type conversionType)
--- End of inner exception stack trace ---
at LinqToDB.Common.ConvertBuilder.ConvertDefault(Object value, Type conversionType)
at lambda_method187(Closure , IDataReader )
at LinqToDB.Expressions.ConvertFromDataReaderExpression.ColumnReader.GetValue(IDataReader dataReader)
at lambda_method186(Closure , IQueryRunner , IDataReader )
at LinqToDB.Linq.QueryRunner.Mapper`1.Map(IDataContext context, IQueryRunner queryRunner, IDataReader dataReader)
at LinqToDB.Linq.QueryRunner.ExecuteQueryAsync[T](Query query, IDataContext dataContext, Mapper`1 mapper, Expression expression, Object[] ps, Object[] preambles, Int32 queryNumber, Func`2 func, TakeSkipDelegate skipAction, TakeSkipDelegate takeAction, CancellationToken cancellationToken)
at LinqToDB.Linq.QueryRunner.ExecuteQueryAsync[T](Query query, IDataContext dataContext, Mapper`1 mapper, Expression expression, Object[] ps, Object[] preambles, Int32 queryNumber, Func`2 func, TakeSkipDelegate skipAction, TakeSkipDelegate takeAction, CancellationToken cancellationToken)
at LinqToDB.Linq.Builder.FirstSingleBuilder.FirstSingleContext.<>c__DisplayClass3_0`1.<<GetFirstElement>b__1>d.MoveNext()
--- End of stack trace from previous location ---
at LinqToDB.Linq.ExpressionQuery`1.LinqToDB.Async.IQueryProviderAsync.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken)
at linq2db_repro.Database.Program.Main(String[] args) in /app/Program.cs:line 51
at linq2db_repro.Database.Program.Main(String[] args) in /app/Program.cs:line 52
at linq2db_repro.Database.Program.<Main>(String[] args)
Steps to reproduce
https://github.com/pcwiek/linq2db-repro
For the ease of use & future-proofing, I’ll also put it here inline
namespace linq2db_repro
{
[Table("test")]
public class TestEntity
{
[NotNull]
[Column("timestamp_range")]
public NpgsqlRange<DateTimeOffset> RangeMappedAsDateTimeOffset { get; set; }
[NotNull]
[Column("timestamp_range")]
public NpgsqlRange<DateTime> RangeMappedAsDateTime { get; set; }
}
public class Database : DataConnection
{
public Database(string connectionString) : base(
new PostgreSQLDataProvider(PostgreSQLVersion.v95),
connectionString
)
{
}
public ITable<TestEntity> TestEntities => GetTable<TestEntity>();
}
class Program
{
async static Task Main(string[] args)
{
const string connectionString = "Host=db;Database=repro;Username=postgres;Password=postgres;";
await using var db = new Database(connectionString);
await db.ExecuteAsync(@"
CREATE TABLE IF NOT EXISTS test (
timestamp_range tstzrange not null
)
");
// The insert works
await db.TestEntities.InsertAsync(() => new TestEntity
{
RangeMappedAsDateTimeOffset = new NpgsqlRange<DateTimeOffset>(
DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)),
DateTime.UtcNow)
});
// This fails
var range = await db.TestEntities.Select(a => a.RangeMappedAsDateTimeOffset).FirstAsync();
Console.WriteLine("Range: {0}", range);
}
}
}
OK, this fails with the exception above, how about DateTime
mapping?
If we use RangeMappedAsDateTime
property with DateTime
type, on insert we get:
Npgsql.PostgresException (0x80004005): 42804: column "timestamp_range" is of type tstzrange but expression is of type tsrange
at Npgsql.NpgsqlConnector.<ReadMessage>g__ReadMessageLong|194_0(NpgsqlConnector connector, Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteReader(CommandBehavior behavior, Boolean async, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteReader(CommandBehavior behavior, Boolean async, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteNonQuery(Boolean async, CancellationToken cancellationToken)
at LinqToDB.Data.DataConnection.ExecuteNonQueryAsync(CancellationToken cancellationToken)
at LinqToDB.Data.DataConnection.QueryRunner.ExecuteNonQueryAsync(CancellationToken cancellationToken)
at LinqToDB.Linq.QueryRunner.NonQueryQueryAsync(Query query, IDataContext dataContext, Expression expression, Object[] ps, Object[] preambles, CancellationToken cancellationToken)
at LinqToDB.Linq.ExpressionQuery`1.LinqToDB.Async.IQueryProviderAsync.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken)
at LinqToDB.LinqExtensions.InsertAsync[T](ITable`1 target, Expression`1 setter, CancellationToken token)
at linq2db_repro.Program.Main(String[] args) in /app/Program.cs:line 47
at linq2db_repro.Program.Main(String[] args) in /app/Program.cs:line 54
at linq2db_repro.Program.<Main>(String[] args)
Exception data:
Severity: ERROR
SqlState: 42804
MessageText: column "timestamp_range" is of type tstzrange but expression is of type tsrange
Hint: You will need to rewrite or cast the expression.
Position: 49
File: parse_target.c
Line: 592
Routine: transformAssignedExpr
In the meantime I figured out that using DateTimeOffset
to insert data, and query using the property mapped as DateTime
works fine, although it is quite a hacky workaround:
await db.TestEntities.InsertAsync(() => new TestEntity
{
RangeMappedAsDateTimeOffset = new NpgsqlRange<DateTimeOffset>(
DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)),
DateTime.UtcNow)
});
NpgsqlRange<DateTime> range = await db.TestEntities.Select(a => a.RangeMappedAsDateTime).FirstAsync();
Console.WriteLine("Range: {0}", range);
When executed:
root@6846b48528a2:/app# dotnet run dist/linq2db-repro.dll
Range: [01/25/2021 18:37:30,01/26/2021 18:37:30]
Any ideas?
Environment details
linq2db version: 3.2.3
Database Server: Postgres 12 (postgres:12
docker image)
Database Provider: Npgsql 5.0.3
Operating system: Debian GNU/Linux 10 (buster) (mcr.microsoft.com/dotnet/sdk:5.0
docker image)
.NET Framework: .NET 5.0.102
Issue Analytics
- State:
- Created 3 years ago
- Comments:5 (5 by maintainers)
What I can say. NpgsqlRange is quite a sad story currently and I would recommend to use default types with it. For
tstzrange
it isDateTime
.Anyway, I’m working on improving this on our side.
Another related issue #3563