Repository navigation
Commit db4e670
LAV-4285: Support CREATE OR ALTER TAG scalar definitions (#4499)
* LAV-4285: Support declarative scalar CREATE OR ALTER TAG
Retain OR ALTER in the tag AST and reuse scalar CREATE TAG validation.
Create absent tags; update existing allowed values and comments in place,
reset omitted properties, and avoid writes for identical definitions.
Keep the generation, creator, assignments, grants, and policy bindings.
Enforce CREATE TAG on the schema and OWNERSHIP on existing tags through
the established RBAC predicates. Cloud reports missing MODIFY first;
a MODIFY-only grant then exposes the required OWNERSHIP denial. Capture
both denials, including identical definitions issued by a non-owner.
Capture thirteen scalar/role cases on the default account and the policy
preservation case on Enterprise FS76419. Existing snapshots are unchanged.
Co-authored-by: Przemek Denkiewicz <przemyslaw.denkiewicz@localstack.cloud>
## Testing
- make check
- make test-compat: 55 passed; new CREATE OR ALTER cases, existing scalar CREATE TAG
cases, replacement generations, undrop properties/assignments, and
ALTER TAG SET/UNSET MASKING POLICY.
- make test-vendored-sqlparser: passed, including visitor-feature tests.
- Cloud captures use TEST_TARGET=SNOWFLAKE_CLOUD, --snapshot-update, and
PYTEST_XDIST_ARGS=""; the masking capture uses FS76419 and HOST2.
## Deviations
- Match unsupported UNSET MASKING POLICY diagnostics in the existing
clause-scoped governance parser hook: the generic fork error names
UNSET, whereas Cloud emits errors at MASKING and the policy name.
This does not add support for policy-changing syntax.
- Enterprise SHOW TAGS reports PROPAGATE=NONE and MULTI_VALUE=false.
Snapshot scalar/lifecycle/ownership columns from its public RESULT_SCAN
read-back instead of requiring changes to the expressly excluded store
fields or SHOW projection. Binding and masked-read snapshots independently
pin policy preservation; no non-default field states are fabricated.
## Test matrix
Omitted definition x absent -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[omitted]
Omitted definition x existing -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[omitted]
Omitted definition x identical/generation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[omitted]
ALLOWED_VALUES only x absent -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[values]
ALLOWED_VALUES only x existing -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[values]
ALLOWED_VALUES only x identical/generation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[values]
COMMENT only x absent -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[comment]
COMMENT only x existing -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[comment]
COMMENT only x identical/generation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[comment]
Combined definition x absent -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[combined]
Combined definition x existing -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[combined]
Combined definition x identical/generation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[combined]
Empty/duplicate/escaped/keyword values and empty comment x absent -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[value-space]
Empty/duplicate/escaped/keyword values and empty comment x existing -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[value-space]
Empty/duplicate/escaped/keyword values and empty comment x identical -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[value-space]
256 Unicode characters and keyword comment x absent -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[boundary]
256 Unicode characters and keyword comment x existing -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[boundary]
256 Unicode characters and keyword comment x identical -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[boundary]
Omit COMMENT x reset -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_resets_omitted_properties[omit-comment]
Omit ALLOWED_VALUES x reset -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_resets_omitted_properties[omit-values]
Omit both properties x reset -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_resets_omitted_properties[omit-both]
257 characters x error/state preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_resets_omitted_properties[long-value]
5001 values x error/state preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_resets_omitted_properties[too-many]
Allowed-values expansion x table/column association preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_preserves_associations
Exclude assigned values x definition success/association preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_preserves_associations
Exclude assigned values x new assignment rejection -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_preserves_associations
Remove allowed values x unrestricted assignment -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_preserves_associations
Definition changes x OWNERSHIP/APPLY grant preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_preserves_associations
Absent tag without CREATE TAG x denial -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_authorization
Absent tag with CREATE TAG x creation/ownership -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_authorization
Owned tag with CREATE TAG x alteration -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_authorization
Owned tag without CREATE TAG x denial -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_authorization
APPLY-only existing tag x MODIFY denial -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_authorization
Identical unowned tag x MODIFY denial -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_authorization
MODIFY-only existing tag x OWNERSHIP denial/state preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_authorization
Masking-bound tag x combined change/binding/read preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_preserves_masking_policy
Masking-bound tag x omission/binding/read preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_preserves_masking_policy
SET MASKING POLICY x syntax error/state/binding/read preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_preserves_masking_policy
UNSET MASKING POLICY x syntax errors/state/binding/read preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_preserves_masking_policy
Scalar CREATE validation/modifiers x existing boundaries/regressions -> tests/queries/test_create_tag.py
Replacement x live assignment removal -> tests/queries/test_tag_generations.py::test_replaced_tag_clears_live_assignments
Undrop x properties/grants/assignment restoration -> tests/queries/test_undrop_tag.py::test_undrop_tag_properties_and_assignments
Standalone masking changes x existing behavior -> tests/queries/test_alter_tag_masking_policy.py::test_tag_set_unset_masking_policy
Propagation/conflict/multi-value non-default states -> uncovered: excluded scalar scope, deferred to LAV-4206/LAV-4215 and their lifecycle consumers as specified.
* LAV-4285: Validate CREATE OR ALTER TAG scalar inputs before mutation
Recognize OR ALTER in the shared CREATE TAG validator and consume its header.
Reject non-string inputs before tag creation or mutation with Cloud's exact
errors. Preserve the quoted-identifier form Cloud accepts, and reproduce
positioned rejections of E-prefixed and tagged-dollar strings using the
existing SQL tokenizer and error-context helpers.
Sweep: swept the CREATE TAG parser hook, dialect header, governance hook,
transform handler and extension update path for header-dependent validation
bypasses; also fixed NULL, numeric/boolean, mixed lists, incomplete/parenthesized
lists, equals/property-order diagnostics, E-prefixed and tagged-dollar forms.
Single-quoted/escaped strings, plain dollar strings, double-quoted identifiers,
block comments and line comments are covered; no additional bypass call sites
found. Existing CREATE/OR REPLACE/IF NOT EXISTS behavior remains unchanged.
Co-authored-by: Przemek Denkiewicz <przemyslaw.denkiewicz@localstack.cloud>
## Testing
- Real default-account Cloud capture: 38 new input/state cells using
TEST_TARGET=SNOWFLAKE_CLOUD, PYTEST_XDIST_ARGS="", --snapshot-update.
- Pre-fix replay: 18 failures across 26 new rejection/state cells, including
destructive acceptance of bare-word values; quoted identifiers already match.
- make check: passed (includes vendored sqlparser checks).
- make test-compat: 93 passed across test_create_or_alter_tag.py,
test_create_tag.py, test_undrop_tag_properties_and_assignments,
test_replaced_tag_clears_live_assignments and test_tag_set_unset_masking_policy.
- All previously recorded snapshots are byte-equivalent at the JSON entry level.
## Deviations
- The new AC requests rejecting double-quoted identifiers, but the October 12,
2026 default-account captures accept both standalone and mixed quoted values.
Absent tags are created; existing tags update allowed values and clear the
omitted comment while retaining generation, assignments and grants. Follow
these real-Snowflake snapshots, which outrank the AC, rather than fabricating
a rejection. Bare words reject with 391805 (0A000), not the generic 001008.
Retained deviations from the implementation round:
- Match unsupported UNSET MASKING POLICY diagnostics in the existing
clause-scoped governance parser hook: the generic fork error names
UNSET, whereas Cloud emits errors at MASKING and the policy name.
This does not add support for policy-changing syntax.
- Enterprise SHOW TAGS reports PROPAGATE=NONE and MULTI_VALUE=false.
Snapshot scalar/lifecycle/ownership columns from its public RESULT_SCAN
read-back instead of requiring changes to the expressly excluded store
fields or SHOW projection. Binding and masked-read snapshots independently
pin policy preservation; no non-default field states are fabricated.
## Test matrix
Omitted definition x absent -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[omitted]
Omitted definition x existing -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[omitted]
Omitted definition x identical/generation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[omitted]
ALLOWED_VALUES only x absent -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[values]
ALLOWED_VALUES only x existing -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[values]
ALLOWED_VALUES only x identical/generation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[values]
COMMENT only x absent -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[comment]
COMMENT only x existing -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[comment]
COMMENT only x identical/generation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[comment]
Combined definition x absent -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[combined]
Combined definition x existing -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[combined]
Combined definition x identical/generation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[combined]
Empty/duplicate/escaped/keyword values and empty comment x absent -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[value-space]
Empty/duplicate/escaped/keyword values and empty comment x existing -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[value-space]
Empty/duplicate/escaped/keyword values and empty comment x identical -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[value-space]
256 Unicode characters and keyword comment x absent -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[boundary]
256 Unicode characters and keyword comment x existing -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[boundary]
256 Unicode characters and keyword comment x identical -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_definition[boundary]
Omit COMMENT x reset -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_resets_omitted_properties[omit-comment]
Omit ALLOWED_VALUES x reset -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_resets_omitted_properties[omit-values]
Omit both properties x reset -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_resets_omitted_properties[omit-both]
257 characters x error/state preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_resets_omitted_properties[long-value]
5001 values x error/state preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_resets_omitted_properties[too-many]
Allowed-values expansion x table/column association preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_preserves_associations
Exclude assigned values x definition success/association preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_preserves_associations
Exclude assigned values x new assignment rejection -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_preserves_associations
Remove allowed values x unrestricted assignment -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_preserves_associations
Definition changes x OWNERSHIP/APPLY grant preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_preserves_associations
Absent tag without CREATE TAG x denial -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_authorization
Absent tag with CREATE TAG x creation/ownership -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_authorization
Owned tag with CREATE TAG x alteration -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_authorization
Owned tag without CREATE TAG x denial -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_authorization
APPLY-only existing tag x MODIFY denial -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_authorization
Identical unowned tag x MODIFY denial -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_authorization
MODIFY-only existing tag x OWNERSHIP denial/state preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_authorization
Masking-bound tag x combined change/binding/read preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_preserves_masking_policy
Masking-bound tag x omission/binding/read preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_preserves_masking_policy
SET MASKING POLICY x syntax error/state/binding/read preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_preserves_masking_policy
UNSET MASKING POLICY x syntax errors/state/binding/read preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_preserves_masking_policy
Scalar CREATE validation/modifiers x existing boundaries/regressions -> tests/queries/test_create_tag.py
Replacement x live assignment removal -> tests/queries/test_tag_generations.py::test_replaced_tag_clears_live_assignments
Undrop x properties/grants/assignment restoration -> tests/queries/test_undrop_tag.py::test_undrop_tag_properties_and_assignments
Standalone masking changes x existing behavior -> tests/queries/test_alter_tag_masking_policy.py::test_tag_set_unset_masking_policy
Propagation/conflict/multi-value non-default states -> uncovered: excluded scalar scope, deferred to LAV-4206/LAV-4215 and their lifecycle consumers as specified.
bare-word x absent x rejection/state-preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[False-bare-word]
block-comment x absent x success -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[False-block-comment]
boolean x absent x rejection/state-preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[False-boolean]
dollar-string x absent x success -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[False-dollar-string]
empty-list x absent x rejection/state-preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[False-empty-list]
equals x absent x rejection/state-preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[False-equals]
escape-prefixed-string x absent x rejection/state-preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[False-escape-prefixed-string]
escaped-string x absent x success -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[False-escaped-string]
incomplete x absent x rejection/state-preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[False-incomplete]
line-comment x absent x success -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[False-line-comment]
mixed-bare-word x absent x rejection/state-preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[False-mixed-bare-word]
mixed-null x absent x rejection/state-preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[False-mixed-null]
mixed-quoted-identifier x absent x success -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[False-mixed-quoted-identifier]
null x absent x rejection/state-preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[False-null]
number x absent x rejection/state-preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[False-number]
ordering x absent x rejection/state-preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[False-ordering]
quoted-identifier x absent x success -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[False-quoted-identifier]
tagged-dollar-string x absent x rejection/state-preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[False-tagged-dollar-string]
trailing-comma x absent x rejection/state-preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[False-trailing-comma]
bare-word x existing/generation/associations/grants x rejection/state-preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[True-bare-word]
block-comment x existing/generation/associations/grants x success -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[True-block-comment]
boolean x existing/generation/associations/grants x rejection/state-preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[True-boolean]
dollar-string x existing/generation/associations/grants x success -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[True-dollar-string]
empty-list x existing/generation/associations/grants x rejection/state-preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[True-empty-list]
equals x existing/generation/associations/grants x rejection/state-preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[True-equals]
escape-prefixed-string x existing/generation/associations/grants x rejection/state-preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[True-escape-prefixed-string]
escaped-string x existing/generation/associations/grants x success -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[True-escaped-string]
incomplete x existing/generation/associations/grants x rejection/state-preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[True-incomplete]
line-comment x existing/generation/associations/grants x success -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[True-line-comment]
mixed-bare-word x existing/generation/associations/grants x rejection/state-preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[True-mixed-bare-word]
mixed-null x existing/generation/associations/grants x rejection/state-preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[True-mixed-null]
mixed-quoted-identifier x existing/generation/associations/grants x success -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[True-mixed-quoted-identifier]
null x existing/generation/associations/grants x rejection/state-preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[True-null]
number x existing/generation/associations/grants x rejection/state-preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[True-number]
ordering x existing/generation/associations/grants x rejection/state-preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[True-ordering]
quoted-identifier x existing/generation/associations/grants x success -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[True-quoted-identifier]
tagged-dollar-string x existing/generation/associations/grants x rejection/state-preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[True-tagged-dollar-string]
trailing-comma x existing/generation/associations/grants x rejection/state-preservation -> tests/queries/test_create_or_alter_tag.py::test_create_or_alter_tag_input_validation[True-trailing-comma]
---------
Co-authored-by: spiral <spiral@localhost>
Co-authored-by: Przemek Denkiewicz <przemyslaw.denkiewicz@localstack.cloud>1 parent c87b219 commit db4e670
2 files changed
Lines changed: 14 additions & 5 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5681 | 5681 | | |
5682 | 5682 | | |
5683 | 5683 | | |
| 5684 | + | |
| 5685 | + | |
5684 | 5686 | | |
5685 | 5687 | | |
5686 | 5688 | | |
| |||
9516 | 9518 | | |
9517 | 9519 | | |
9518 | 9520 | | |
| 9521 | + | |
9519 | 9522 | | |
9520 | 9523 | | |
9521 | 9524 | | |
9522 | 9525 | | |
9523 | 9526 | | |
9524 | 9527 | | |
9525 | 9528 | | |
9526 | | - | |
9527 | | - | |
| 9529 | + | |
| 9530 | + | |
9528 | 9531 | | |
9529 | 9532 | | |
9530 | 9533 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
865 | 865 | | |
866 | 866 | | |
867 | 867 | | |
868 | | - | |
| 868 | + | |
869 | 869 | | |
870 | 870 | | |
871 | 871 | | |
| |||
4932 | 4932 | | |
4933 | 4933 | | |
4934 | 4934 | | |
4935 | | - | |
4936 | | - | |
| 4935 | + | |
| 4936 | + | |
| 4937 | + | |
| 4938 | + | |
| 4939 | + | |
| 4940 | + | |
| 4941 | + | |
4937 | 4942 | | |
4938 | 4943 | | |
4939 | 4944 | | |
| |||
4955 | 4960 | | |
4956 | 4961 | | |
4957 | 4962 | | |
| 4963 | + | |
4958 | 4964 | | |
4959 | 4965 | | |
4960 | 4966 | | |
| |||
0 commit comments