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
97 changes: 85 additions & 12 deletions scripts/dialect-convert/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,11 @@ def __init__(self, profile: dict):
self.table_columns: dict[str, set[str]] = collections.defaultdict(set)
self.dropped_columns: dict[str, set[str]] = collections.defaultdict(set)
self.relations: set[str] = set()
self.pending_statements: list[str] = []
# SQLite refuses DROP COLUMN when an index or CHECK still references the
# column, so track both to decide whether a drop can be emitted.
self.check_columns: dict[str, set[str]] = collections.defaultdict(set)
self.table_indexes: dict[str, list[tuple[str, set[str]]]] = collections.defaultdict(list)
self.skipped: list[tuple[str, str, str]] = []
self.downgrades: list[tuple[str, str, str]] = []

Expand Down Expand Up @@ -318,8 +323,10 @@ def portable_check(self, expr: str) -> str | None:
candidate = _rename_functions(RE["cast"].sub("", expr).strip(), self.p)
return candidate if self.is_portable(candidate) else None

def portable_default(self, expr: str) -> str | None:
def portable_default(self, expr: str, is_array: bool = False) -> str | None:
value = RE["epoch"].sub("__EPOCH__", expr.strip())
if is_array and value.strip() in ("'{}'", '"{}"'):
return "'[]'"
value = RE["cast"].sub("", value).strip()
low = value.lower()
if "nextval(" in low or "gen_random_uuid" in low or "uuid_generate" in low:
Expand Down Expand Up @@ -466,6 +473,13 @@ def _table_item(self, f: str, table: str, item: str) -> str:
return self._table_constraint(f, table, item)
return self._column(f, table, item)

def _pin(self, table: str, expr: str) -> None:
"""Record columns a surviving constraint references.

SQLite refuses DROP COLUMN while any index or constraint mentions the
column, so every emitted constraint pins whatever it names."""
self.check_columns[table.lower()].update(_identifiers(expr))

def _table_constraint(self, f: str, table: str, c: str) -> str:
up = c.upper()
if "EXCLUDE USING" in up:
Expand Down Expand Up @@ -496,7 +510,10 @@ def _table_constraint(self, f: str, table: str, c: str) -> str:
if portable is None:
self.skip(f, "non_portable_check", c)
return ""
self._pin(table, portable)
out = out[:open_idx + 1] + portable + out[close_idx:]

self._pin(table, out)
return out

def _column(self, f: str, table: str, col: str) -> str:
Expand All @@ -518,7 +535,9 @@ def _column(self, f: str, table: str, col: str) -> str:
return ""

self.table_columns[table.lower()].add(name.lower())
suffix = self._modifiers(f, table, name, modifiers)
suffix = self._modifiers(
f, table, name, modifiers, is_array=bool(RE["array_suffix"].search(sql_type))
)
return f'"{name}" {mapped}' + (f" {suffix}" if suffix else "")

def _generated_column(self, f: str, table: str, name: str, rest: str) -> str:
Expand All @@ -543,7 +562,9 @@ def _generated_column(self, f: str, table: str, name: str, rest: str) -> str:
self.table_columns[table.lower()].add(name.lower())
return f'"{name}" {mapped} GENERATED ALWAYS AS ({portable}) {stored}'

def _modifiers(self, f: str, table: str, column: str, mods: str) -> str:
def _modifiers(
self, f: str, table: str, column: str, mods: str, is_array: bool = False
) -> str:
value = RE["collate"].sub("", RE["identity"].sub("", mods.strip()))
value = RE["not_valid"].sub("", value)
parts: list[str] = []
Expand All @@ -552,7 +573,7 @@ def _modifiers(self, f: str, table: str, column: str, mods: str) -> str:
if dm:
expr, remainder = _split_default(value[dm.end():])
value = (value[:dm.start()] + " " + remainder).strip()
portable = self.portable_default(expr)
portable = self.portable_default(expr, is_array=is_array)
if portable is not None:
parts.append("DEFAULT " + portable)
else:
Expand All @@ -566,6 +587,8 @@ def _modifiers(self, f: str, table: str, column: str, mods: str) -> str:
portable = self.portable_check(value[open_idx + 1:close_idx])
if portable is not None:
parts.append(f"CHECK ({portable})")
self._pin(table, portable)
self._pin(table, column)
else:
self.skip(f, "non_portable_check", f"{table}.{column}")
value = (value[:cm.start()] + value[close_idx + 1:]).strip()
Expand Down Expand Up @@ -594,6 +617,9 @@ def _alter_table(self, f: str, s: str) -> str:
emitted = []
for action in split_top_level(actions):
converted = self._alter_action(f, table, action)
if self.pending_statements:
emitted.extend(self.pending_statements)
self.pending_statements = []
if converted:
emitted.append(f'ALTER TABLE "{table}" {converted};')
return ("\n\n" + SPLIT_MARKER + "\n\n").join(emitted)
Expand All @@ -605,14 +631,7 @@ def _alter_action(self, f: str, table: str, action: str) -> str:
if RE["add_column"].match(a):
return self._add_column(f, table, a)
if up.startswith("DROP COLUMN"):
if not self.p["supports_drop_column"]:
self.skip(f, "drop_column_unsupported", f"{table}: {a}")
return ""
dm = RE["drop_column"].match(a)
col = (dm.group(2) or dm.group(1)) if dm else ""
self.dropped_columns[table.lower()].add(col.lower())
self.table_columns[table.lower()].discard(col.lower())
return f'DROP COLUMN "{col}"'
return self._drop_column(f, table, a)
if up.startswith(("RENAME COLUMN", "RENAME TO")):
return a
if up.startswith(("ADD CONSTRAINT", "ADD PRIMARY KEY", "ADD FOREIGN KEY", "ADD UNIQUE", "ADD CHECK", "ADD EXCLUDE")):
Expand All @@ -631,6 +650,41 @@ def _alter_action(self, f: str, table: str, action: str) -> str:
self.skip(f, "no_op", f"{table}: {a}")
return ""

