@memberjunction/sqlglot-ts 5.40.2 → 5.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/MJPostgresTranspiler.d.ts +66 -0
- package/dist/MJPostgresTranspiler.d.ts.map +1 -0
- package/dist/MJPostgresTranspiler.js +126 -0
- package/dist/MJPostgresTranspiler.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/python/mj_postgres.py +1571 -0
- package/src/python/test_mj_postgres.py +573 -0
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Regression tests for the MJ PostgreSQL sqlglot dialect (mj_postgres.py).
|
|
3
|
+
|
|
4
|
+
Plain-assert tests (no pytest dependency). Run with the project venv:
|
|
5
|
+
python3 packages/SQLGlotTS/src/python/test_mj_postgres.py
|
|
6
|
+
Exits non-zero on first failure.
|
|
7
|
+
"""
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
12
|
+
from mj_postgres import mj_transpile # noqa: E402
|
|
13
|
+
|
|
14
|
+
_failures = 0
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def check(name, sql, must_contain=(), must_not_contain=(), expect_unhandled=0, extra_bit_cols=None):
|
|
18
|
+
global _failures
|
|
19
|
+
import os as _os, json as _json
|
|
20
|
+
_prev = _os.environ.get("MJ_EXTRA_BIT_COLS")
|
|
21
|
+
if extra_bit_cols is not None:
|
|
22
|
+
_os.environ["MJ_EXTRA_BIT_COLS"] = _json.dumps(extra_bit_cols)
|
|
23
|
+
try:
|
|
24
|
+
r = mj_transpile(sql)
|
|
25
|
+
finally:
|
|
26
|
+
if extra_bit_cols is not None:
|
|
27
|
+
if _prev is None:
|
|
28
|
+
_os.environ.pop("MJ_EXTRA_BIT_COLS", None)
|
|
29
|
+
else:
|
|
30
|
+
_os.environ["MJ_EXTRA_BIT_COLS"] = _prev
|
|
31
|
+
joined = "\n".join(r["sql"])
|
|
32
|
+
# Whitespace-insensitive substring match so assertions survive pretty-printing
|
|
33
|
+
# (sqlglot may wrap a statement across lines, e.g. `DEFAULT (\n CURRENT_USER\n)`).
|
|
34
|
+
import re as _re2
|
|
35
|
+
nows = _re2.sub(r"\s+", "", joined)
|
|
36
|
+
errs = []
|
|
37
|
+
for s in must_contain:
|
|
38
|
+
if _re2.sub(r"\s+", "", s) not in nows:
|
|
39
|
+
errs.append(f"missing {s!r}")
|
|
40
|
+
for s in must_not_contain:
|
|
41
|
+
if _re2.sub(r"\s+", "", s) in nows:
|
|
42
|
+
errs.append(f"should not contain {s!r}")
|
|
43
|
+
if len(r["unhandled"]) != expect_unhandled:
|
|
44
|
+
errs.append(f"unhandled={len(r['unhandled'])} (expected {expect_unhandled}): {r['unhandled']}")
|
|
45
|
+
if errs:
|
|
46
|
+
_failures += 1
|
|
47
|
+
print(f"FAIL {name}")
|
|
48
|
+
for e in errs:
|
|
49
|
+
print(f" {e}")
|
|
50
|
+
print(f" output:\n{joined}\n")
|
|
51
|
+
else:
|
|
52
|
+
print(f"ok {name}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# --- AST type / function / boolean encoding ---------------------------------
|
|
56
|
+
check("type mappings",
|
|
57
|
+
"CREATE TABLE ${flyway:defaultSchema}.Foo (ID UNIQUEIDENTIFIER NOT NULL, Notes NVARCHAR(MAX) NULL, Name NVARCHAR(50));",
|
|
58
|
+
must_contain=['"ID" UUID', '"Notes" TEXT', '"Name" VARCHAR(50)'],
|
|
59
|
+
must_not_contain=["NVARCHAR", "VARCHAR(MAX)", "UNIQUEIDENTIFIER"])
|
|
60
|
+
|
|
61
|
+
check("function + boolean default rewrite",
|
|
62
|
+
"CREATE TABLE ${flyway:defaultSchema}.Foo (ID UNIQUEIDENTIFIER DEFAULT NEWSEQUENTIALID(), IsActive BIT NOT NULL DEFAULT 1, IsX BIT DEFAULT 0, C DATETIMEOFFSET DEFAULT GETUTCDATE());",
|
|
63
|
+
must_contain=["GEN_RANDOM_UUID()", "BOOLEAN NOT NULL DEFAULT TRUE", "DEFAULT FALSE", "TIMESTAMPTZ", "NOW()"],
|
|
64
|
+
must_not_contain=["NEWSEQUENTIALID", "GETUTCDATE", "DEFAULT 1", "DEFAULT 0"])
|
|
65
|
+
|
|
66
|
+
check("flyway macro preserved verbatim, identifiers quoted",
|
|
67
|
+
"ALTER TABLE ${flyway:defaultSchema}.APIKey ADD KeyPrefix NVARCHAR(20) NULL;",
|
|
68
|
+
must_contain=['${flyway:defaultSchema}."APIKey"', '"KeyPrefix" VARCHAR(20)'],
|
|
69
|
+
must_not_contain=['"${flyway', "__mj_flyway"])
|
|
70
|
+
|
|
71
|
+
# --- Column-level FK: FOREIGN KEY REFERENCES → bare REFERENCES ---------------
|
|
72
|
+
check("inline column FK drops FOREIGN KEY keyword (PG column constraint)",
|
|
73
|
+
"ALTER TABLE ${flyway:defaultSchema}.AIAgentRequest ADD "
|
|
74
|
+
"RequestTypeID UNIQUEIDENTIFIER NULL CONSTRAINT FK_x FOREIGN KEY "
|
|
75
|
+
"REFERENCES ${flyway:defaultSchema}.AIAgentRequestType(ID), "
|
|
76
|
+
"Priority INT NOT NULL CONSTRAINT DF_p DEFAULT 50;",
|
|
77
|
+
must_contain=['CONSTRAINT "FK_x" REFERENCES ${flyway:defaultSchema}."AIAgentRequestType" ("ID")',
|
|
78
|
+
'ADD COLUMN "Priority" INT NOT NULL', 'DEFAULT 50'],
|
|
79
|
+
must_not_contain=["FOREIGN KEY"])
|
|
80
|
+
|
|
81
|
+
# --- Table-level constraint mixed into ADD list (sqlglot mis-parse) ----------
|
|
82
|
+
check("table CONSTRAINT in multi-item ADD → ADD CONSTRAINT (not a bogus column)",
|
|
83
|
+
"ALTER TABLE ${flyway:defaultSchema}.AIPrompt ADD "
|
|
84
|
+
"AssistantPrefill NVARCHAR(MAX) NULL, "
|
|
85
|
+
"PrefillFallbackMode NVARCHAR(20) NOT NULL DEFAULT 'Ignore', "
|
|
86
|
+
"CONSTRAINT CK_AIPrompt_PrefillFallbackMode CHECK (PrefillFallbackMode IN ('Ignore','None'));",
|
|
87
|
+
must_contain=['ADD COLUMN "AssistantPrefill" TEXT',
|
|
88
|
+
'ADD COLUMN "PrefillFallbackMode" VARCHAR(20) NOT NULL DEFAULT \'Ignore\'',
|
|
89
|
+
'ADD CONSTRAINT "CK_AIPrompt_PrefillFallbackMode" CHECK'],
|
|
90
|
+
must_not_contain=['ADD COLUMN "CONSTRAINT"', "USERDEFINED"])
|
|
91
|
+
|
|
92
|
+
# --- FLOAT precision: bare/wide FLOAT → DOUBLE PRECISION, narrow → REAL -------
|
|
93
|
+
check("FLOAT widths map to DOUBLE PRECISION / REAL",
|
|
94
|
+
"CREATE TABLE ${flyway:defaultSchema}.Foo (A FLOAT NULL, B FLOAT(53) NULL, C FLOAT(24) NULL);",
|
|
95
|
+
must_contain=['"A" DOUBLE PRECISION', '"B" DOUBLE PRECISION', '"C" REAL'],
|
|
96
|
+
must_not_contain=['"A" REAL', "FLOAT(53)", "FLOAT(24)"])
|
|
97
|
+
|
|
98
|
+
# --- Datetime types are tz-aware (match oracle `timestamp with time zone`) ----
|
|
99
|
+
check("DATETIME/DATETIME2/SMALLDATETIME → TIMESTAMPTZ",
|
|
100
|
+
"CREATE TABLE ${flyway:defaultSchema}.Foo (A DATETIME NULL, B DATETIME2 NULL, C SMALLDATETIME NULL, D DATETIMEOFFSET NULL);",
|
|
101
|
+
must_contain=['"A" TIMESTAMPTZ', '"B" TIMESTAMPTZ', '"C" TIMESTAMPTZ', '"D" TIMESTAMPTZ'],
|
|
102
|
+
must_not_contain=["TIMESTAMP NULL", "TIMESTAMP,", "TIMESTAMP)"])
|
|
103
|
+
|
|
104
|
+
# --- USER_NAME() in a DEFAULT → CURRENT_USER (else CREATE TABLE fails + cascades) --
|
|
105
|
+
check("USER_NAME()/SUSER_NAME() in column default → CURRENT_USER",
|
|
106
|
+
"CREATE TABLE ${flyway:defaultSchema}.ConversationDetail (Role NVARCHAR(20) NOT NULL DEFAULT (USER_NAME()), Owner NVARCHAR(20) NULL DEFAULT (SUSER_NAME()));",
|
|
107
|
+
must_contain=["DEFAULT (CURRENT_USER)"],
|
|
108
|
+
must_not_contain=["USER_NAME(", "SUSER_NAME("])
|
|
109
|
+
|
|
110
|
+
# --- boolean = integer CHECK (type-aware) ------------------------------------
|
|
111
|
+
check("BIT column CHECK = 1/0 → = TRUE/FALSE; integer column untouched; paren-wrapped",
|
|
112
|
+
"CREATE TABLE ${flyway:defaultSchema}.Foo (IsActive BIT NOT NULL, Priority INT NOT NULL, "
|
|
113
|
+
"CONSTRAINT CK_a CHECK (IsActive = (1)), CONSTRAINT CK_p CHECK (Priority = 1));",
|
|
114
|
+
must_contain=['CHECK ("IsActive" = TRUE)', 'CHECK ("Priority" = 1)'],
|
|
115
|
+
must_not_contain=['"IsActive" = 1', '"IsActive" = (1)'])
|
|
116
|
+
|
|
117
|
+
check("boolean CHECK in SEPARATE ALTER resolves via file-level BIT registry",
|
|
118
|
+
"CREATE TABLE ${flyway:defaultSchema}.AIAgent (EnableContextCompression BIT NOT NULL);\nGO\n"
|
|
119
|
+
"ALTER TABLE ${flyway:defaultSchema}.AIAgent ADD CONSTRAINT CK_x CHECK (EnableContextCompression = (0));",
|
|
120
|
+
must_contain=['CHECK ("EnableContextCompression" = FALSE)'],
|
|
121
|
+
must_not_contain=['= (0)', '= 0)'])
|
|
122
|
+
|
|
123
|
+
check("UPDATE SET + WHERE on BIT column → TRUE/FALSE via file-level registry; INT column untouched",
|
|
124
|
+
"CREATE TABLE ${flyway:defaultSchema}.EntityField (IsNameField BIT NOT NULL, "
|
|
125
|
+
"AutoUpdateIsNameField BIT NOT NULL, Sequence INT NOT NULL);\nGO\n"
|
|
126
|
+
"UPDATE ${flyway:defaultSchema}.EntityField SET IsNameField = 1 "
|
|
127
|
+
"WHERE AutoUpdateIsNameField = 1 AND Sequence = 1;\nGO\n"
|
|
128
|
+
"UPDATE ${flyway:defaultSchema}.EntityField SET IsNameField = 0 WHERE AutoUpdateIsNameField = 0;",
|
|
129
|
+
must_contain=['SET "IsNameField" = TRUE', '"AutoUpdateIsNameField" = TRUE',
|
|
130
|
+
'SET "IsNameField" = FALSE', '"AutoUpdateIsNameField" = FALSE',
|
|
131
|
+
'"Sequence" = 1'],
|
|
132
|
+
must_not_contain=['"IsNameField" = 1', '"IsNameField" = 0',
|
|
133
|
+
'"AutoUpdateIsNameField" = 1', '"AutoUpdateIsNameField" = 0'])
|
|
134
|
+
|
|
135
|
+
check("DELETE WHERE on BIT column → TRUE/FALSE; integer column untouched",
|
|
136
|
+
"CREATE TABLE ${flyway:defaultSchema}.Foo (IsActive BIT NOT NULL, Priority INT NOT NULL);\nGO\n"
|
|
137
|
+
"DELETE FROM ${flyway:defaultSchema}.Foo WHERE IsActive = 0 AND Priority = 1;",
|
|
138
|
+
must_contain=['"IsActive" = FALSE', '"Priority" = 1'],
|
|
139
|
+
must_not_contain=['"IsActive" = 0'])
|
|
140
|
+
|
|
141
|
+
check("BIT column with <> / != → TRUE/FALSE (not just =); both operand orders",
|
|
142
|
+
"CREATE TABLE ${flyway:defaultSchema}.Foo (IsActive BIT NOT NULL);\nGO\n"
|
|
143
|
+
"UPDATE ${flyway:defaultSchema}.Foo SET IsActive = 1 WHERE IsActive <> 0;\nGO\n"
|
|
144
|
+
"DELETE FROM ${flyway:defaultSchema}.Foo WHERE 1 <> IsActive;",
|
|
145
|
+
must_contain=['"IsActive" <> FALSE', '"IsActive" = TRUE'],
|
|
146
|
+
must_not_contain=['<> 0', '1 <>', '<> 1'])
|
|
147
|
+
|
|
148
|
+
check("BIT column IN (0, 1) → IN (FALSE, TRUE); integer column's IN untouched",
|
|
149
|
+
"CREATE TABLE ${flyway:defaultSchema}.Foo (IsActive BIT NOT NULL, Priority INT NOT NULL);\nGO\n"
|
|
150
|
+
"DELETE FROM ${flyway:defaultSchema}.Foo WHERE IsActive IN (0, 1) AND Priority IN (0, 1);",
|
|
151
|
+
must_contain=['"IsActive" IN (FALSE, TRUE)', '"Priority" IN (0, 1)'],
|
|
152
|
+
must_not_contain=['"IsActive" IN (0, 1)'])
|
|
153
|
+
|
|
154
|
+
check("cross-file BIT registry (MJ_EXTRA_BIT_COLS) coerces UPDATE to a table NOT declared in-file",
|
|
155
|
+
"UPDATE ${flyway:defaultSchema}.EntityField SET IsNameField = 1 WHERE AutoUpdateIsNameField = 1;",
|
|
156
|
+
must_contain=['SET "IsNameField" = TRUE', '"AutoUpdateIsNameField" = TRUE'],
|
|
157
|
+
must_not_contain=['= 1'],
|
|
158
|
+
extra_bit_cols=[["entityfield", "isnamefield"], ["entityfield", "autoupdateisnamefield"]])
|
|
159
|
+
|
|
160
|
+
# Residual self-check: a bool-vs-int shape the coercion pass does NOT model (CASE switch
|
|
161
|
+
# form, `CASE col WHEN 1`) must be surfaced as a gap, never emitted as `boolean = integer`.
|
|
162
|
+
check("unmodeled bool-vs-int shape is reported as a gap, not silently emitted",
|
|
163
|
+
"CREATE TABLE ${flyway:defaultSchema}.Foo (IsActive BIT NOT NULL, Label NVARCHAR(10));\nGO\n"
|
|
164
|
+
"UPDATE ${flyway:defaultSchema}.Foo SET Label = CASE IsActive WHEN 1 THEN 'y' ELSE 'n' END;",
|
|
165
|
+
must_not_contain=['WHEN 1 THEN'],
|
|
166
|
+
expect_unhandled=1)
|
|
167
|
+
|
|
168
|
+
check("SET NOCOUNT/XACT_ABORT/QUOTED_IDENTIFIER batch-control dropped",
|
|
169
|
+
"SET NOCOUNT ON;\nSET XACT_ABORT ON;\nSET QUOTED_IDENTIFIER ON;\nCREATE TABLE ${flyway:defaultSchema}.Foo (ID UNIQUEIDENTIFIER NOT NULL);",
|
|
170
|
+
must_contain=['CREATE TABLE'],
|
|
171
|
+
must_not_contain=["NOCOUNT", "XACT_ABORT", "QUOTED_IDENTIFIER"])
|
|
172
|
+
|
|
173
|
+
# --- string concat 'a' + col → 'a' || col ------------------------------------
|
|
174
|
+
check("string concat + → || (numeric + untouched)",
|
|
175
|
+
"UPDATE ${flyway:defaultSchema}.Entity SET Name = 'MJ: ' + Name WHERE Seq = Seq + 1;",
|
|
176
|
+
must_contain=["'MJ: ' || ", '"Seq" + 1'],
|
|
177
|
+
must_not_contain=["'MJ: ' + "])
|
|
178
|
+
|
|
179
|
+
# --- table-level ISJSON CHECK dropped (column-level already covered) ---------
|
|
180
|
+
check("table-level ISJSON CHECK dropped; sibling constraint kept",
|
|
181
|
+
"CREATE TABLE ${flyway:defaultSchema}.Foo (Data NVARCHAR(MAX) NULL, X INT NULL, "
|
|
182
|
+
"CONSTRAINT CK_json CHECK (ISJSON(Data) > 0), CONSTRAINT CK_x CHECK (X > 0));",
|
|
183
|
+
must_contain=['CONSTRAINT "CK_x" CHECK'],
|
|
184
|
+
must_not_contain=["ISJSON", "CK_json"])
|
|
185
|
+
|
|
186
|
+
check("ALTER ADD with only an ISJSON CHECK → whole ALTER dropped",
|
|
187
|
+
"ALTER TABLE ${flyway:defaultSchema}.Foo ADD CONSTRAINT CK_json CHECK (ISJSON(Data) > 0);",
|
|
188
|
+
must_contain=[],
|
|
189
|
+
must_not_contain=["ISJSON", "ALTER TABLE"])
|
|
190
|
+
|
|
191
|
+
# --- Procedural T-SQL glue is reported as unhandled, never emitted as $v SQL --
|
|
192
|
+
check("DECLARE / SELECT @v= / IF @v EXEC dropped to unhandled (real DDL kept)",
|
|
193
|
+
"ALTER TABLE ${flyway:defaultSchema}.AIAgentRequest ADD CONSTRAINT CK_S CHECK (Status IN ('A','B'));\n"
|
|
194
|
+
"DECLARE @ConstraintName AS VARCHAR(200);\n"
|
|
195
|
+
"SELECT @ConstraintName = cc.CONSTRAINT_NAME FROM INFORMATION_SCHEMA.CHECK_CONSTRAINTS cc WHERE cc.x = 1;\n"
|
|
196
|
+
"IF @ConstraintName IS NOT NULL EXEC('ALTER TABLE y DROP CONSTRAINT ' + @ConstraintName);",
|
|
197
|
+
must_contain=['ADD CONSTRAINT "CK_S" CHECK'],
|
|
198
|
+
must_not_contain=["$ConstraintName", "DECLARE", "@ConstraintName", "EXEC("],
|
|
199
|
+
expect_unhandled=3)
|
|
200
|
+
|
|
201
|
+
# --- Transform 1: sp_addextendedproperty → COMMENT ON -----------------------
|
|
202
|
+
check("sp_addextendedproperty COLUMN → COMMENT ON COLUMN",
|
|
203
|
+
"""EXEC sp_addextendedproperty
|
|
204
|
+
@name = N'MS_Description', @value = N'A short preview (it''s safe)',
|
|
205
|
+
@level0type = N'SCHEMA', @level0name = N'${flyway:defaultSchema}',
|
|
206
|
+
@level1type = N'TABLE', @level1name = N'APIKey',
|
|
207
|
+
@level2type = N'COLUMN', @level2name = N'KeyPrefix';""",
|
|
208
|
+
must_contain=['COMMENT ON COLUMN ${flyway:defaultSchema}."APIKey"."KeyPrefix" IS', "it''s safe"],
|
|
209
|
+
must_not_contain=["sp_addextendedproperty", "@value"])
|
|
210
|
+
|
|
211
|
+
check("sp_addextendedproperty TABLE-level → COMMENT ON TABLE",
|
|
212
|
+
"""EXEC sp_addextendedproperty
|
|
213
|
+
@name=N'MS_Description', @value=N'A table',
|
|
214
|
+
@level0type=N'SCHEMA', @level0name=N'${flyway:defaultSchema}',
|
|
215
|
+
@level1type=N'TABLE', @level1name=N'Widget';""",
|
|
216
|
+
must_contain=['COMMENT ON TABLE ${flyway:defaultSchema}."Widget" IS'],
|
|
217
|
+
must_not_contain=["sp_addextendedproperty"])
|
|
218
|
+
|
|
219
|
+
# --- Envelope boundary: semicolon INSIDE an sp_addextendedproperty value -----
|
|
220
|
+
# A `;` in the description must NOT terminate the envelope early (else it cuts the
|
|
221
|
+
# statement and corrupts every later gap boundary, dropping real DDL like the table after).
|
|
222
|
+
check("sp_addextendedproperty with ';' in the value doesn't break following DDL",
|
|
223
|
+
"EXEC sp_addextendedproperty @name=N'MS_Description', "
|
|
224
|
+
"@value=N'Applies to all apps; when set, scoped to one.', "
|
|
225
|
+
"@level0type=N'SCHEMA', @level0name=N'${flyway:defaultSchema}', "
|
|
226
|
+
"@level1type=N'TABLE', @level1name=N'Foo', @level2type=N'COLUMN', @level2name=N'Bar';\n"
|
|
227
|
+
"CREATE TABLE ${flyway:defaultSchema}.AfterComment (ID UNIQUEIDENTIFIER NOT NULL);",
|
|
228
|
+
must_contain=['COMMENT ON COLUMN', 'Applies to all apps; when set, scoped to one.',
|
|
229
|
+
'CREATE TABLE ${flyway:defaultSchema}."AfterComment"'],
|
|
230
|
+
must_not_contain=["sp_addextendedproperty"])
|
|
231
|
+
|
|
232
|
+
# --- Transform 2: IF [NOT] EXISTS(...) BEGIN ... END → DO block --------------
|
|
233
|
+
check("idempotent IF NOT EXISTS BEGIN INSERT END → DO block (non-metadata table)",
|
|
234
|
+
"""IF NOT EXISTS (SELECT 1 FROM ${flyway:defaultSchema}.Widget WHERE ID = 'abc')
|
|
235
|
+
BEGIN
|
|
236
|
+
INSERT INTO ${flyway:defaultSchema}.Widget (ID, Name) VALUES ('abc', 'KeyPrefix');
|
|
237
|
+
END""",
|
|
238
|
+
must_contain=["DO $$", "IF NOT EXISTS (", "THEN", "END IF;", "END $$;",
|
|
239
|
+
'INSERT INTO ${flyway:defaultSchema}."Widget"', '"ID"', '"Name"'],
|
|
240
|
+
must_not_contain=["BEGIN\n INSERT", "sp_addext"])
|
|
241
|
+
|
|
242
|
+
check("entity-metadata seed (EntityField) is KEPT + transpiled to an idempotent DO block (PG CodeGen does not recreate entity metadata)",
|
|
243
|
+
"""IF NOT EXISTS (SELECT 1 FROM [${flyway:defaultSchema}].[EntityField] WHERE [ID] = 'abc')
|
|
244
|
+
BEGIN
|
|
245
|
+
INSERT INTO [${flyway:defaultSchema}].[EntityField] ([ID], [Name]) VALUES ('abc', 'X');
|
|
246
|
+
END""",
|
|
247
|
+
must_contain=['DO $$', 'INSERT INTO ${flyway:defaultSchema}."EntityField"', "VALUES ('abc', 'X')"],
|
|
248
|
+
must_not_contain=["[EntityField]"])
|
|
249
|
+
|
|
250
|
+
# --- Transform: standalone DEFAULT constraint → ALTER COLUMN SET DEFAULT ------
|
|
251
|
+
# sqlglot can't parse `ADD CONSTRAINT … DEFAULT (…) FOR [col]`; these carry ~all of
|
|
252
|
+
# MJ's column defaults. Must emit PG `ALTER COLUMN … SET DEFAULT`, converting the expr.
|
|
253
|
+
check("standalone newsequentialid() default → ALTER COLUMN SET DEFAULT gen_random_uuid()",
|
|
254
|
+
"ALTER TABLE [${flyway:defaultSchema}].[ApplicationEntity] ADD CONSTRAINT [DF_AE_ID] DEFAULT (newsequentialid()) FOR [ID];",
|
|
255
|
+
must_contain=['ALTER TABLE ${flyway:defaultSchema}."ApplicationEntity" ALTER COLUMN "ID" SET DEFAULT GEN_RANDOM_UUID()'],
|
|
256
|
+
must_not_contain=["ADD CONSTRAINT", "newsequentialid", "FOR ["])
|
|
257
|
+
|
|
258
|
+
check("standalone getutcdate() default → SET DEFAULT NOW()",
|
|
259
|
+
"ALTER TABLE [__mj].[RowLevelSecurityFilter] ADD CONSTRAINT [DF_x] DEFAULT (getutcdate()) FOR [__mj_CreatedAt];",
|
|
260
|
+
must_contain=['ALTER TABLE "__mj"."RowLevelSecurityFilter" ALTER COLUMN "__mj_CreatedAt" SET DEFAULT NOW()'],
|
|
261
|
+
must_not_contain=["ADD CONSTRAINT", "getutcdate", "FOR ["])
|
|
262
|
+
|
|
263
|
+
check("standalone BIT default ((1))/((0)) → TRUE/FALSE via bit registry; non-bit numeric kept",
|
|
264
|
+
"CREATE TABLE ${flyway:defaultSchema}.ApplicationEntity (DefaultForNewUser BIT NOT NULL, Sequence INT NOT NULL);\nGO\n"
|
|
265
|
+
"ALTER TABLE [${flyway:defaultSchema}].[ApplicationEntity] ADD CONSTRAINT [DF_d] DEFAULT ((1)) FOR [DefaultForNewUser];\n"
|
|
266
|
+
"ALTER TABLE [${flyway:defaultSchema}].[ApplicationEntity] ADD CONSTRAINT [DF_s] DEFAULT ((0)) FOR [Sequence];",
|
|
267
|
+
must_contain=['ALTER COLUMN "DefaultForNewUser" SET DEFAULT TRUE',
|
|
268
|
+
'ALTER COLUMN "Sequence" SET DEFAULT 0'],
|
|
269
|
+
must_not_contain=["ADD CONSTRAINT", "DEFAULT ((1))", "SET DEFAULT 1"])
|
|
270
|
+
|
|
271
|
+
check("standalone string default preserved",
|
|
272
|
+
"ALTER TABLE [__mj].[Foo] ADD CONSTRAINT [DF_st] DEFAULT (N'Active') FOR [Status];",
|
|
273
|
+
must_contain=['ALTER COLUMN "Status" SET DEFAULT \'Active\''],
|
|
274
|
+
must_not_contain=["ADD CONSTRAINT", "N'Active'"])
|
|
275
|
+
|
|
276
|
+
# --- Transform: ALTER COLUMN type/nullability change → PG TYPE + SET/DROP NOT NULL --
|
|
277
|
+
check("ALTER COLUMN type + NOT NULL → TYPE change + SET NOT NULL",
|
|
278
|
+
"ALTER TABLE [__mj].[Foo] ALTER COLUMN [Name] NVARCHAR(200) NOT NULL;",
|
|
279
|
+
must_contain=['ALTER TABLE "__mj"."Foo" ALTER COLUMN "Name" TYPE VARCHAR(200)',
|
|
280
|
+
'ALTER COLUMN "Name" SET NOT NULL'],
|
|
281
|
+
must_not_contain=["ALTER COLUMN [Name]", "NVARCHAR", "DROP NOT NULL"])
|
|
282
|
+
|
|
283
|
+
check("ALTER COLUMN NVARCHAR(MAX) NULL → TYPE TEXT + DROP NOT NULL; flyway macro kept",
|
|
284
|
+
"ALTER TABLE [${flyway:defaultSchema}].[Bar] ALTER COLUMN [Note] NVARCHAR(MAX) NULL;",
|
|
285
|
+
must_contain=['ALTER TABLE ${flyway:defaultSchema}."Bar" ALTER COLUMN "Note" TYPE TEXT',
|
|
286
|
+
'ALTER COLUMN "Note" DROP NOT NULL'],
|
|
287
|
+
must_not_contain=["NVARCHAR", "SET NOT NULL", "__mj_flyway"])
|
|
288
|
+
|
|
289
|
+
check("ALTER COLUMN with a leading comment (own GO batch) still transpiles",
|
|
290
|
+
"-- Phase 4 — Email becomes optional.\n"
|
|
291
|
+
"ALTER TABLE ${flyway:defaultSchema}.MagicLinkInvite ALTER COLUMN Email NVARCHAR(255) NULL;",
|
|
292
|
+
must_contain=['ALTER TABLE ${flyway:defaultSchema}."MagicLinkInvite" ALTER COLUMN "Email" TYPE VARCHAR(255)',
|
|
293
|
+
'DROP NOT NULL'],
|
|
294
|
+
must_not_contain=["NVARCHAR", "Phase 4"])
|
|
295
|
+
|
|
296
|
+
# --- Transform: sp_dropextendedproperty → COMMENT ON … IS NULL ---------------
|
|
297
|
+
check("sp_dropextendedproperty COLUMN → COMMENT ON COLUMN … IS NULL",
|
|
298
|
+
"EXEC sp_dropextendedproperty @name=N'MS_Description', "
|
|
299
|
+
"@level0type=N'SCHEMA', @level0name=N'${flyway:defaultSchema}', "
|
|
300
|
+
"@level1type=N'TABLE', @level1name=N'Foo', @level2type=N'COLUMN', @level2name=N'Bar';",
|
|
301
|
+
must_contain=['COMMENT ON COLUMN ${flyway:defaultSchema}."Foo"."Bar" IS NULL'],
|
|
302
|
+
must_not_contain=["sp_dropextendedproperty", "EXEC"])
|
|
303
|
+
|
|
304
|
+
check("sp_dropextendedproperty TABLE-level → COMMENT ON TABLE … IS NULL",
|
|
305
|
+
"EXEC sp_dropextendedproperty @name=N'MS_Description', "
|
|
306
|
+
"@level0type=N'SCHEMA', @level0name=N'${flyway:defaultSchema}', "
|
|
307
|
+
"@level1type=N'TABLE', @level1name=N'Widget';",
|
|
308
|
+
must_contain=['COMMENT ON TABLE ${flyway:defaultSchema}."Widget" IS NULL'],
|
|
309
|
+
must_not_contain=["sp_dropextendedproperty"])
|
|
310
|
+
|
|
311
|
+
# --- GO batches + mixed envelope ordering -----------------------------------
|
|
312
|
+
check("GO batches: DDL + comment + idempotent insert, all clean, order preserved",
|
|
313
|
+
"""ALTER TABLE ${flyway:defaultSchema}.APIKey ADD KeyPrefix NVARCHAR(20) NULL;
|
|
314
|
+
GO
|
|
315
|
+
EXEC sp_addextendedproperty @name=N'MS_Description', @value=N'x',
|
|
316
|
+
@level0type=N'SCHEMA', @level0name=N'${flyway:defaultSchema}',
|
|
317
|
+
@level1type=N'TABLE', @level1name=N'APIKey', @level2type=N'COLUMN', @level2name=N'KeyPrefix';
|
|
318
|
+
GO""",
|
|
319
|
+
must_contain=["ADD COLUMN", "COMMENT ON COLUMN"],
|
|
320
|
+
must_not_contain=["GO", "sp_addextendedproperty"])
|
|
321
|
+
|
|
322
|
+
# --- extended-property drop guard (SQL-Server-only) -------------------------
|
|
323
|
+
# IF EXISTS(sys.extended_properties …) BEGIN sp_dropextendedproperty END → dropped;
|
|
324
|
+
# the re-add that follows still emits its COMMENT ON. (Magic_Link TokenHash case.)
|
|
325
|
+
check("IF EXISTS(sys.extended_properties) BEGIN sp_dropextendedproperty END guard → dropped; re-add kept",
|
|
326
|
+
"""IF EXISTS (SELECT 1 FROM sys.extended_properties
|
|
327
|
+
WHERE major_id = OBJECT_ID('${flyway:defaultSchema}.MagicLinkInvite')
|
|
328
|
+
AND name = 'MS_Description')
|
|
329
|
+
BEGIN
|
|
330
|
+
EXEC sp_dropextendedproperty @name=N'MS_Description',
|
|
331
|
+
@level0type=N'SCHEMA', @level0name=N'${flyway:defaultSchema}',
|
|
332
|
+
@level1type=N'TABLE', @level1name=N'MagicLinkInvite',
|
|
333
|
+
@level2type=N'COLUMN', @level2name=N'TokenHash';
|
|
334
|
+
END;
|
|
335
|
+
GO
|
|
336
|
+
EXEC sp_addextendedproperty @name=N'MS_Description', @value=N'base64url hash',
|
|
337
|
+
@level0type=N'SCHEMA', @level0name=N'${flyway:defaultSchema}',
|
|
338
|
+
@level1type=N'TABLE', @level1name=N'MagicLinkInvite', @level2type=N'COLUMN', @level2name=N'TokenHash';
|
|
339
|
+
GO""",
|
|
340
|
+
must_contain=['COMMENT ON COLUMN ${flyway:defaultSchema}."MagicLinkInvite"."TokenHash" IS \'base64url hash\''],
|
|
341
|
+
must_not_contain=["sys.extended_properties", "sp_dropextendedproperty", "DO $$"])
|
|
342
|
+
|
|
343
|
+
# --- data-DML: UPDATE…FROM self-alias + ISJSON + JSON_VALUE (Backfill_UserView) ----
|
|
344
|
+
check("UPDATE alias…FROM target alias JOIN → PG UPDATE target AS alias…FROM other; ISJSON→IS JSON; JSON_VALUE→jsonb ->>",
|
|
345
|
+
"""UPDATE uv SET uv.ViewTypeID = vt.ID
|
|
346
|
+
FROM ${flyway:defaultSchema}.UserView AS uv
|
|
347
|
+
INNER JOIN ${flyway:defaultSchema}.ViewType AS vt ON vt.Name = JSON_VALUE(uv.DisplayState, '$.defaultMode')
|
|
348
|
+
WHERE uv.ViewTypeID IS NULL AND ISJSON(uv.DisplayState) = 1;""",
|
|
349
|
+
must_contain=[
|
|
350
|
+
'UPDATE ${flyway:defaultSchema}."UserView" AS "uv"',
|
|
351
|
+
'SET "ViewTypeID" = "vt"."ID"',
|
|
352
|
+
'FROM ${flyway:defaultSchema}."ViewType" AS "vt"',
|
|
353
|
+
'("uv"."DisplayState")::jsonb ->> \'defaultMode\'',
|
|
354
|
+
'"uv"."DisplayState" IS JSON',
|
|
355
|
+
],
|
|
356
|
+
# NB: don't assert absence of 'ISJSON' here — whitespace-insensitive matching
|
|
357
|
+
# collapses the legitimate 'IS JSON' predicate to 'ISJSON'. The IS-JSON must_contain
|
|
358
|
+
# above already proves the rewrite.
|
|
359
|
+
must_not_contain=['JSON_EXTRACT_PATH_TEXT', 'UPDATE "uv" SET', 'SET "uv"."ViewTypeID"'])
|
|
360
|
+
|
|
361
|
+
check("flyway macro inside a string literal (Entity.SchemaName seed) is restored, not left as the sentinel",
|
|
362
|
+
"INSERT INTO ${flyway:defaultSchema}.[Entity] ([Name], [SchemaName]) "
|
|
363
|
+
"VALUES ('MJ: Magic Link Invites', '${flyway:defaultSchema}');",
|
|
364
|
+
must_contain=[
|
|
365
|
+
'INSERT INTO ${flyway:defaultSchema}."Entity"',
|
|
366
|
+
"VALUES ('MJ: Magic Link Invites', '${flyway:defaultSchema}')",
|
|
367
|
+
],
|
|
368
|
+
must_not_contain=['__mj_flyway_default_schema__'])
|
|
369
|
+
|
|
370
|
+
check("flyway macro as a schema qualifier embedded in a stored predicate string is restored",
|
|
371
|
+
"INSERT INTO ${flyway:defaultSchema}.[RowLevelSecurityFilter] ([FilterText]) "
|
|
372
|
+
"VALUES ('RoleID IN (SELECT RoleID FROM ${flyway:defaultSchema}.vwUserRoles "
|
|
373
|
+
"WHERE UserID = ''{{UserID}}'')');",
|
|
374
|
+
must_contain=['FROM ${flyway:defaultSchema}.vwUserRoles'],
|
|
375
|
+
must_not_contain=['__mj_flyway_default_schema__'])
|
|
376
|
+
|
|
377
|
+
check("flyway sentinel never leaks into a trailing comment either (final safety net)",
|
|
378
|
+
"ALTER TABLE ${flyway:defaultSchema}.[MagicLinkInviteApplication] "
|
|
379
|
+
"ADD [__mj_CreatedAt] DATETIMEOFFSET NULL "
|
|
380
|
+
"/* SQL text to add special date field __mj_CreatedAt to entity "
|
|
381
|
+
"${flyway:defaultSchema}.MagicLinkInviteApplication */;",
|
|
382
|
+
must_not_contain=['__mj_flyway_default_schema__'])
|
|
383
|
+
|
|
384
|
+
# --- extprop batches must not drop sibling statements (no pure-comment shortcut) ---
|
|
385
|
+
check("CREATE INDEX sharing a batch with an extprop is kept, both emitted",
|
|
386
|
+
"CREATE INDEX IDX_Foo_Bar ON [${flyway:defaultSchema}].[Foo] ([Bar]);\n"
|
|
387
|
+
"EXEC sp_addextendedproperty @name=N'MS_Description', @value=N'x', "
|
|
388
|
+
"@level0type=N'SCHEMA', @level0name=N'${flyway:defaultSchema}', "
|
|
389
|
+
"@level1type=N'TABLE', @level1name=N'Foo', @level2type=N'COLUMN', @level2name=N'Bar';",
|
|
390
|
+
must_contain=['CREATE INDEX "IDX_Foo_Bar"',
|
|
391
|
+
'COMMENT ON COLUMN ${flyway:defaultSchema}."Foo"."Bar"'],
|
|
392
|
+
must_not_contain=["sp_addextendedproperty"])
|
|
393
|
+
|
|
394
|
+
check("baseline TRY/CATCH extprop wrapper → only the COMMENT ON; plumbing dropped, not unhandled",
|
|
395
|
+
"BEGIN TRY\n"
|
|
396
|
+
"\tEXEC sp_addextendedproperty N'MS_Description', N'Active flag.', 'SCHEMA', N'__mj', "
|
|
397
|
+
"'TABLE', N'AIAction', 'COLUMN', N'IsActive'\n"
|
|
398
|
+
"END TRY\n"
|
|
399
|
+
"BEGIN CATCH\n"
|
|
400
|
+
"\tDECLARE @msg nvarchar(max);\n"
|
|
401
|
+
"\tDECLARE @severity int;\n"
|
|
402
|
+
"\tDECLARE @state int;\n"
|
|
403
|
+
"\tSELECT @msg = ERROR_MESSAGE(), @severity = ERROR_SEVERITY(), @state = ERROR_STATE();\n"
|
|
404
|
+
"\tRAISERROR(@msg, @severity, @state);\n"
|
|
405
|
+
"\n"
|
|
406
|
+
"\tSET NOEXEC ON\n"
|
|
407
|
+
"END CATCH",
|
|
408
|
+
must_contain=['COMMENT ON COLUMN __mj."AIAction"."IsActive" IS \'Active flag.\''],
|
|
409
|
+
must_not_contain=["DECLARE", "RAISERROR", "ERROR_MESSAGE", "BEGIN CATCH"])
|
|
410
|
+
|
|
411
|
+
# --- IF body mixing a real statement with an extprop keeps BOTH ---------------
|
|
412
|
+
check("guarded INSERT + extprop in one IF body → DO block with INSERT and COMMENT ON",
|
|
413
|
+
"IF NOT EXISTS (SELECT 1 FROM ${flyway:defaultSchema}.Widget WHERE ID = 'abc')\n"
|
|
414
|
+
"BEGIN\n"
|
|
415
|
+
" INSERT INTO ${flyway:defaultSchema}.Widget (ID, Name) VALUES ('abc', 'X');\n"
|
|
416
|
+
" EXEC sp_addextendedproperty @name=N'MS_Description', @value=N'w', "
|
|
417
|
+
"@level0type=N'SCHEMA', @level0name=N'${flyway:defaultSchema}', "
|
|
418
|
+
"@level1type=N'TABLE', @level1name=N'Widget';\n"
|
|
419
|
+
"END",
|
|
420
|
+
must_contain=["DO $$", 'INSERT INTO ${flyway:defaultSchema}."Widget"',
|
|
421
|
+
'COMMENT ON TABLE ${flyway:defaultSchema}."Widget" IS \'w\''],
|
|
422
|
+
must_not_contain=["sp_addextendedproperty"])
|
|
423
|
+
|
|
424
|
+
# --- UPDATE…FROM join semantics ------------------------------------------------
|
|
425
|
+
check("UPDATE…FROM LEFT JOIN anti-join → unhandled (inner-join rewrite would update 0 rows)",
|
|
426
|
+
"UPDATE t SET t.Flag = 1 FROM ${flyway:defaultSchema}.Target AS t "
|
|
427
|
+
"LEFT JOIN ${flyway:defaultSchema}.Other o ON o.TargetID = t.ID WHERE o.ID IS NULL;",
|
|
428
|
+
must_not_contain=["UPDATE"],
|
|
429
|
+
expect_unhandled=1)
|
|
430
|
+
|
|
431
|
+
check("UPDATE…FROM with two INNER joins: extra join's ON moves to WHERE once (not duplicated)",
|
|
432
|
+
"UPDATE uv SET uv.X = b.Y FROM ${flyway:defaultSchema}.UserView AS uv "
|
|
433
|
+
"INNER JOIN ${flyway:defaultSchema}.A a ON a.ID = uv.AID "
|
|
434
|
+
"INNER JOIN ${flyway:defaultSchema}.B b ON b.ID = a.BID "
|
|
435
|
+
"WHERE uv.X IS NULL;",
|
|
436
|
+
must_contain=['UPDATE ${flyway:defaultSchema}."UserView" AS "uv"',
|
|
437
|
+
'CROSS JOIN ${flyway:defaultSchema}."B" AS "b"',
|
|
438
|
+
'"b"."ID" = "a"."BID"'],
|
|
439
|
+
must_not_contain=['AS "b" ON'])
|
|
440
|
+
|
|
441
|
+
# --- ALTER COLUMN atomic-type changes ------------------------------------------
|
|
442
|
+
check("ALTER COLUMN INT→BIGINT widening emits the TYPE change, not just nullability",
|
|
443
|
+
"ALTER TABLE [__mj].[Foo] ALTER COLUMN [Counter] BIGINT NOT NULL;",
|
|
444
|
+
must_contain=['ALTER TABLE "__mj"."Foo" ALTER COLUMN "Counter" TYPE BIGINT',
|
|
445
|
+
'ALTER COLUMN "Counter" SET NOT NULL', "DO $$"],
|
|
446
|
+
must_not_contain=["DROP NOT NULL"])
|
|
447
|
+
|
|
448
|
+
check("ALTER COLUMN with no nullability spec → DROP NOT NULL (T-SQL default is NULLable)",
|
|
449
|
+
"ALTER TABLE [__mj].[Foo] ALTER COLUMN [Counter] BIGINT;",
|
|
450
|
+
must_contain=['ALTER COLUMN "Counter" TYPE BIGINT',
|
|
451
|
+
'ALTER COLUMN "Counter" DROP NOT NULL'],
|
|
452
|
+
must_not_contain=["SET NOT NULL"])
|
|
453
|
+
|
|
454
|
+
# --- procedural leak filter covers INSERT/DELETE with @variables ----------------
|
|
455
|
+
check("INSERT with a T-SQL @variable → unhandled, no $var SQL emitted",
|
|
456
|
+
"INSERT INTO ${flyway:defaultSchema}.Widget (ID, Name) VALUES (@ID, 'X');",
|
|
457
|
+
must_not_contain=["$ID", "INSERT"],
|
|
458
|
+
expect_unhandled=1)
|
|
459
|
+
|
|
460
|
+
check("DELETE with a T-SQL @variable → unhandled, no $var SQL emitted",
|
|
461
|
+
"DELETE FROM ${flyway:defaultSchema}.Widget WHERE ID = @ID;",
|
|
462
|
+
must_not_contain=["$ID", "DELETE"],
|
|
463
|
+
expect_unhandled=1)
|
|
464
|
+
|
|
465
|
+
# --- extprop terminator optional at chunk boundaries ----------------------------
|
|
466
|
+
check("extprop as the last statement with no trailing semicolon still emits its COMMENT ON",
|
|
467
|
+
"ALTER TABLE ${flyway:defaultSchema}.Foo ADD Bar INT NULL;\n"
|
|
468
|
+
"EXEC sp_addextendedproperty @name=N'MS_Description', @value=N'tail', "
|
|
469
|
+
"@level0type=N'SCHEMA', @level0name=N'${flyway:defaultSchema}', "
|
|
470
|
+
"@level1type=N'TABLE', @level1name=N'Foo', @level2type=N'COLUMN', @level2name=N'Bar'",
|
|
471
|
+
must_contain=['ADD COLUMN "Bar" INT',
|
|
472
|
+
'COMMENT ON COLUMN ${flyway:defaultSchema}."Foo"."Bar" IS \'tail\''],
|
|
473
|
+
must_not_contain=["sp_addextendedproperty"])
|
|
474
|
+
|
|
475
|
+
# --- intentional codegen-object extprop skips are NOT unhandled -----------------
|
|
476
|
+
check("codegen-object extprop (vw*) in a mixed batch: skipped silently, not unhandled",
|
|
477
|
+
"ALTER TABLE ${flyway:defaultSchema}.Foo ADD Bar INT NULL;\n"
|
|
478
|
+
"EXEC sp_addextendedproperty @name=N'MS_Description', @value=N'v', "
|
|
479
|
+
"@level0type=N'SCHEMA', @level0name=N'${flyway:defaultSchema}', "
|
|
480
|
+
"@level1type=N'TABLE', @level1name=N'vwFooViews';",
|
|
481
|
+
must_contain=['ADD COLUMN "Bar" INT'],
|
|
482
|
+
must_not_contain=["COMMENT ON", "vwFooViews"])
|
|
483
|
+
|
|
484
|
+
# --- MONEY/SMALLMONEY mapping ----------------------------------------------------
|
|
485
|
+
check("MONEY/SMALLMONEY → DECIMAL(19,4)/DECIMAL(10,4)",
|
|
486
|
+
"CREATE TABLE ${flyway:defaultSchema}.Invoice (Amount MONEY NOT NULL, Tip SMALLMONEY NULL);",
|
|
487
|
+
must_contain=['"Amount" DECIMAL(19, 4)', '"Tip" DECIMAL(10, 4)'],
|
|
488
|
+
must_not_contain=["MONEY"])
|
|
489
|
+
|
|
490
|
+
# --- BIT registry survives a poison statement in the same batch -------------------
|
|
491
|
+
check("poison statement doesn't wipe BIT registration for other tables in its batch",
|
|
492
|
+
"SELECT FROM FROM;\n"
|
|
493
|
+
"CREATE TABLE ${flyway:defaultSchema}.PoisonMate (IsCool BIT NOT NULL);\n"
|
|
494
|
+
"GO\n"
|
|
495
|
+
"ALTER TABLE ${flyway:defaultSchema}.PoisonMate ADD CONSTRAINT CK_pc CHECK (IsCool = (1));",
|
|
496
|
+
must_contain=['CHECK ("IsCool" = TRUE)'],
|
|
497
|
+
must_not_contain=['= (1)'],
|
|
498
|
+
expect_unhandled=1)
|
|
499
|
+
|
|
500
|
+
# --- sys.* / OBJECT_ID() guard conditions ------------------------------------------
|
|
501
|
+
check("sys.columns existence guard → information_schema.columns (no sys.* emitted)",
|
|
502
|
+
"IF NOT EXISTS (SELECT 1 FROM sys.columns "
|
|
503
|
+
"WHERE object_id = OBJECT_ID('${flyway:defaultSchema}.Foo') AND name = 'Bar')\n"
|
|
504
|
+
"BEGIN\n"
|
|
505
|
+
" ALTER TABLE ${flyway:defaultSchema}.Foo ADD Bar INT NULL;\n"
|
|
506
|
+
"END",
|
|
507
|
+
must_contain=["DO $$", "NOT EXISTS (SELECT 1 FROM information_schema.columns",
|
|
508
|
+
"table_schema = '${flyway:defaultSchema}'", "table_name = 'Foo'",
|
|
509
|
+
"column_name = 'Bar'", 'ADD COLUMN "Bar" INT'],
|
|
510
|
+
must_not_contain=["sys.", "OBJECT_ID"])
|
|
511
|
+
|
|
512
|
+
check("sys.objects user-table existence guard → to_regclass",
|
|
513
|
+
"IF NOT EXISTS (SELECT * FROM sys.objects "
|
|
514
|
+
"WHERE object_id = OBJECT_ID(N'${flyway:defaultSchema}.Widget') AND type = N'U')\n"
|
|
515
|
+
"BEGIN\n"
|
|
516
|
+
" CREATE TABLE ${flyway:defaultSchema}.Widget (ID INT NOT NULL);\n"
|
|
517
|
+
"END",
|
|
518
|
+
must_contain=["to_regclass('${flyway:defaultSchema}.\"Widget\"') IS NULL",
|
|
519
|
+
'CREATE TABLE ${flyway:defaultSchema}."Widget"'],
|
|
520
|
+
must_not_contain=["sys.", "OBJECT_ID"])
|
|
521
|
+
|
|
522
|
+
check("sys.indexes guard → pg_indexes",
|
|
523
|
+
"IF NOT EXISTS (SELECT 1 FROM sys.indexes "
|
|
524
|
+
"WHERE name = 'IDX_AUTO_MJ_FKEY_Entity_ParentID' "
|
|
525
|
+
"AND object_id = OBJECT_ID('[${flyway:defaultSchema}].[Entity]'))\n"
|
|
526
|
+
"BEGIN\n"
|
|
527
|
+
" CREATE INDEX IDX_AUTO_MJ_FKEY_Entity_ParentID ON [${flyway:defaultSchema}].[Entity] ([ParentID]);\n"
|
|
528
|
+
"END",
|
|
529
|
+
must_contain=["pg_indexes", "schemaname = '${flyway:defaultSchema}'",
|
|
530
|
+
"tablename = 'Entity'", "indexname = 'IDX_AUTO_MJ_FKEY_Entity_ParentID'",
|
|
531
|
+
"CREATE INDEX"],
|
|
532
|
+
must_not_contain=["sys.", "OBJECT_ID"])
|
|
533
|
+
|
|
534
|
+
check("unrecognized sys.* guard → whole IF routed to unhandled, sys.* never emitted",
|
|
535
|
+
"IF EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = 'FK_x')\n"
|
|
536
|
+
"BEGIN\n"
|
|
537
|
+
" ALTER TABLE ${flyway:defaultSchema}.Foo DROP CONSTRAINT FK_x;\n"
|
|
538
|
+
"END",
|
|
539
|
+
must_not_contain=["sys.", "DO $$", "DROP CONSTRAINT"],
|
|
540
|
+
expect_unhandled=1)
|
|
541
|
+
|
|
542
|
+
# --- IF…BEGIN body scanner: CASE…END and in-string END don't truncate the block -----
|
|
543
|
+
check("CASE…END (and 'END' in a literal) inside a guarded UPDATE; same-batch DDL after survives",
|
|
544
|
+
"IF NOT EXISTS (SELECT 1 FROM ${flyway:defaultSchema}.Widget WHERE ID = 'a')\n"
|
|
545
|
+
"BEGIN\n"
|
|
546
|
+
" UPDATE ${flyway:defaultSchema}.Widget "
|
|
547
|
+
"SET Status = CASE WHEN Score > 5 THEN 'High' ELSE 'Low' END, Name = 'THE END';\n"
|
|
548
|
+
"END\n"
|
|
549
|
+
"CREATE TABLE ${flyway:defaultSchema}.AfterBlock (ID UNIQUEIDENTIFIER NOT NULL);",
|
|
550
|
+
must_contain=["DO $$", "CASE WHEN", "'High'", "'THE END'", "END IF;",
|
|
551
|
+
'CREATE TABLE ${flyway:defaultSchema}."AfterBlock"'],
|
|
552
|
+
must_not_contain=[])
|
|
553
|
+
|
|
554
|
+
# --- hand-written routines: report, never emit half-translated bodies ----------------
|
|
555
|
+
check("bare CREATE PROCEDURE → unhandled, no invalid '$x' PG emitted; same-batch DDL survives",
|
|
556
|
+
"CREATE TABLE ${flyway:defaultSchema}.Job (ID UNIQUEIDENTIFIER NOT NULL);\n"
|
|
557
|
+
"GO\n"
|
|
558
|
+
"CREATE PROCEDURE ${flyway:defaultSchema}.spClaimJob @x INT AS BEGIN "
|
|
559
|
+
"UPDATE ${flyway:defaultSchema}.Job SET LockToken = @x; END;",
|
|
560
|
+
must_contain=['CREATE TABLE ${flyway:defaultSchema}."Job"'],
|
|
561
|
+
must_not_contain=["CREATE PROCEDURE", "$x"],
|
|
562
|
+
expect_unhandled=1)
|
|
563
|
+
|
|
564
|
+
check("bare CREATE FUNCTION → unhandled (T-SQL body is not transpilable)",
|
|
565
|
+
"CREATE FUNCTION ${flyway:defaultSchema}.GetThing (@id UNIQUEIDENTIFIER) "
|
|
566
|
+
"RETURNS NVARCHAR(100) AS BEGIN RETURN 'x'; END;",
|
|
567
|
+
must_not_contain=["CREATE FUNCTION"],
|
|
568
|
+
expect_unhandled=1)
|
|
569
|
+
|
|
570
|
+
if _failures:
|
|
571
|
+
print(f"\n{_failures} test(s) FAILED")
|
|
572
|
+
sys.exit(1)
|
|
573
|
+
print("\nall dialect tests passed")
|