Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
using CmdScale.EntityFrameworkCore.TimescaleDB.Abstractions;
using CmdScale.EntityFrameworkCore.TimescaleDB.Operations;
using Microsoft.EntityFrameworkCore.Infrastructure;

namespace CmdScale.EntityFrameworkCore.TimescaleDB.Design.Generators
{
public class HypertableOperationGenerator
{
public static void Generate(CreateHypertableOperation operation, IndentedStringBuilder builder)
{
List<string> statements =
[
$"SELECT create_hypertable('\"\"{operation.TableName}\"\"', '{operation.TimeColumnName}');"
];

// ChunkTimeInterval
if (!string.IsNullOrEmpty(operation.ChunkTimeInterval))
{
// Check if the interval is a plain number (e.g., for microseconds).
if (long.TryParse(operation.ChunkTimeInterval, out _))
{
// If it's a number, don't wrap it in quotes.
statements.Add($"SELECT set_chunk_time_interval('\"\"{operation.TableName}\"\"', {operation.ChunkTimeInterval}::bigint);");
}
else
{
// If it's a string like '7 days', wrap it in quotes.
statements.Add($"SELECT set_chunk_time_interval('\"\"{operation.TableName}\"\"', INTERVAL '{operation.ChunkTimeInterval}');");
}
}

// EnableCompression
if (operation.EnableCompression || operation.ChunkSkipColumns?.Count > 0)
{
bool enableCompression = operation.EnableCompression || (operation.ChunkSkipColumns != null && operation.ChunkSkipColumns.Count > 0);
statements.Add($"ALTER TABLE \"\"{operation.TableName}\"\" SET (timescaledb.compress = {enableCompression.ToString().ToLower()});");
}

// ChunkSkipColumns
if (operation.ChunkSkipColumns != null && operation.ChunkSkipColumns.Count > 0)
{
statements.Add("SET timescaledb.enable_chunk_skipping = 'ON';");

foreach (string column in operation.ChunkSkipColumns)
{
statements.Add($"SELECT enable_chunk_skipping('\"\"{operation.TableName}\"\"', '{column}');");
}
}

// AdditionalDimensions
if (operation.AdditionalDimensions != null && operation.AdditionalDimensions.Count > 0)
{
foreach (Dimension dimension in operation.AdditionalDimensions)
{
if (dimension.Type == EDimensionType.Range)
{
statements.Add($"SELECT add_dimension('\"\"{operation.TableName}\"\"', by_range('{dimension.ColumnName}', INTERVAL '{dimension.Interval}'));");
}
else if (dimension.Type == EDimensionType.Hash)
{
statements.Add($"SELECT add_dimension('\"\"{operation.TableName}\"\"', by_hash('{dimension.ColumnName}', {dimension.NumberOfPartitions}));");
}
}
}

MigrationBuilderSqlHelper.BuildQueryString(statements, builder);
}

public static void Generate(AlterHypertableOperation operation, IndentedStringBuilder builder)
{
List<string> statements = [];

// Check for ChunkTimeInterval change
if (operation.ChunkTimeInterval != operation.OldChunkTimeInterval)
{
if (operation.ChunkTimeInterval != operation.OldChunkTimeInterval)
{
// Check if the interval is a plain number (e.g., for microseconds).
if (long.TryParse(operation.ChunkTimeInterval, out _))
{
// If it's a number, don't wrap it in quotes.
statements.Add($"SELECT set_chunk_time_interval('\"\"{operation.TableName}\"\"', {operation.ChunkTimeInterval}::bigint);");
}
else
{
// If it's a string like '7 days', wrap it in quotes.
statements.Add($"SELECT set_chunk_time_interval('\"\"{operation.TableName}\"\"', INTERVAL '{operation.ChunkTimeInterval}');");
}
}
}

// Check for EnableCompression change
bool newCompressionState = operation.EnableCompression || (operation.ChunkSkipColumns != null && operation.ChunkSkipColumns.Any());
bool oldCompressionState = operation.OldEnableCompression || (operation.OldChunkSkipColumns != null && operation.OldChunkSkipColumns.Any());

if (newCompressionState != oldCompressionState)
{
string compressionValue = newCompressionState.ToString().ToLower();
statements.Add($"ALTER TABLE \"\"{operation.TableName}\"\" SET (timescaledb.compress = {compressionValue});");
}

// Handle ChunkSkipColumns
IReadOnlyList<string> newColumns = operation.ChunkSkipColumns ?? [];
IReadOnlyList<string> oldColumns = operation.OldChunkSkipColumns ?? [];
List<string> addedColumns = [.. newColumns.Except(oldColumns)];

if (addedColumns.Count != 0)
{
statements.Add("SET timescaledb.enable_chunk_skipping = 'ON';");

foreach (string column in addedColumns)
{
statements.Add($"SELECT enable_chunk_skipping('\"\"{operation.TableName}\"\"', '{column}');");
}
}

List<string> removedColumns = [.. oldColumns.Except(newColumns)];
if (removedColumns.Count != 0)
{
foreach (string column in removedColumns)
{
statements.Add($"SELECT disable_chunk_skipping('\"\"{operation.TableName}\"\"', '{column}');");
}
}

MigrationBuilderSqlHelper.BuildQueryString(statements, builder);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore.Infrastructure;

namespace CmdScale.EntityFrameworkCore.TimescaleDB.Design.Generators
{
public static class MigrationBuilderSqlHelper
{
public static void BuildQueryString(List<string> statements, IndentedStringBuilder builder)
{
if (statements.Count > 0)
{
builder.AppendLine(".Sql(@\"");
using (builder.Indent())
{
foreach (string statement in statements)
{
builder.AppendLine(statement);
}
}
builder.Append("\")");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
using CmdScale.EntityFrameworkCore.TimescaleDB.Operations;
using Microsoft.EntityFrameworkCore.Infrastructure;
using System.Globalization;

namespace CmdScale.EntityFrameworkCore.TimescaleDB.Design.Generators
{
public class ReorderPolicyOperationGenerator
{
public static void Generate(AddReorderPolicyOperation operation, IndentedStringBuilder builder)
{
List<string> statements =
[
BuildAddReorderPolicySql(operation.TableName, operation.IndexName, operation.InitialStart)
];

List<string> alterJobClauses = BuildAlterJobClauses(operation);
if (alterJobClauses.Count != 0)
{
statements.Add(BuildAlterJobSql(operation.TableName, alterJobClauses));
}

MigrationBuilderSqlHelper.BuildQueryString(statements, builder);
}

public static void Generate(AlterReorderPolicyOperation operation, IndentedStringBuilder builder)
{
List<string> statements = [];
bool needsRecreation = operation.IndexName != operation.OldIndexName || operation.InitialStart != operation.OldInitialStart;

if (needsRecreation)
{
statements.Add($"SELECT remove_reorder_policy('\"\"{operation.TableName}\"\"', if_exists => true);");
statements.Add(BuildAddReorderPolicySql(operation.TableName, operation.IndexName, operation.InitialStart));

// Create a temporary "add" operation representing the final desired state to ensure existing settings are reapplied.
AddReorderPolicyOperation finalStateOperation = new()
{
TableName = operation.TableName,
IndexName = operation.IndexName,
InitialStart = operation.InitialStart,
ScheduleInterval = operation.ScheduleInterval,
MaxRuntime = operation.MaxRuntime,
MaxRetries = operation.MaxRetries,
RetryPeriod = operation.RetryPeriod
};

List<string> finalStateClauses = BuildAlterJobClauses(finalStateOperation);
if (finalStateClauses.Count != 0)
{
statements.Add(BuildAlterJobSql(operation.TableName, finalStateClauses));
}
}
else
{
List<string> changedClauses = BuildAlterJobClauses(operation);
if (changedClauses.Count != 0)
{
statements.Add(BuildAlterJobSql(operation.TableName, changedClauses));
}
}

MigrationBuilderSqlHelper.BuildQueryString(statements, builder);
}

public static void Generate(DropReorderPolicyOperation operation, IndentedStringBuilder builder)
{
List<string> statements =
[
$"SELECT remove_reorder_policy('\"\"{operation.TableName}\"\"', if_exists => true);"
];
MigrationBuilderSqlHelper.BuildQueryString(statements, builder);
}

private static List<string> BuildAlterJobClauses(AddReorderPolicyOperation operation)
{
List<string> clauses = [];
if (!string.IsNullOrWhiteSpace(operation.ScheduleInterval) && operation.ScheduleInterval != DefaultValues.ReorderPolicyScheduleInterval)
clauses.Add($"schedule_interval => INTERVAL '{operation.ScheduleInterval}'");

if (!string.IsNullOrWhiteSpace(operation.MaxRuntime) && operation.MaxRuntime != DefaultValues.ReorderPolicyMaxRuntime)
clauses.Add($"max_runtime => INTERVAL '{operation.MaxRuntime}'");

if (operation.MaxRetries != null && operation.MaxRetries != DefaultValues.ReorderPolicyMaxRetries)
clauses.Add($"max_retries => {operation.MaxRetries}");

if (!string.IsNullOrWhiteSpace(operation.RetryPeriod) && operation.RetryPeriod != DefaultValues.ReorderPolicyRetryPeriod)
clauses.Add($"retry_period => INTERVAL '{operation.RetryPeriod}'");

return clauses;
}

private static List<string> BuildAlterJobClauses(AlterReorderPolicyOperation operation)
{
List<string> clauses = [];

if (!string.IsNullOrWhiteSpace(operation.ScheduleInterval) && operation.ScheduleInterval != operation.OldScheduleInterval)
clauses.Add($"schedule_interval => INTERVAL '{operation.ScheduleInterval}'");

if (!string.IsNullOrWhiteSpace(operation.MaxRuntime) && operation.MaxRuntime != operation.OldMaxRuntime)
{
string maxRuntimeValue = string.IsNullOrWhiteSpace(operation.MaxRuntime) ? "NULL" : $"INTERVAL '{operation.MaxRuntime}'";
clauses.Add($"max_runtime => {maxRuntimeValue}");
}

if (operation.MaxRetries != null && operation.MaxRetries != operation.OldMaxRetries)
clauses.Add($"max_retries => {operation.MaxRetries}");

if (!string.IsNullOrWhiteSpace(operation.RetryPeriod) && operation.RetryPeriod != operation.OldRetryPeriod)
clauses.Add($"retry_period => INTERVAL '{operation.RetryPeriod}'");

return clauses;
}

private static string BuildAlterJobSql(string tableName, IEnumerable<string> clauses)
{
return $@"
SELECT alter_job(job_id, {string.Join(", ", clauses)})
FROM timescaledb_information.jobs
WHERE proc_name = 'policy_reorder' AND hypertable_name = '{tableName}';".Trim();
}

private static string BuildAddReorderPolicySql(string tableName, string indexName, DateTime? initialStart)
{
string baseSql = $"SELECT add_reorder_policy('\"\"{tableName}\"\"', '{indexName}'";

List<string> optionalArgs = [];

// Add optional arguments if they are provided
if (initialStart.HasValue)
{
// Use ISO 8601 format for timestamps to avoid ambiguity
string timestamp = initialStart.Value.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture);
optionalArgs.Add($"initial_start => '{timestamp}'");
}

if (optionalArgs.Count > 0)
{
baseSql += $", {string.Join(", ", optionalArgs)}";
}

baseSql += ");";
return baseSql;
}
}
}
Loading
Loading