def _drop_column(self, f: str, table: str, action: str) -> str:
dm = RE["drop_column"].match(action)
if not dm:
self.skip(f, "unrecognized_statement", f"{table}: {action}")
return ""

col = (dm.group(2) or dm.group(1)).lower()
key = table.lower()

if not self.p["supports_drop_column"]:
self.skip(f, "drop_column_unsupported", f"{table}.{col}")
return ""

# A surviving CHECK pinned to the column makes the drop illegal, and there
# is no way to alter a CHECK out of the way in SQLite.
if col not in self.table_columns[key]:
self.skip(f, "drop_column_never_created", f"{table}.{col}")
return ""

if col in self.check_columns[key]:
self.skip(f, "drop_column_pinned_by_constraint", f"{table}.{col}")
return ""

blocking = [name for name, cols in self.table_indexes[key] if col in cols]
for name in blocking:
self.pending_statements.append(f'DROP INDEX IF EXISTS "{name}";')
self.table_indexes[key] = [
(name, cols) for name, cols in self.table_indexes[key] if col not in cols
]

self.dropped_columns[key].add(col)
self.table_columns[key].discard(col)

return f'DROP COLUMN "{col}"'

def _add_column(self, f: str, table: str, action: str) -> str:
definition = RE["add_column"].match(action).group(1).strip()
m = RE["column_def"].match(definition)
Expand Down Expand Up @@ -687,6 +741,11 @@ def _create_index(self, f: str, s: str) -> str:
columns = RE["cast"].sub("", RE["nulls_order"].sub("", RE["opclass"].sub("", columns)))
tail = RE["cast"].sub("", RE["with_opts"].sub("", RE["include"].sub("", tail)))
tail = re.sub(r"(?i)\s+TABLESPACE\s+\w+", "", tail).strip() or ";"

self.table_indexes[table.lower()].append(
(_unquote(m.group(2)), _identifiers(columns) | _identifiers(tail))
)

return f"{header.rstrip()} ({columns}){tail}"

def _view(self, f: str, s: str) -> str:
Expand Down Expand Up @@ -716,6 +775,20 @@ def _matview(self, f: str, s: str) -> str:
# helpers
# --------------------------------------------------------------------------

IDENTIFIER_RE = re.compile(r'"([^"]+)"|\b([a-z_][a-z0-9_]*)\b', re.I)


def _identifiers(expr: str) -> set[str]:
"""Lowercased identifiers appearing in an expression, keywords included.

Over-approximating is safe here: an extra name only makes a DROP COLUMN more
conservative, never less."""
found = set()
for quoted, bare in IDENTIFIER_RE.findall(expr or ""):
found.add((quoted or bare).lower())
return found


def _unquote(v: str) -> str:
return v.strip().strip('"')

Expand Down
7 changes: 5 additions & 2 deletions scripts/dialect-convert/profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
"int4": "INTEGER", "smallint": "INTEGER", "int2": "INTEGER", "bigserial": "INTEGER",
"serial": "INTEGER", "smallserial": "INTEGER", "boolean": "INTEGER", "bool": "INTEGER",
"real": "REAL", "float4": "REAL", "float8": "REAL", "double precision": "REAL",
"numeric": "NUMERIC", "decimal": "NUMERIC", "money": "NUMERIC",
# REAL rather than NUMERIC: NUMERIC affinity demotes integral values to
# INTEGER, which then refuses to scan into a Go float64. Precision is the
# same either way, and SQLite has no exact decimal regardless.
"numeric": "REAL", "decimal": "REAL", "money": "REAL",
"text": "TEXT", "citext": "TEXT", "varchar": "TEXT", "character varying": "TEXT",
"char": "TEXT", "character": "TEXT", "uuid": "TEXT", "json": "TEXT", "jsonb": "TEXT",
"xml": "TEXT", "inet": "TEXT", "cidr": "TEXT", "macaddr": "TEXT", "interval": "TEXT",
Expand Down Expand Up @@ -51,7 +54,7 @@
"index_methods": {"", "btree"},
"supports_add_constraint": False,
"supports_alter_column": False,
"supports_drop_column": False,
"supports_drop_column": True,
"supports_stored_generated_in_alter": False,
"materialized_view_as_view": True,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ type AccountType struct {
Description string `json:"description" bun:"description,type:TEXT,nullzero"`
Category Category `json:"category" bun:"category,type:account_category_enum,notnull"`
Color string `json:"color" bun:"color,type:VARCHAR(10),nullzero"`
IsSystem bool `json:"isSystem" bun:"is_system,type:BOOLEAN,default:false"`
IsSystem bool `json:"isSystem" bun:"is_system,type:BOOLEAN"`
Version int64 `json:"version" bun:"version,type:BIGINT"`
CreatedAt int64 `json:"createdAt" bun:"created_at,notnull,default:extract(epoch from current_timestamp)::bigint"`
UpdatedAt int64 `json:"updatedAt" bun:"updated_at,notnull,default:extract(epoch from current_timestamp)::bigint"`
Expand Down
4 changes: 2 additions & 2 deletions services/tms/internal/core/domain/agent/agentexception.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ type AgentException struct {
SubjectType SubjectType `json:"subjectType" bun:"subject_type,type:agent_subject_type_enum,notnull"`
SubjectID pulid.ID `json:"subjectId" bun:"subject_id,type:VARCHAR(100),notnull"`
AttemptSummary string `json:"attemptSummary" bun:"attempt_summary,type:TEXT,notnull"`
Evidence []EvidenceRef `json:"evidence" bun:"evidence,type:JSONB,notnull,default:'[]'::jsonb"`
BlastRadius int `json:"blastRadius" bun:"blast_radius,type:INTEGER,notnull,default:0"`
Evidence []EvidenceRef `json:"evidence" bun:"evidence,type:JSONB,notnull,default:'[]'"`
BlastRadius int `json:"blastRadius" bun:"blast_radius,type:INTEGER,notnull"`
ResolutionState ResolutionState `json:"resolutionState" bun:"resolution_state,type:agent_resolution_state_enum,notnull,default:'Open'"`
ResolutionNotes string `json:"resolutionNotes" bun:"resolution_notes,type:TEXT,nullzero"`

Expand Down
4 changes: 2 additions & 2 deletions services/tms/internal/core/domain/agent/agentproposal.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ type AgentProposal struct {

RunID pulid.ID `json:"runId" bun:"run_id,type:VARCHAR(100),notnull"`
ToolName string `json:"toolName" bun:"tool_name,type:VARCHAR(100),notnull"`
ToolParams map[string]any `json:"toolParams" bun:"tool_params,type:JSONB,notnull,default:'{}'::jsonb"`
ToolParams map[string]any `json:"toolParams" bun:"tool_params,type:JSONB,notnull,default:'{}'"`
Confidence decimal.Decimal `json:"confidence" bun:"confidence,type:NUMERIC(5,4),notnull,default:0"`
Rationale string `json:"rationale" bun:"rationale,type:TEXT,notnull"`
Evidence []EvidenceRef `json:"evidence" bun:"evidence,type:JSONB,notnull,default:'[]'::jsonb"`
Evidence []EvidenceRef `json:"evidence" bun:"evidence,type:JSONB,notnull,default:'[]'"`
AutonomyTier AutonomyTier `json:"autonomyTier" bun:"autonomy_tier,type:agent_autonomy_tier_enum,notnull,default:'Propose'"`
Status ProposalStatus `json:"status" bun:"status,type:agent_proposal_status_enum,notnull,default:'Pending'"`

Expand Down
2 changes: 1 addition & 1 deletion services/tms/internal/core/domain/apikey/usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type UsageDaily struct {
OrganizationID pulid.ID `json:"organizationId" bun:"organization_id,type:VARCHAR(100),notnull"`
BusinessUnitID pulid.ID `json:"businessUnitId" bun:"business_unit_id,type:VARCHAR(100),notnull"`
UsageDate time.Time `json:"usageDate" bun:"usage_date,pk,type:date,notnull"`
RequestCount int64 `json:"requestCount" bun:"request_count,notnull,default:0"`
RequestCount int64 `json:"requestCount" bun:"request_count,notnull"`
}

func (u *UsageDaily) Validate(multiErr *errortypes.MultiError) {
Expand Down
12 changes: 6 additions & 6 deletions services/tms/internal/core/domain/audit/auditentry.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ type Entry struct {
BusinessUnitID pulid.ID `json:"businessUnitId" bun:"business_unit_id,type:VARCHAR(100),notnull"`
OrganizationID pulid.ID `json:"organizationId" bun:"organization_id,type:VARCHAR(100),notnull"`
Timestamp int64 `json:"timestamp" bun:"timestamp,notnull,default:extract(epoch from current_timestamp)::bigint"`
Changes map[string]any `json:"changes,omitempty" bun:"changes,type:JSONB,default:'{}'::jsonb"`
PreviousState map[string]any `json:"previousState,omitempty" bun:"previous_state,type:JSONB,default:'{}'::jsonb"`
CurrentState map[string]any `json:"currentState,omitempty" bun:"current_state,type:JSONB,default:'{}'::jsonb"`
Metadata map[string]any `json:"metadata,omitempty" bun:"metadata,type:JSONB,default:'{}'::jsonb"`
Changes map[string]any `json:"changes,omitempty" bun:"changes,type:JSONB,default:'{}'"`
PreviousState map[string]any `json:"previousState,omitempty" bun:"previous_state,type:JSONB,default:'{}'"`
CurrentState map[string]any `json:"currentState,omitempty" bun:"current_state,type:JSONB,default:'{}'"`
Metadata map[string]any `json:"metadata,omitempty" bun:"metadata,type:JSONB,default:'{}'"`
Resource permission.Resource `json:"resource" bun:"resource,type:VARCHAR(50),notnull"` // Should be the same as the resource in the permission service
Operation permission.Operation `json:"operation" bun:"operation,type:VARCHAR(50),notnull"` // Should be the same as the operation in the permission service
ResourceID string `json:"resourceId" bun:"resource_id,type:VARCHAR(100),notnull"`
Expand All @@ -48,8 +48,8 @@ type Entry struct {
Comment string `json:"comment,omitempty" bun:"comment,type:TEXT"`
IPAddress string `json:"ipAddress,omitempty" bun:"ip_address,type:VARCHAR(45)"` // IPv6 addresses need space
Category Category `json:"category" bun:"category,type:audit_category_enum,notnull,default:'System'"`
SensitiveData bool `json:"sensitiveData" bun:"sensitive_data,notnull,default:false"`
Critical bool `json:"critical" bun:"critical,notnull,default:false"`
SensitiveData bool `json:"sensitiveData" bun:"sensitive_data,notnull"`
Critical bool `json:"critical" bun:"critical,notnull"`
User *tenant.User `json:"user,omitempty" bun:"rel:belongs-to,join:user_id=id"`
APIKey *apikey.Key `json:"apiKey,omitempty" bun:"rel:belongs-to,join:api_key_id=id"`
Organization *tenant.Organization `json:"-" bun:"rel:belongs-to,join:organization_id=id"`
Expand Down
2 changes: 1 addition & 1 deletion services/tms/internal/core/domain/audit/dlqentry.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type DLQEntry struct {
OriginalEntryID pulid.ID `json:"originalEntryId" bun:"original_entry_id,type:VARCHAR(100),notnull"`
EntryData map[string]any `json:"entryData" bun:"entry_data,type:JSONB,notnull"`
FailureTime int64 `json:"failureTime" bun:"failure_time,notnull"`
RetryCount int `json:"retryCount" bun:"retry_count,notnull,default:0"`
RetryCount int `json:"retryCount" bun:"retry_count,notnull"`
LastError string `json:"lastError" bun:"last_error,type:TEXT"`
NextRetryAt int64 `json:"nextRetryAt" bun:"next_retry_at"`
Status DLQStatus `json:"status" bun:"status,type:VARCHAR(20),notnull,default:'pending'"`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type BankReceipt struct {
ExceptionReason string `json:"exceptionReason" bun:"exception_reason,type:TEXT,nullzero"`
CreatedByID pulid.ID `json:"createdById" bun:"created_by_id,type:VARCHAR(100),notnull"`
UpdatedByID pulid.ID `json:"updatedById" bun:"updated_by_id,type:VARCHAR(100),nullzero"`
Version int64 `json:"version" bun:"version,type:BIGINT,notnull,default:0"`
Version int64 `json:"version" bun:"version,type:BIGINT,notnull"`
CreatedAt int64 `json:"createdAt" bun:"created_at,type:BIGINT,notnull,default:extract(epoch from current_timestamp)::bigint"`
UpdatedAt int64 `json:"updatedAt" bun:"updated_at,type:BIGINT,notnull,default:extract(epoch from current_timestamp)::bigint"`
}
Expand Down
Loading
Loading