From bd686fd41c0e68e2adde2038faaee648afac26d7 Mon Sep 17 00:00:00 2001 From: Kazuho Cryer-Shinozuka Date: Fri, 3 Oct 2025 00:16:02 +0900 Subject: [PATCH 01/16] partition projection --- packages/@aws-cdk/aws-glue-alpha/README.md | 159 ++++ packages/@aws-cdk/aws-glue-alpha/lib/index.ts | 1 + .../lib/partition-projection.ts | 462 +++++++++++ .../@aws-cdk/aws-glue-alpha/lib/table-base.ts | 70 ++ ...efaultTestDeployAssert9180F049.assets.json | 20 + ...aultTestDeployAssert9180F049.template.json | 36 + ...-cdk-glue-partition-projection.assets.json | 20 + ...dk-glue-partition-projection.template.json | 426 ++++++++++ .../cdk.out | 1 + .../integ.json | 13 + .../manifest.json | 733 ++++++++++++++++++ .../tree.json | 1 + .../test/integ.partition-projection.ts | 142 ++++ .../aws-glue-alpha/test/table-base.test.ts | 398 ++++++++++ 14 files changed, 2482 insertions(+) create mode 100644 packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts create mode 100644 packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.js.snapshot/GluePartitionProjectionTestDefaultTestDeployAssert9180F049.assets.json create mode 100644 packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.js.snapshot/GluePartitionProjectionTestDefaultTestDeployAssert9180F049.template.json create mode 100644 packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.js.snapshot/aws-cdk-glue-partition-projection.assets.json create mode 100644 packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.js.snapshot/aws-cdk-glue-partition-projection.template.json create mode 100644 packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.js.snapshot/cdk.out create mode 100644 packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.js.snapshot/integ.json create mode 100644 packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.js.snapshot/manifest.json create mode 100644 packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.js.snapshot/tree.json create mode 100644 packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.ts diff --git a/packages/@aws-cdk/aws-glue-alpha/README.md b/packages/@aws-cdk/aws-glue-alpha/README.md index ac72a980a29af..20d10e3196b6c 100644 --- a/packages/@aws-cdk/aws-glue-alpha/README.md +++ b/packages/@aws-cdk/aws-glue-alpha/README.md @@ -713,6 +713,165 @@ new glue.S3Table(this, 'MyTable', { }); ``` +### Partition Projection + +Partition projection allows Athena to automatically add new partitions as new data arrives, without requiring `ALTER TABLE ADD PARTITION` statements. This improves query performance and reduces management overhead by eliminating the need to manually manage partition metadata. + +For more information, see the [AWS documentation on partition projection](https://docs.aws.amazon.com/athena/latest/ug/partition-projection.html). + +#### INTEGER Projection + +For partition keys with sequential numeric values: + +```ts +declare const myDatabase: glue.Database; +new glue.S3Table(this, 'MyTable', { + database: myDatabase, + columns: [{ + name: 'data', + type: glue.Schema.STRING, + }], + partitionKeys: [{ + name: 'year', + type: glue.Schema.INTEGER, + }], + dataFormat: glue.DataFormat.JSON, + partitionProjection: { + year: { + type: glue.PartitionProjectionType.INTEGER, + range: [2020, 2023], + interval: 1, // optional, defaults to 1 + digits: 4, // optional, pads with leading zeros + }, + }, +}); +``` + +#### DATE Projection + +For partition keys with date or timestamp values: + +```ts +declare const myDatabase: glue.Database; +new glue.S3Table(this, 'MyTable', { + database: myDatabase, + columns: [{ + name: 'data', + type: glue.Schema.STRING, + }], + partitionKeys: [{ + name: 'date', + type: glue.Schema.STRING, + }], + dataFormat: glue.DataFormat.JSON, + partitionProjection: { + date: { + type: glue.PartitionProjectionType.DATE, + range: ['2020-01-01', '2023-12-31'], + format: 'yyyy-MM-dd', + interval: 1, // optional, defaults to 1 + intervalUnit: 'DAYS', // optional: YEARS, MONTHS, WEEKS, DAYS, HOURS, MINUTES, SECONDS + }, + }, +}); +``` + +#### ENUM Projection + +For partition keys with a known set of values: + +```ts +declare const myDatabase: glue.Database; +new glue.S3Table(this, 'MyTable', { + database: myDatabase, + columns: [{ + name: 'data', + type: glue.Schema.STRING, + }], + partitionKeys: [{ + name: 'region', + type: glue.Schema.STRING, + }], + dataFormat: glue.DataFormat.JSON, + partitionProjection: { + region: { + type: glue.PartitionProjectionType.ENUM, + values: ['us-east-1', 'us-west-2', 'eu-west-1'], + }, + }, +}); +``` + +#### INJECTED Projection + +For custom partition values injected at query time: + +```ts +declare const myDatabase: glue.Database; +new glue.S3Table(this, 'MyTable', { + database: myDatabase, + columns: [{ + name: 'data', + type: glue.Schema.STRING, + }], + partitionKeys: [{ + name: 'custom', + type: glue.Schema.STRING, + }], + dataFormat: glue.DataFormat.JSON, + partitionProjection: { + custom: { + type: glue.PartitionProjectionType.INJECTED, + }, + }, +}); +``` + +#### Multiple Partition Projections + +You can configure partition projection for multiple partition keys: + +```ts +declare const myDatabase: glue.Database; +new glue.S3Table(this, 'MyTable', { + database: myDatabase, + columns: [{ + name: 'data', + type: glue.Schema.STRING, + }], + partitionKeys: [ + { + name: 'year', + type: glue.Schema.INTEGER, + }, + { + name: 'month', + type: glue.Schema.INTEGER, + }, + { + name: 'region', + type: glue.Schema.STRING, + }, + ], + dataFormat: glue.DataFormat.JSON, + partitionProjection: { + year: { + type: glue.PartitionProjectionType.INTEGER, + range: [2020, 2023], + }, + month: { + type: glue.PartitionProjectionType.INTEGER, + range: [1, 12], + digits: 2, + }, + region: { + type: glue.PartitionProjectionType.ENUM, + values: ['us-east-1', 'us-west-2'], + }, + }, +}); +``` + ### Glue Connections Glue connections allow external data connections to third party databases and data warehouses. However, these connections can also be assigned to Glue Tables, allowing you to query external data sources using the Glue Data Catalog. diff --git a/packages/@aws-cdk/aws-glue-alpha/lib/index.ts b/packages/@aws-cdk/aws-glue-alpha/lib/index.ts index 765f41d167535..adc0a2d6359e1 100644 --- a/packages/@aws-cdk/aws-glue-alpha/lib/index.ts +++ b/packages/@aws-cdk/aws-glue-alpha/lib/index.ts @@ -6,6 +6,7 @@ export * from './data-format'; export * from './data-quality-ruleset'; export * from './database'; export * from './external-table'; +export * from './partition-projection'; export * from './s3-table'; export * from './schema'; export * from './security-configuration'; diff --git a/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts b/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts new file mode 100644 index 0000000000000..e8b98719e607a --- /dev/null +++ b/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts @@ -0,0 +1,462 @@ +import { UnscopedValidationError } from 'aws-cdk-lib'; + +/** + * Partition projection type. + * + * Determines how Athena projects partition values. + * + * @see https://docs.aws.amazon.com/athena/latest/ug/partition-projection-supported-types.html + */ +export enum PartitionProjectionType { + /** + * Project partition values as integers within a range. + */ + INTEGER = 'integer', + + /** + * Project partition values as dates within a range. + */ + DATE = 'date', + + /** + * Project partition values from an explicit list of values. + */ + ENUM = 'enum', + + /** + * Project partition values that are injected at query time. + */ + INJECTED = 'injected', +} + +/** + * Configuration for INTEGER partition projection. + * + * @see https://docs.aws.amazon.com/athena/latest/ug/partition-projection-supported-types.html#partition-projection-integer-type + */ +export interface IntegerPartitionConfiguration { + /** + * The type of partition projection. + */ + readonly type: PartitionProjectionType.INTEGER; + + /** + * Range of integer partition values [min, max] (inclusive). + * + * Array must contain exactly 2 elements: [min, max] + * + * @example [0, 100] + */ + readonly range: number[]; + + /** + * Interval between partition values. + * + * @default 1 + */ + readonly interval?: number; + + /** + * Number of digits to pad the partition value with leading zeros. + * + * @default - no padding + * + * @example + * // With digits: 4, partition values: 0001, 0002, ..., 0100 + * digits: 4 + */ + readonly digits?: number; +} + +/** + * Configuration for DATE partition projection. + * + * @see https://docs.aws.amazon.com/athena/latest/ug/partition-projection-supported-types.html#partition-projection-date-type + */ +export interface DatePartitionConfiguration { + /** + * The type of partition projection. + */ + readonly type: PartitionProjectionType.DATE; + + /** + * Range of date partition values [start, end] (inclusive) in ISO 8601 format. + * + * Array must contain exactly 2 elements: [start, end] + * + * @example ['2020-01-01', '2023-12-31'] + * @example ['2020-01-01-00-00-00', '2023-12-31-23-59-59'] + */ + readonly range: string[]; + + /** + * Date format for partition values. + * + * Uses Java SimpleDateFormat patterns. + * + * @see https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html + * + * @example + * 'yyyy-MM-dd' + * 'yyyy-MM-dd-HH-mm-ss' + * 'yyyyMMdd' + */ + readonly format: string; + + /** + * Interval between partition values. + * + * @default 1 + */ + readonly interval?: number; + + /** + * Unit for the interval. + * + * @default DAYS + */ + readonly intervalUnit?: 'YEARS' | 'MONTHS' | 'WEEKS' | 'DAYS' | 'HOURS' | 'MINUTES' | 'SECONDS'; +} + +/** + * Configuration for ENUM partition projection. + * + * @see https://docs.aws.amazon.com/athena/latest/ug/partition-projection-supported-types.html#partition-projection-enum-type + */ +export interface EnumPartitionConfiguration { + /** + * The type of partition projection. + */ + readonly type: PartitionProjectionType.ENUM; + + /** + * Explicit list of partition values. + * + * @example ['us-east-1', 'us-west-2', 'eu-west-1'] + */ + readonly values: string[]; +} + +/** + * Configuration for INJECTED partition projection. + * + * Partition values are injected at query time through the query statement. + * + * @see https://docs.aws.amazon.com/athena/latest/ug/partition-projection-supported-types.html#partition-projection-injected-type + */ +export interface InjectedPartitionConfiguration { + /** + * The type of partition projection. + */ + readonly type: PartitionProjectionType.INJECTED; +} + +/** + * Partition projection configuration. + * + * Discriminated union of partition projection types. + */ +export type PartitionConfiguration = + | IntegerPartitionConfiguration + | DatePartitionConfiguration + | EnumPartitionConfiguration + | InjectedPartitionConfiguration; + +/** + * Partition projection configuration for a table. + * + * Maps partition column names to their projection configurations. + * The key is the partition column name, the value is the partition configuration. + * + * @example + * { + * year: { + * type: PartitionProjectionType.INTEGER, + * range: [2020, 2023], + * }, + * region: { + * type: PartitionProjectionType.ENUM, + * values: ['us-east-1', 'us-west-2'], + * }, + * } + */ +export type PartitionProjection = { [columnName: string]: PartitionConfiguration }; + +/** + * Validates INTEGER partition projection configuration. + * + * @param columnName - The partition column name + * @param config - The INTEGER partition configuration + * @throws {UnscopedValidationError} if the configuration is invalid + */ +export function validateIntegerPartition(columnName: string, config: IntegerPartitionConfiguration): void { + // Validate range + if (config.range.length !== 2) { + throw new UnscopedValidationError( + `INTEGER partition projection range for "${columnName}" must be [min, max], but got array of length ${config.range.length}`, + ); + } + + const [min, max] = config.range; + if (!Number.isInteger(min) || !Number.isInteger(max)) { + throw new UnscopedValidationError( + `INTEGER partition projection range for "${columnName}" must contain integers, but got [${min}, ${max}]`, + ); + } + + if (min > max) { + throw new UnscopedValidationError( + `INTEGER partition projection range for "${columnName}" must be [min, max] where min <= max, but got [${min}, ${max}]`, + ); + } + + // Validate interval + if (config.interval !== undefined) { + if (!Number.isInteger(config.interval) || config.interval <= 0) { + throw new UnscopedValidationError( + `INTEGER partition projection interval for "${columnName}" must be a positive integer, but got ${config.interval}`, + ); + } + } + + // Validate digits + if (config.digits !== undefined) { + if (!Number.isInteger(config.digits) || config.digits < 1) { + throw new UnscopedValidationError( + `INTEGER partition projection digits for "${columnName}" must be an integer >= 1, but got ${config.digits}`, + ); + } + } +} + +/** + * Validates DATE partition projection configuration. + * + * @param columnName - The partition column name + * @param config - The DATE partition configuration + * @throws {UnscopedValidationError} if the configuration is invalid + */ +export function validateDatePartition(columnName: string, config: DatePartitionConfiguration): void { + // Validate range + if (config.range.length !== 2) { + throw new UnscopedValidationError( + `DATE partition projection range for "${columnName}" must be [start, end], but got array of length ${config.range.length}`, + ); + } + + const [start, end] = config.range; + if (typeof start !== 'string' || typeof end !== 'string') { + throw new UnscopedValidationError( + `DATE partition projection range for "${columnName}" must contain strings, but got [${typeof start}, ${typeof end}]`, + ); + } + + if (start.trim() === '' || end.trim() === '') { + throw new UnscopedValidationError( + `DATE partition projection range for "${columnName}" must not contain empty strings`, + ); + } + + // Validate format + if (typeof config.format !== 'string' || config.format.trim() === '') { + throw new UnscopedValidationError( + `DATE partition projection format for "${columnName}" must be a non-empty string`, + ); + } + + // Validate interval + if (config.interval !== undefined) { + if (!Number.isInteger(config.interval) || config.interval <= 0) { + throw new UnscopedValidationError( + `DATE partition projection interval for "${columnName}" must be a positive integer, but got ${config.interval}`, + ); + } + } + + // Validate interval unit + if (config.intervalUnit !== undefined) { + const validUnits = ['YEARS', 'MONTHS', 'WEEKS', 'DAYS', 'HOURS', 'MINUTES', 'SECONDS']; + if (!validUnits.includes(config.intervalUnit)) { + throw new UnscopedValidationError( + `DATE partition projection interval unit for "${columnName}" must be one of ${validUnits.join(', ')}, but got ${config.intervalUnit}`, + ); + } + } +} + +/** + * Validates ENUM partition projection configuration. + * + * @param columnName - The partition column name + * @param config - The ENUM partition configuration + * @throws {UnscopedValidationError} if the configuration is invalid + */ +export function validateEnumPartition(columnName: string, config: EnumPartitionConfiguration): void { + // Validate values + if (!Array.isArray(config.values) || config.values.length === 0) { + throw new UnscopedValidationError( + `ENUM partition projection values for "${columnName}" must be a non-empty array`, + ); + } + + for (let i = 0; i < config.values.length; i++) { + const value = config.values[i]; + if (typeof value !== 'string') { + throw new UnscopedValidationError( + `ENUM partition projection values for "${columnName}" must contain only strings, but found ${typeof value} at index ${i}`, + ); + } + if (value.trim() === '') { + throw new UnscopedValidationError( + `ENUM partition projection values for "${columnName}" must not contain empty strings`, + ); + } + } +} + +/** + * Validates INJECTED partition projection configuration. + * + * @param _columnName - The partition column name + * @param _config - The INJECTED partition configuration + * @throws {UnscopedValidationError} if the configuration is invalid + */ +export function validateInjectedPartition(_columnName: string, _config: InjectedPartitionConfiguration): void { + // INJECTED type has no additional properties to validate + // This function exists for completeness and future extensibility +} + +/** + * Validates partition projection configuration based on its type. + * + * @param columnName - The partition column name + * @param config - The partition configuration + * @throws {UnscopedValidationError} if the configuration is invalid + */ +export function validatePartitionConfiguration(columnName: string, config: PartitionConfiguration): void { + switch (config.type) { + case PartitionProjectionType.INTEGER: + validateIntegerPartition(columnName, config); + break; + case PartitionProjectionType.DATE: + validateDatePartition(columnName, config); + break; + case PartitionProjectionType.ENUM: + validateEnumPartition(columnName, config); + break; + case PartitionProjectionType.INJECTED: + validateInjectedPartition(columnName, config); + break; + default: + // TypeScript exhaustiveness check + const exhaustiveCheck: never = config; + throw new UnscopedValidationError( + `Unknown partition projection type for "${columnName}": ${(exhaustiveCheck as any).type}`, + ); + } +} + +/** + * Generates CloudFormation parameters for INTEGER partition projection. + * + * @param columnName - The partition column name + * @param config - The INTEGER partition configuration + * @returns CloudFormation parameters + */ +function generateIntegerParameters(columnName: string, config: IntegerPartitionConfiguration): { [key: string]: string } { + const params: { [key: string]: string } = { + [`projection.${columnName}.type`]: 'integer', + [`projection.${columnName}.range`]: `${config.range[0]},${config.range[1]}`, + }; + + if (config.interval !== undefined) { + params[`projection.${columnName}.interval`] = config.interval.toString(); + } + + if (config.digits !== undefined) { + params[`projection.${columnName}.digits`] = config.digits.toString(); + } + + return params; +} + +/** + * Generates CloudFormation parameters for DATE partition projection. + * + * @param columnName - The partition column name + * @param config - The DATE partition configuration + * @returns CloudFormation parameters + */ +function generateDateParameters(columnName: string, config: DatePartitionConfiguration): { [key: string]: string } { + const params: { [key: string]: string } = { + [`projection.${columnName}.type`]: 'date', + [`projection.${columnName}.range`]: `${config.range[0]},${config.range[1]}`, + [`projection.${columnName}.format`]: config.format, + }; + + if (config.interval !== undefined) { + params[`projection.${columnName}.interval`] = config.interval.toString(); + } + + if (config.intervalUnit !== undefined) { + params[`projection.${columnName}.interval.unit`] = config.intervalUnit; + } + + return params; +} + +/** + * Generates CloudFormation parameters for ENUM partition projection. + * + * @param columnName - The partition column name + * @param config - The ENUM partition configuration + * @returns CloudFormation parameters + */ +function generateEnumParameters(columnName: string, config: EnumPartitionConfiguration): { [key: string]: string } { + return { + [`projection.${columnName}.type`]: 'enum', + [`projection.${columnName}.values`]: config.values.join(','), + }; +} + +/** + * Generates CloudFormation parameters for INJECTED partition projection. + * + * @param columnName - The partition column name + * @param _config - The INJECTED partition configuration + * @returns CloudFormation parameters + */ +function generateInjectedParameters(columnName: string, _config: InjectedPartitionConfiguration): { [key: string]: string } { + return { + [`projection.${columnName}.type`]: 'injected', + }; +} + +/** + * Generates CloudFormation parameters for partition projection configuration. + * + * @param columnName - The partition column name + * @param config - The partition configuration + * @returns CloudFormation parameters + */ +export function generatePartitionProjectionParameters( + columnName: string, + config: PartitionConfiguration, +): { [key: string]: string } { + switch (config.type) { + case PartitionProjectionType.INTEGER: + return generateIntegerParameters(columnName, config); + case PartitionProjectionType.DATE: + return generateDateParameters(columnName, config); + case PartitionProjectionType.ENUM: + return generateEnumParameters(columnName, config); + case PartitionProjectionType.INJECTED: + return generateInjectedParameters(columnName, config); + default: + // TypeScript exhaustiveness check + const exhaustiveCheck: never = config; + throw new UnscopedValidationError( + `Unknown partition projection type for "${columnName}": ${(exhaustiveCheck as any).type}`, + ); + } +} diff --git a/packages/@aws-cdk/aws-glue-alpha/lib/table-base.ts b/packages/@aws-cdk/aws-glue-alpha/lib/table-base.ts index e423b99419c92..4610f9c6558b1 100644 --- a/packages/@aws-cdk/aws-glue-alpha/lib/table-base.ts +++ b/packages/@aws-cdk/aws-glue-alpha/lib/table-base.ts @@ -6,6 +6,7 @@ import { AwsCustomResource } from 'aws-cdk-lib/custom-resources'; import { Construct } from 'constructs'; import { DataFormat } from './data-format'; import { IDatabase } from './database'; +import { validatePartitionConfiguration, generatePartitionProjectionParameters } from './partition-projection'; import { Column } from './schema'; import { StorageParameter } from './storage-parameter'; @@ -157,6 +158,18 @@ export interface TableBaseProps { * @default - The parameter is not defined */ readonly parameters?: { [key: string]: string }; + + /** + * Partition projection configuration for this table. + * + * Partition projection allows Athena to automatically add new partitions + * without requiring `ALTER TABLE ADD PARTITION` statements. + * + * @see https://docs.aws.amazon.com/athena/latest/ug/partition-projection.html + * + * @default - No partition projection + */ + readonly partitionProjection?: import('./partition-projection').PartitionProjection; } /** @@ -223,6 +236,11 @@ export abstract class TableBase extends Resource implements ITable { */ public readonly storageParameters?: StorageParameter[]; + /** + * This table's partition projection configuration if enabled. + */ + public readonly partitionProjection?: import('./partition-projection').PartitionProjection; + /** * The tables' properties associated with the table. * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-table-tableinput.html#cfn-glue-table-tableinput-parameters @@ -251,9 +269,15 @@ export abstract class TableBase extends Resource implements ITable { this.columns = props.columns; this.partitionKeys = props.partitionKeys; this.storageParameters = props.storageParameters; + this.partitionProjection = props.partitionProjection; this.parameters = props.parameters ?? {}; this.compressed = props.compressed ?? false; + + // Validate and generate partition projection parameters + if (this.partitionProjection) { + this.validateAndGeneratePartitionProjection(); + } } public abstract grantRead(grantee: iam.IGrantable): iam.Grant; @@ -327,6 +351,52 @@ export abstract class TableBase extends Resource implements ITable { } } + /** + * Validate partition projection configuration and merge generated + * parameters into this.parameters. + * + * @throws {ValidationError} if partition projection configuration is invalid + */ + private validateAndGeneratePartitionProjection(): void { + if (!this.partitionProjection) { + return; + } + + // Validate that partition keys exist + if (!this.partitionKeys || this.partitionKeys.length === 0) { + throw new ValidationError( + 'The table must have partition keys to use partition projection', + this, + ); + } + + const partitionKeyNames = this.partitionKeys.map(pk => pk.name); + + // Validate each partition projection configuration + for (const [columnName, config] of Object.entries(this.partitionProjection)) { + // Validate that column is a partition key + if (!partitionKeyNames.includes(columnName)) { + throw new ValidationError( + `Partition projection column "${columnName}" must be a partition key. ` + + `Partition keys are: ${partitionKeyNames.join(', ')}`, + this, + ); + } + + // Validate the configuration based on type + validatePartitionConfiguration(columnName, config); + + // Generate CloudFormation parameters + const generatedParams = generatePartitionProjectionParameters(columnName, config); + + // Merge into this.parameters + Object.assign(this.parameters, generatedParams); + } + + // Enable partition projection globally + this.parameters['projection.enabled'] = 'true'; + } + /** * Grant the given identity custom permissions. */ diff --git a/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.js.snapshot/GluePartitionProjectionTestDefaultTestDeployAssert9180F049.assets.json b/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.js.snapshot/GluePartitionProjectionTestDefaultTestDeployAssert9180F049.assets.json new file mode 100644 index 0000000000000..6d54e8b0f97ec --- /dev/null +++ b/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.js.snapshot/GluePartitionProjectionTestDefaultTestDeployAssert9180F049.assets.json @@ -0,0 +1,20 @@ +{ + "version": "48.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "displayName": "GluePartitionProjectionTestDefaultTestDeployAssert9180F049 Template", + "source": { + "path": "GluePartitionProjectionTestDefaultTestDeployAssert9180F049.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region-d8d86b35": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.js.snapshot/GluePartitionProjectionTestDefaultTestDeployAssert9180F049.template.json b/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.js.snapshot/GluePartitionProjectionTestDefaultTestDeployAssert9180F049.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.js.snapshot/GluePartitionProjectionTestDefaultTestDeployAssert9180F049.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.js.snapshot/aws-cdk-glue-partition-projection.assets.json b/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.js.snapshot/aws-cdk-glue-partition-projection.assets.json new file mode 100644 index 0000000000000..41d3d9ab7a745 --- /dev/null +++ b/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.js.snapshot/aws-cdk-glue-partition-projection.assets.json @@ -0,0 +1,20 @@ +{ + "version": "48.0.0", + "files": { + "f0b62fe7856df25f96634ec796c98551b0d4264ff09de58b08a89cc5e53a6808": { + "displayName": "aws-cdk-glue-partition-projection Template", + "source": { + "path": "aws-cdk-glue-partition-projection.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region-8219f245": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "f0b62fe7856df25f96634ec796c98551b0d4264ff09de58b08a89cc5e53a6808.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.js.snapshot/aws-cdk-glue-partition-projection.template.json b/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.js.snapshot/aws-cdk-glue-partition-projection.template.json new file mode 100644 index 0000000000000..dd45f62288829 --- /dev/null +++ b/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.js.snapshot/aws-cdk-glue-partition-projection.template.json @@ -0,0 +1,426 @@ +{ + "Resources": { + "DatabaseB269D8BB": { + "Type": "AWS::Glue::Database", + "Properties": { + "CatalogId": { + "Ref": "AWS::AccountId" + }, + "DatabaseInput": { + "Name": "partition_projection_test" + } + } + }, + "TableIntegerBucketD9685175": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "TableIntegerTableFDD1B215": { + "Type": "AWS::Glue::Table", + "Properties": { + "CatalogId": { + "Ref": "AWS::AccountId" + }, + "DatabaseName": { + "Ref": "DatabaseB269D8BB" + }, + "TableInput": { + "Description": "integer_projection generated by CDK", + "Name": "integer_projection", + "Parameters": { + "classification": "json", + "has_encrypted_data": true, + "projection.year.type": "integer", + "projection.year.range": "2020,2023", + "projection.year.interval": "1", + "projection.year.digits": "4", + "projection.enabled": "true" + }, + "PartitionKeys": [ + { + "Name": "year", + "Type": "int" + } + ], + "StorageDescriptor": { + "Columns": [ + { + "Name": "data", + "Type": "string" + } + ], + "Compressed": false, + "InputFormat": "org.apache.hadoop.mapred.TextInputFormat", + "Location": { + "Fn::Join": [ + "", + [ + "s3://", + { + "Ref": "TableIntegerBucketD9685175" + }, + "/" + ] + ] + }, + "OutputFormat": "org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat", + "SerdeInfo": { + "SerializationLibrary": "org.openx.data.jsonserde.JsonSerDe" + }, + "StoredAsSubDirectories": false + }, + "TableType": "EXTERNAL_TABLE" + } + } + }, + "TableDateBucket859C392D": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "TableDateTable95A0F4A1": { + "Type": "AWS::Glue::Table", + "Properties": { + "CatalogId": { + "Ref": "AWS::AccountId" + }, + "DatabaseName": { + "Ref": "DatabaseB269D8BB" + }, + "TableInput": { + "Description": "date_projection generated by CDK", + "Name": "date_projection", + "Parameters": { + "classification": "json", + "has_encrypted_data": true, + "projection.date.type": "date", + "projection.date.range": "2020-01-01,2023-12-31", + "projection.date.format": "yyyy-MM-dd", + "projection.date.interval": "1", + "projection.date.interval.unit": "DAYS", + "projection.enabled": "true" + }, + "PartitionKeys": [ + { + "Name": "date", + "Type": "string" + } + ], + "StorageDescriptor": { + "Columns": [ + { + "Name": "data", + "Type": "string" + } + ], + "Compressed": false, + "InputFormat": "org.apache.hadoop.mapred.TextInputFormat", + "Location": { + "Fn::Join": [ + "", + [ + "s3://", + { + "Ref": "TableDateBucket859C392D" + }, + "/" + ] + ] + }, + "OutputFormat": "org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat", + "SerdeInfo": { + "SerializationLibrary": "org.openx.data.jsonserde.JsonSerDe" + }, + "StoredAsSubDirectories": false + }, + "TableType": "EXTERNAL_TABLE" + } + } + }, + "TableEnumBucket1019891D": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "TableEnumTableF18874FE": { + "Type": "AWS::Glue::Table", + "Properties": { + "CatalogId": { + "Ref": "AWS::AccountId" + }, + "DatabaseName": { + "Ref": "DatabaseB269D8BB" + }, + "TableInput": { + "Description": "enum_projection generated by CDK", + "Name": "enum_projection", + "Parameters": { + "classification": "json", + "has_encrypted_data": true, + "projection.region.type": "enum", + "projection.region.values": "us-east-1,us-west-2,eu-west-1", + "projection.enabled": "true" + }, + "PartitionKeys": [ + { + "Name": "region", + "Type": "string" + } + ], + "StorageDescriptor": { + "Columns": [ + { + "Name": "data", + "Type": "string" + } + ], + "Compressed": false, + "InputFormat": "org.apache.hadoop.mapred.TextInputFormat", + "Location": { + "Fn::Join": [ + "", + [ + "s3://", + { + "Ref": "TableEnumBucket1019891D" + }, + "/" + ] + ] + }, + "OutputFormat": "org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat", + "SerdeInfo": { + "SerializationLibrary": "org.openx.data.jsonserde.JsonSerDe" + }, + "StoredAsSubDirectories": false + }, + "TableType": "EXTERNAL_TABLE" + } + } + }, + "TableInjectedBucketD6778F64": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "TableInjectedTable7165BACE": { + "Type": "AWS::Glue::Table", + "Properties": { + "CatalogId": { + "Ref": "AWS::AccountId" + }, + "DatabaseName": { + "Ref": "DatabaseB269D8BB" + }, + "TableInput": { + "Description": "injected_projection generated by CDK", + "Name": "injected_projection", + "Parameters": { + "classification": "json", + "has_encrypted_data": true, + "projection.custom.type": "injected", + "projection.enabled": "true" + }, + "PartitionKeys": [ + { + "Name": "custom", + "Type": "string" + } + ], + "StorageDescriptor": { + "Columns": [ + { + "Name": "data", + "Type": "string" + } + ], + "Compressed": false, + "InputFormat": "org.apache.hadoop.mapred.TextInputFormat", + "Location": { + "Fn::Join": [ + "", + [ + "s3://", + { + "Ref": "TableInjectedBucketD6778F64" + }, + "/" + ] + ] + }, + "OutputFormat": "org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat", + "SerdeInfo": { + "SerializationLibrary": "org.openx.data.jsonserde.JsonSerDe" + }, + "StoredAsSubDirectories": false + }, + "TableType": "EXTERNAL_TABLE" + } + } + }, + "TableMultipleBucketC9DE89BA": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "TableMultipleTable3A543510": { + "Type": "AWS::Glue::Table", + "Properties": { + "CatalogId": { + "Ref": "AWS::AccountId" + }, + "DatabaseName": { + "Ref": "DatabaseB269D8BB" + }, + "TableInput": { + "Description": "multiple_projection generated by CDK", + "Name": "multiple_projection", + "Parameters": { + "classification": "json", + "has_encrypted_data": true, + "projection.year.type": "integer", + "projection.year.range": "2020,2023", + "projection.month.type": "integer", + "projection.month.range": "1,12", + "projection.month.digits": "2", + "projection.region.type": "enum", + "projection.region.values": "us-east-1,us-west-2", + "projection.enabled": "true" + }, + "PartitionKeys": [ + { + "Name": "year", + "Type": "int" + }, + { + "Name": "month", + "Type": "int" + }, + { + "Name": "region", + "Type": "string" + } + ], + "StorageDescriptor": { + "Columns": [ + { + "Name": "data", + "Type": "string" + } + ], + "Compressed": false, + "InputFormat": "org.apache.hadoop.mapred.TextInputFormat", + "Location": { + "Fn::Join": [ + "", + [ + "s3://", + { + "Ref": "TableMultipleBucketC9DE89BA" + }, + "/" + ] + ] + }, + "OutputFormat": "org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat", + "SerdeInfo": { + "SerializationLibrary": "org.openx.data.jsonserde.JsonSerDe" + }, + "StoredAsSubDirectories": false + }, + "TableType": "EXTERNAL_TABLE" + } + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.js.snapshot/cdk.out b/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.js.snapshot/cdk.out new file mode 100644 index 0000000000000..523a9aac37cbf --- /dev/null +++ b/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"48.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.js.snapshot/integ.json b/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.js.snapshot/integ.json new file mode 100644 index 0000000000000..43ede4f606c9a --- /dev/null +++ b/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.js.snapshot/integ.json @@ -0,0 +1,13 @@ +{ + "version": "48.0.0", + "testCases": { + "GluePartitionProjectionTest/DefaultTest": { + "stacks": [ + "aws-cdk-glue-partition-projection" + ], + "assertionStack": "GluePartitionProjectionTest/DefaultTest/DeployAssert", + "assertionStackName": "GluePartitionProjectionTestDefaultTestDeployAssert9180F049" + } + }, + "minimumCliVersion": "2.1027.0" +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.js.snapshot/manifest.json b/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.js.snapshot/manifest.json new file mode 100644 index 0000000000000..9a38dd95b813d --- /dev/null +++ b/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.js.snapshot/manifest.json @@ -0,0 +1,733 @@ +{ + "version": "48.0.0", + "artifacts": { + "aws-cdk-glue-partition-projection.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "aws-cdk-glue-partition-projection.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "aws-cdk-glue-partition-projection": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "aws-cdk-glue-partition-projection.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/f0b62fe7856df25f96634ec796c98551b0d4264ff09de58b08a89cc5e53a6808.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "aws-cdk-glue-partition-projection.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "aws-cdk-glue-partition-projection.assets" + ], + "metadata": { + "/aws-cdk-glue-partition-projection/Database": [ + { + "type": "aws:cdk:analytics:construct", + "data": "*" + } + ], + "/aws-cdk-glue-partition-projection/Database/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "DatabaseB269D8BB" + } + ], + "/aws-cdk-glue-partition-projection/TableInteger": [ + { + "type": "aws:cdk:analytics:construct", + "data": "*" + } + ], + "/aws-cdk-glue-partition-projection/TableInteger/Bucket": [ + { + "type": "aws:cdk:analytics:construct", + "data": { + "encryption": "S3_MANAGED", + "encryptionKey": "*" + } + } + ], + "/aws-cdk-glue-partition-projection/TableInteger/Bucket/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "TableIntegerBucketD9685175" + } + ], + "/aws-cdk-glue-partition-projection/TableInteger/Table": [ + { + "type": "aws:cdk:logicalId", + "data": "TableIntegerTableFDD1B215" + } + ], + "/aws-cdk-glue-partition-projection/TableDate": [ + { + "type": "aws:cdk:analytics:construct", + "data": "*" + } + ], + "/aws-cdk-glue-partition-projection/TableDate/Bucket": [ + { + "type": "aws:cdk:analytics:construct", + "data": { + "encryption": "S3_MANAGED", + "encryptionKey": "*" + } + } + ], + "/aws-cdk-glue-partition-projection/TableDate/Bucket/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "TableDateBucket859C392D" + } + ], + "/aws-cdk-glue-partition-projection/TableDate/Table": [ + { + "type": "aws:cdk:logicalId", + "data": "TableDateTable95A0F4A1" + } + ], + "/aws-cdk-glue-partition-projection/TableEnum": [ + { + "type": "aws:cdk:analytics:construct", + "data": "*" + } + ], + "/aws-cdk-glue-partition-projection/TableEnum/Bucket": [ + { + "type": "aws:cdk:analytics:construct", + "data": { + "encryption": "S3_MANAGED", + "encryptionKey": "*" + } + } + ], + "/aws-cdk-glue-partition-projection/TableEnum/Bucket/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "TableEnumBucket1019891D" + } + ], + "/aws-cdk-glue-partition-projection/TableEnum/Table": [ + { + "type": "aws:cdk:logicalId", + "data": "TableEnumTableF18874FE" + } + ], + "/aws-cdk-glue-partition-projection/TableInjected": [ + { + "type": "aws:cdk:analytics:construct", + "data": "*" + } + ], + "/aws-cdk-glue-partition-projection/TableInjected/Bucket": [ + { + "type": "aws:cdk:analytics:construct", + "data": { + "encryption": "S3_MANAGED", + "encryptionKey": "*" + } + } + ], + "/aws-cdk-glue-partition-projection/TableInjected/Bucket/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "TableInjectedBucketD6778F64" + } + ], + "/aws-cdk-glue-partition-projection/TableInjected/Table": [ + { + "type": "aws:cdk:logicalId", + "data": "TableInjectedTable7165BACE" + } + ], + "/aws-cdk-glue-partition-projection/TableMultiple": [ + { + "type": "aws:cdk:analytics:construct", + "data": "*" + } + ], + "/aws-cdk-glue-partition-projection/TableMultiple/Bucket": [ + { + "type": "aws:cdk:analytics:construct", + "data": { + "encryption": "S3_MANAGED", + "encryptionKey": "*" + } + } + ], + "/aws-cdk-glue-partition-projection/TableMultiple/Bucket/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "TableMultipleBucketC9DE89BA" + } + ], + "/aws-cdk-glue-partition-projection/TableMultiple/Table": [ + { + "type": "aws:cdk:logicalId", + "data": "TableMultipleTable3A543510" + } + ], + "/aws-cdk-glue-partition-projection/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/aws-cdk-glue-partition-projection/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "aws-cdk-glue-partition-projection" + }, + "GluePartitionProjectionTestDefaultTestDeployAssert9180F049.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "GluePartitionProjectionTestDefaultTestDeployAssert9180F049.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "GluePartitionProjectionTestDefaultTestDeployAssert9180F049": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "GluePartitionProjectionTestDefaultTestDeployAssert9180F049.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "GluePartitionProjectionTestDefaultTestDeployAssert9180F049.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "GluePartitionProjectionTestDefaultTestDeployAssert9180F049.assets" + ], + "metadata": { + "/GluePartitionProjectionTest/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/GluePartitionProjectionTest/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "GluePartitionProjectionTest/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + }, + "aws-cdk-lib/feature-flag-report": { + "type": "cdk:feature-flag-report", + "properties": { + "module": "aws-cdk-lib", + "flags": { + "@aws-cdk/aws-signer:signingProfileNamePassedToCfn": { + "recommendedValue": true, + "explanation": "Pass signingProfileName to CfnSigningProfile" + }, + "@aws-cdk/core:newStyleStackSynthesis": { + "recommendedValue": true, + "explanation": "Switch to new stack synthesis method which enables CI/CD", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/core:stackRelativeExports": { + "recommendedValue": true, + "explanation": "Name exports based on the construct paths relative to the stack, rather than the global construct path", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/aws-ecs-patterns:secGroupsDisablesImplicitOpenListener": { + "recommendedValue": true, + "explanation": "Disable implicit openListener when custom security groups are provided" + }, + "@aws-cdk/aws-rds:lowercaseDbIdentifier": { + "recommendedValue": true, + "explanation": "Force lowercasing of RDS Cluster names in CDK", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": { + "recommendedValue": true, + "explanation": "Allow adding/removing multiple UsagePlanKeys independently", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/aws-lambda:recognizeVersionProps": { + "recommendedValue": true, + "explanation": "Enable this feature flag to opt in to the updated logical id calculation for Lambda Version created using the `fn.currentVersion`.", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/aws-lambda:recognizeLayerVersion": { + "userValue": true, + "recommendedValue": true, + "explanation": "Enable this feature flag to opt in to the updated logical id calculation for Lambda Version created using the `fn.currentVersion`." + }, + "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": { + "recommendedValue": true, + "explanation": "Enable this feature flag to have cloudfront distributions use the security policy TLSv1.2_2021 by default.", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/core:checkSecretUsage": { + "userValue": true, + "recommendedValue": true, + "explanation": "Enable this flag to make it impossible to accidentally use SecretValues in unsafe locations" + }, + "@aws-cdk/core:target-partitions": { + "recommendedValue": [ + "aws", + "aws-cn" + ], + "explanation": "What regions to include in lookup tables of environment agnostic stacks" + }, + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": { + "userValue": true, + "recommendedValue": true, + "explanation": "ECS extensions will automatically add an `awslogs` driver if no logging is specified" + }, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": { + "userValue": true, + "recommendedValue": true, + "explanation": "Enable this feature flag to have Launch Templates generated by the `InstanceRequireImdsv2Aspect` use unique names." + }, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": { + "userValue": true, + "recommendedValue": true, + "explanation": "ARN format used by ECS. In the new ARN format, the cluster name is part of the resource ID." + }, + "@aws-cdk/aws-iam:minimizePolicies": { + "userValue": true, + "recommendedValue": true, + "explanation": "Minimize IAM policies by combining Statements" + }, + "@aws-cdk/core:validateSnapshotRemovalPolicy": { + "userValue": true, + "recommendedValue": true, + "explanation": "Error on snapshot removal policies on resources that do not support it." + }, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": { + "userValue": true, + "recommendedValue": true, + "explanation": "Generate key aliases that include the stack name" + }, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": { + "userValue": true, + "recommendedValue": true, + "explanation": "Enable this feature flag to create an S3 bucket policy by default in cases where an AWS service would automatically create the Policy if one does not exist." + }, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": { + "userValue": true, + "recommendedValue": true, + "explanation": "Restrict KMS key policy for encrypted Queues a bit more" + }, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": { + "userValue": true, + "recommendedValue": true, + "explanation": "Make default CloudWatch Role behavior safe for multiple API Gateways in one environment" + }, + "@aws-cdk/core:enablePartitionLiterals": { + "userValue": true, + "recommendedValue": true, + "explanation": "Make ARNs concrete if AWS partition is known" + }, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": { + "userValue": true, + "recommendedValue": true, + "explanation": "Event Rules may only push to encrypted SQS queues in the same account" + }, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": { + "userValue": true, + "recommendedValue": true, + "explanation": "Avoid setting the \"ECS\" deployment controller when adding a circuit breaker" + }, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": { + "userValue": true, + "recommendedValue": true, + "explanation": "Enable this feature to by default create default policy names for imported roles that depend on the stack the role is in." + }, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": { + "userValue": true, + "recommendedValue": true, + "explanation": "Use S3 Bucket Policy instead of ACLs for Server Access Logging" + }, + "@aws-cdk/aws-route53-patters:useCertificate": { + "userValue": true, + "recommendedValue": true, + "explanation": "Use the official `Certificate` resource instead of `DnsValidatedCertificate`" + }, + "@aws-cdk/customresources:installLatestAwsSdkDefault": { + "userValue": false, + "recommendedValue": false, + "explanation": "Whether to install the latest SDK by default in AwsCustomResource" + }, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": { + "userValue": true, + "recommendedValue": true, + "explanation": "Use unique resource name for Database Proxy" + }, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": { + "userValue": true, + "recommendedValue": true, + "explanation": "Remove CloudWatch alarms from deployment group" + }, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": { + "userValue": true, + "recommendedValue": true, + "explanation": "Include authorizer configuration in the calculation of the API deployment logical ID." + }, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": { + "userValue": true, + "recommendedValue": true, + "explanation": "Define user data for a launch template by default when a machine image is provided." + }, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": { + "userValue": true, + "recommendedValue": true, + "explanation": "SecretTargetAttachments uses the ResourcePolicy of the attached Secret." + }, + "@aws-cdk/aws-redshift:columnId": { + "userValue": true, + "recommendedValue": true, + "explanation": "Whether to use an ID to track Redshift column changes" + }, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": { + "userValue": true, + "recommendedValue": true, + "explanation": "Enable AmazonEMRServicePolicy_v2 managed policies" + }, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": { + "userValue": true, + "recommendedValue": true, + "explanation": "Restrict access to the VPC default security group" + }, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": { + "userValue": true, + "recommendedValue": true, + "explanation": "Generate a unique id for each RequestValidator added to a method" + }, + "@aws-cdk/aws-kms:aliasNameRef": { + "userValue": true, + "recommendedValue": true, + "explanation": "KMS Alias name and keyArn will have implicit reference to KMS Key" + }, + "@aws-cdk/aws-kms:applyImportedAliasPermissionsToPrincipal": { + "userValue": true, + "recommendedValue": true, + "explanation": "Enable grant methods on Aliases imported by name to use kms:ResourceAliases condition" + }, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": { + "userValue": true, + "recommendedValue": true, + "explanation": "Generate a launch template when creating an AutoScalingGroup" + }, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": { + "userValue": true, + "recommendedValue": true, + "explanation": "Include the stack prefix in the stack name generation process" + }, + "@aws-cdk/aws-efs:denyAnonymousAccess": { + "userValue": true, + "recommendedValue": true, + "explanation": "EFS denies anonymous clients accesses" + }, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": { + "userValue": true, + "recommendedValue": true, + "explanation": "Enables support for Multi-AZ with Standby deployment for opensearch domains" + }, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": { + "userValue": true, + "recommendedValue": true, + "explanation": "Enables aws-lambda-nodejs.Function to use the latest available NodeJs runtime as the default" + }, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, mount targets will have a stable logicalId that is linked to the associated subnet." + }, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, a scope of InstanceParameterGroup for AuroraClusterInstance with each parameters will change." + }, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, will always use the arn for identifiers for CfnSourceApiAssociation in the GraphqlApi construct rather than id." + }, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, creating an RDS database cluster from a snapshot will only render credentials for snapshot credentials." + }, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, the CodeCommit source action is using the default branch name 'main'." + }, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, the logical ID of a Lambda permission for a Lambda action includes an alarm ID." + }, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": { + "userValue": true, + "recommendedValue": true, + "explanation": "Enables Pipeline to set the default value for crossAccountKeys to false." + }, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": { + "userValue": true, + "recommendedValue": true, + "explanation": "Enables Pipeline to set the default pipeline type to V2." + }, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, IAM Policy created from KMS key grant will reduce the resource scope to this key only." + }, + "@aws-cdk/pipelines:reduceAssetRoleTrustScope": { + "recommendedValue": true, + "explanation": "Remove the root account principal from PipelineAssetsFileRole trust policy", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/aws-eks:nodegroupNameAttribute": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, nodegroupName attribute of the provisioned EKS NodeGroup will not have the cluster name prefix." + }, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, the default volume type of the EBS volume will be GP3" + }, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, remove default deployment alarm settings" + }, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": { + "userValue": false, + "recommendedValue": false, + "explanation": "When enabled, the custom resource used for `AwsCustomResource` will configure the `logApiResponseData` property as true by default" + }, + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": { + "userValue": false, + "recommendedValue": false, + "explanation": "When enabled, Adding notifications to a bucket in the current stack will not remove notification from imported stack." + }, + "@aws-cdk/aws-stepfunctions-tasks:useNewS3UriParametersForBedrockInvokeModelTask": { + "recommendedValue": true, + "explanation": "When enabled, use new props for S3 URI field in task definition of state machine for bedrock invoke model.", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/core:explicitStackTags": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, stack tags need to be assigned explicitly on a Stack." + }, + "@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": { + "userValue": false, + "recommendedValue": false, + "explanation": "When set to true along with canContainersAccessInstanceRole=false in ECS cluster, new updated commands will be added to UserData to block container accessing IMDS. **Applicable to Linux only. IMPORTANT: See [details.](#aws-cdkaws-ecsenableImdsBlockingDeprecatedFeature)**" + }, + "@aws-cdk/aws-ecs:disableEcsImdsBlocking": { + "userValue": true, + "recommendedValue": true, + "explanation": "When set to true, CDK synth will throw exception if canContainersAccessInstanceRole is false. **IMPORTANT: See [details.](#aws-cdkaws-ecsdisableEcsImdsBlocking)**" + }, + "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, we will only grant the necessary permissions when users specify cloudwatch log group through logConfiguration" + }, + "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled will allow you to specify a resource policy per replica, and not copy the source table policy to all replicas" + }, + "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, initOptions.timeout and resourceSignalTimeout values will be summed together." + }, + "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, a Lambda authorizer Permission created when using GraphqlApi will be properly scoped with a SourceArn." + }, + "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, the value of property `instanceResourceId` in construct `DatabaseInstanceReadReplica` will be set to the correct value which is `DbiResourceId` instead of currently `DbInstanceArn`" + }, + "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, CFN templates added with `cfn-include` will error if the template contains Resource Update or Create policies with CFN Intrinsics that include non-primitive values." + }, + "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, both `@aws-sdk` and `@smithy` packages will be excluded from the Lambda Node.js 18.x runtime to prevent version mismatches in bundled applications." + }, + "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, the resource of IAM Run Ecs policy generated by SFN EcsRunTask will reference the definition, instead of constructing ARN." + }, + "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, the BastionHost construct will use the latest Amazon Linux 2023 AMI, instead of Amazon Linux 2." + }, + "@aws-cdk/core:aspectStabilization": { + "recommendedValue": true, + "explanation": "When enabled, a stabilization loop will be run when invoking Aspects during synthesis.", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, use a new method for DNS Name of user pool domain target without creating a custom resource." + }, + "@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, the default security group ingress rules will allow IPv6 ingress from anywhere" + }, + "@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, the default behaviour of OIDC provider will reject unauthorized connections" + }, + "@aws-cdk/core:enableAdditionalMetadataCollection": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, CDK will expand the scope of usage data collected to better inform CDK development and improve communication for security concerns and emerging issues." + }, + "@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": { + "userValue": false, + "recommendedValue": false, + "explanation": "[Deprecated] When enabled, Lambda will create new inline policies with AddToRolePolicy instead of adding to the Default Policy Statement" + }, + "@aws-cdk/aws-s3:setUniqueReplicationRoleName": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, CDK will automatically generate a unique role name that is used for s3 object replication." + }, + "@aws-cdk/pipelines:reduceStageRoleTrustScope": { + "recommendedValue": true, + "explanation": "Remove the root account principal from Stage addActions trust policy", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/aws-events:requireEventBusPolicySid": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, grantPutEventsTo() will use resource policies with Statement IDs for service principals." + }, + "@aws-cdk/core:aspectPrioritiesMutating": { + "userValue": true, + "recommendedValue": true, + "explanation": "When set to true, Aspects added by the construct library on your behalf will be given a priority of MUTATING." + }, + "@aws-cdk/aws-dynamodb:retainTableReplica": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, table replica will be default to the removal policy of source table unless specified otherwise." + }, + "@aws-cdk/cognito:logUserPoolClientSecretValue": { + "recommendedValue": false, + "explanation": "When disabled, the value of the user pool client secret will not be logged in the custom resource lambda function logs." + }, + "@aws-cdk/pipelines:reduceCrossAccountActionRoleTrustScope": { + "recommendedValue": true, + "explanation": "When enabled, scopes down the trust policy for the cross-account action role", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/aws-stepfunctions:useDistributedMapResultWriterV2": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, the resultWriterV2 property of DistributedMap will be used insted of resultWriter" + }, + "@aws-cdk/s3-notifications:addS3TrustKeyPolicyForSnsSubscriptions": { + "userValue": true, + "recommendedValue": true, + "explanation": "Add an S3 trust policy to a KMS key resource policy for SNS subscriptions." + }, + "@aws-cdk/aws-ec2:requirePrivateSubnetsForEgressOnlyInternetGateway": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, the EgressOnlyGateway resource is only created if private subnets are defined in the dual-stack VPC." + }, + "@aws-cdk/aws-ec2-alpha:useResourceIdForVpcV2Migration": { + "recommendedValue": false, + "explanation": "When enabled, use resource IDs for VPC V2 migration" + }, + "@aws-cdk/aws-s3:publicAccessBlockedByDefault": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, setting any combination of options for BlockPublicAccess will automatically set true for any options not defined." + }, + "@aws-cdk/aws-lambda:useCdkManagedLogGroup": { + "userValue": true, + "recommendedValue": true, + "explanation": "When enabled, CDK creates and manages loggroup for the lambda function" + } + } + } + } + }, + "minimumCliVersion": "2.1027.0" +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.js.snapshot/tree.json b/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.js.snapshot/tree.json new file mode 100644 index 0000000000000..96f068a5b7f4f --- /dev/null +++ b/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.js.snapshot/tree.json @@ -0,0 +1 @@ +{"version":"tree-0.1","tree":{"id":"App","path":"","constructInfo":{"fqn":"aws-cdk-lib.App","version":"0.0.0"},"children":{"aws-cdk-glue-partition-projection":{"id":"aws-cdk-glue-partition-projection","path":"aws-cdk-glue-partition-projection","constructInfo":{"fqn":"aws-cdk-lib.Stack","version":"0.0.0"},"children":{"Database":{"id":"Database","path":"aws-cdk-glue-partition-projection/Database","constructInfo":{"fqn":"@aws-cdk/aws-glue-alpha.Database","version":"0.0.0","metadata":["*"]},"children":{"Resource":{"id":"Resource","path":"aws-cdk-glue-partition-projection/Database/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_glue.CfnDatabase","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Glue::Database","aws:cdk:cloudformation:props":{"catalogId":{"Ref":"AWS::AccountId"},"databaseInput":{"name":"partition_projection_test"}}}}}},"TableInteger":{"id":"TableInteger","path":"aws-cdk-glue-partition-projection/TableInteger","constructInfo":{"fqn":"@aws-cdk/aws-glue-alpha.S3Table","version":"0.0.0","metadata":["*"]},"children":{"Bucket":{"id":"Bucket","path":"aws-cdk-glue-partition-projection/TableInteger/Bucket","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.Bucket","version":"0.0.0","metadata":[{"encryption":"S3_MANAGED","encryptionKey":"*"}]},"children":{"Resource":{"id":"Resource","path":"aws-cdk-glue-partition-projection/TableInteger/Bucket/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.CfnBucket","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::S3::Bucket","aws:cdk:cloudformation:props":{"bucketEncryption":{"serverSideEncryptionConfiguration":[{"serverSideEncryptionByDefault":{"sseAlgorithm":"AES256"}}]}}}}}},"Table":{"id":"Table","path":"aws-cdk-glue-partition-projection/TableInteger/Table","constructInfo":{"fqn":"aws-cdk-lib.aws_glue.CfnTable","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Glue::Table","aws:cdk:cloudformation:props":{"catalogId":{"Ref":"AWS::AccountId"},"databaseName":{"Ref":"DatabaseB269D8BB"},"tableInput":{"name":"integer_projection","description":"integer_projection generated by CDK","partitionKeys":[{"name":"year","type":"int"}],"parameters":{"classification":"json","has_encrypted_data":true,"projection.year.type":"integer","projection.year.range":"2020,2023","projection.year.interval":"1","projection.year.digits":"4","projection.enabled":"true"},"storageDescriptor":{"location":{"Fn::Join":["",["s3://",{"Ref":"TableIntegerBucketD9685175"},"/"]]},"compressed":false,"storedAsSubDirectories":false,"columns":[{"name":"data","type":"string"}],"inputFormat":"org.apache.hadoop.mapred.TextInputFormat","outputFormat":"org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat","serdeInfo":{"serializationLibrary":"org.openx.data.jsonserde.JsonSerDe"}},"tableType":"EXTERNAL_TABLE"}}}}}},"TableDate":{"id":"TableDate","path":"aws-cdk-glue-partition-projection/TableDate","constructInfo":{"fqn":"@aws-cdk/aws-glue-alpha.S3Table","version":"0.0.0","metadata":["*"]},"children":{"Bucket":{"id":"Bucket","path":"aws-cdk-glue-partition-projection/TableDate/Bucket","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.Bucket","version":"0.0.0","metadata":[{"encryption":"S3_MANAGED","encryptionKey":"*"}]},"children":{"Resource":{"id":"Resource","path":"aws-cdk-glue-partition-projection/TableDate/Bucket/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.CfnBucket","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::S3::Bucket","aws:cdk:cloudformation:props":{"bucketEncryption":{"serverSideEncryptionConfiguration":[{"serverSideEncryptionByDefault":{"sseAlgorithm":"AES256"}}]}}}}}},"Table":{"id":"Table","path":"aws-cdk-glue-partition-projection/TableDate/Table","constructInfo":{"fqn":"aws-cdk-lib.aws_glue.CfnTable","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Glue::Table","aws:cdk:cloudformation:props":{"catalogId":{"Ref":"AWS::AccountId"},"databaseName":{"Ref":"DatabaseB269D8BB"},"tableInput":{"name":"date_projection","description":"date_projection generated by CDK","partitionKeys":[{"name":"date","type":"string"}],"parameters":{"classification":"json","has_encrypted_data":true,"projection.date.type":"date","projection.date.range":"2020-01-01,2023-12-31","projection.date.format":"yyyy-MM-dd","projection.date.interval":"1","projection.date.interval.unit":"DAYS","projection.enabled":"true"},"storageDescriptor":{"location":{"Fn::Join":["",["s3://",{"Ref":"TableDateBucket859C392D"},"/"]]},"compressed":false,"storedAsSubDirectories":false,"columns":[{"name":"data","type":"string"}],"inputFormat":"org.apache.hadoop.mapred.TextInputFormat","outputFormat":"org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat","serdeInfo":{"serializationLibrary":"org.openx.data.jsonserde.JsonSerDe"}},"tableType":"EXTERNAL_TABLE"}}}}}},"TableEnum":{"id":"TableEnum","path":"aws-cdk-glue-partition-projection/TableEnum","constructInfo":{"fqn":"@aws-cdk/aws-glue-alpha.S3Table","version":"0.0.0","metadata":["*"]},"children":{"Bucket":{"id":"Bucket","path":"aws-cdk-glue-partition-projection/TableEnum/Bucket","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.Bucket","version":"0.0.0","metadata":[{"encryption":"S3_MANAGED","encryptionKey":"*"}]},"children":{"Resource":{"id":"Resource","path":"aws-cdk-glue-partition-projection/TableEnum/Bucket/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.CfnBucket","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::S3::Bucket","aws:cdk:cloudformation:props":{"bucketEncryption":{"serverSideEncryptionConfiguration":[{"serverSideEncryptionByDefault":{"sseAlgorithm":"AES256"}}]}}}}}},"Table":{"id":"Table","path":"aws-cdk-glue-partition-projection/TableEnum/Table","constructInfo":{"fqn":"aws-cdk-lib.aws_glue.CfnTable","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Glue::Table","aws:cdk:cloudformation:props":{"catalogId":{"Ref":"AWS::AccountId"},"databaseName":{"Ref":"DatabaseB269D8BB"},"tableInput":{"name":"enum_projection","description":"enum_projection generated by CDK","partitionKeys":[{"name":"region","type":"string"}],"parameters":{"classification":"json","has_encrypted_data":true,"projection.region.type":"enum","projection.region.values":"us-east-1,us-west-2,eu-west-1","projection.enabled":"true"},"storageDescriptor":{"location":{"Fn::Join":["",["s3://",{"Ref":"TableEnumBucket1019891D"},"/"]]},"compressed":false,"storedAsSubDirectories":false,"columns":[{"name":"data","type":"string"}],"inputFormat":"org.apache.hadoop.mapred.TextInputFormat","outputFormat":"org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat","serdeInfo":{"serializationLibrary":"org.openx.data.jsonserde.JsonSerDe"}},"tableType":"EXTERNAL_TABLE"}}}}}},"TableInjected":{"id":"TableInjected","path":"aws-cdk-glue-partition-projection/TableInjected","constructInfo":{"fqn":"@aws-cdk/aws-glue-alpha.S3Table","version":"0.0.0","metadata":["*"]},"children":{"Bucket":{"id":"Bucket","path":"aws-cdk-glue-partition-projection/TableInjected/Bucket","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.Bucket","version":"0.0.0","metadata":[{"encryption":"S3_MANAGED","encryptionKey":"*"}]},"children":{"Resource":{"id":"Resource","path":"aws-cdk-glue-partition-projection/TableInjected/Bucket/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.CfnBucket","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::S3::Bucket","aws:cdk:cloudformation:props":{"bucketEncryption":{"serverSideEncryptionConfiguration":[{"serverSideEncryptionByDefault":{"sseAlgorithm":"AES256"}}]}}}}}},"Table":{"id":"Table","path":"aws-cdk-glue-partition-projection/TableInjected/Table","constructInfo":{"fqn":"aws-cdk-lib.aws_glue.CfnTable","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Glue::Table","aws:cdk:cloudformation:props":{"catalogId":{"Ref":"AWS::AccountId"},"databaseName":{"Ref":"DatabaseB269D8BB"},"tableInput":{"name":"injected_projection","description":"injected_projection generated by CDK","partitionKeys":[{"name":"custom","type":"string"}],"parameters":{"classification":"json","has_encrypted_data":true,"projection.custom.type":"injected","projection.enabled":"true"},"storageDescriptor":{"location":{"Fn::Join":["",["s3://",{"Ref":"TableInjectedBucketD6778F64"},"/"]]},"compressed":false,"storedAsSubDirectories":false,"columns":[{"name":"data","type":"string"}],"inputFormat":"org.apache.hadoop.mapred.TextInputFormat","outputFormat":"org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat","serdeInfo":{"serializationLibrary":"org.openx.data.jsonserde.JsonSerDe"}},"tableType":"EXTERNAL_TABLE"}}}}}},"TableMultiple":{"id":"TableMultiple","path":"aws-cdk-glue-partition-projection/TableMultiple","constructInfo":{"fqn":"@aws-cdk/aws-glue-alpha.S3Table","version":"0.0.0","metadata":["*"]},"children":{"Bucket":{"id":"Bucket","path":"aws-cdk-glue-partition-projection/TableMultiple/Bucket","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.Bucket","version":"0.0.0","metadata":[{"encryption":"S3_MANAGED","encryptionKey":"*"}]},"children":{"Resource":{"id":"Resource","path":"aws-cdk-glue-partition-projection/TableMultiple/Bucket/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.CfnBucket","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::S3::Bucket","aws:cdk:cloudformation:props":{"bucketEncryption":{"serverSideEncryptionConfiguration":[{"serverSideEncryptionByDefault":{"sseAlgorithm":"AES256"}}]}}}}}},"Table":{"id":"Table","path":"aws-cdk-glue-partition-projection/TableMultiple/Table","constructInfo":{"fqn":"aws-cdk-lib.aws_glue.CfnTable","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Glue::Table","aws:cdk:cloudformation:props":{"catalogId":{"Ref":"AWS::AccountId"},"databaseName":{"Ref":"DatabaseB269D8BB"},"tableInput":{"name":"multiple_projection","description":"multiple_projection generated by CDK","partitionKeys":[{"name":"year","type":"int"},{"name":"month","type":"int"},{"name":"region","type":"string"}],"parameters":{"classification":"json","has_encrypted_data":true,"projection.year.type":"integer","projection.year.range":"2020,2023","projection.month.type":"integer","projection.month.range":"1,12","projection.month.digits":"2","projection.region.type":"enum","projection.region.values":"us-east-1,us-west-2","projection.enabled":"true"},"storageDescriptor":{"location":{"Fn::Join":["",["s3://",{"Ref":"TableMultipleBucketC9DE89BA"},"/"]]},"compressed":false,"storedAsSubDirectories":false,"columns":[{"name":"data","type":"string"}],"inputFormat":"org.apache.hadoop.mapred.TextInputFormat","outputFormat":"org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat","serdeInfo":{"serializationLibrary":"org.openx.data.jsonserde.JsonSerDe"}},"tableType":"EXTERNAL_TABLE"}}}}}},"BootstrapVersion":{"id":"BootstrapVersion","path":"aws-cdk-glue-partition-projection/BootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnParameter","version":"0.0.0"}},"CheckBootstrapVersion":{"id":"CheckBootstrapVersion","path":"aws-cdk-glue-partition-projection/CheckBootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnRule","version":"0.0.0"}}}},"GluePartitionProjectionTest":{"id":"GluePartitionProjectionTest","path":"GluePartitionProjectionTest","constructInfo":{"fqn":"@aws-cdk/integ-tests-alpha.IntegTest","version":"0.0.0"},"children":{"DefaultTest":{"id":"DefaultTest","path":"GluePartitionProjectionTest/DefaultTest","constructInfo":{"fqn":"@aws-cdk/integ-tests-alpha.IntegTestCase","version":"0.0.0"},"children":{"Default":{"id":"Default","path":"GluePartitionProjectionTest/DefaultTest/Default","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"}},"DeployAssert":{"id":"DeployAssert","path":"GluePartitionProjectionTest/DefaultTest/DeployAssert","constructInfo":{"fqn":"aws-cdk-lib.Stack","version":"0.0.0"},"children":{"BootstrapVersion":{"id":"BootstrapVersion","path":"GluePartitionProjectionTest/DefaultTest/DeployAssert/BootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnParameter","version":"0.0.0"}},"CheckBootstrapVersion":{"id":"CheckBootstrapVersion","path":"GluePartitionProjectionTest/DefaultTest/DeployAssert/CheckBootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnRule","version":"0.0.0"}}}}}}}},"Tree":{"id":"Tree","path":"Tree","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"}}}}} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.ts b/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.ts new file mode 100644 index 0000000000000..0ffd6e621211f --- /dev/null +++ b/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.ts @@ -0,0 +1,142 @@ +import * as cdk from 'aws-cdk-lib'; +import { IntegTest } from '@aws-cdk/integ-tests-alpha'; +import * as glue from '../lib'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-cdk-glue-partition-projection'); + +const database = new glue.Database(stack, 'Database', { + databaseName: 'partition_projection_test', +}); + +// Test INTEGER partition projection +new glue.S3Table(stack, 'TableInteger', { + database, + tableName: 'integer_projection', + columns: [{ + name: 'data', + type: glue.Schema.STRING, + }], + partitionKeys: [{ + name: 'year', + type: glue.Schema.INTEGER, + }], + dataFormat: glue.DataFormat.JSON, + partitionProjection: { + year: { + type: glue.PartitionProjectionType.INTEGER, + range: [2020, 2023], + interval: 1, + digits: 4, + }, + }, +}); + +// Test DATE partition projection +new glue.S3Table(stack, 'TableDate', { + database, + tableName: 'date_projection', + columns: [{ + name: 'data', + type: glue.Schema.STRING, + }], + partitionKeys: [{ + name: 'date', + type: glue.Schema.STRING, + }], + dataFormat: glue.DataFormat.JSON, + partitionProjection: { + date: { + type: glue.PartitionProjectionType.DATE, + range: ['2020-01-01', '2023-12-31'], + format: 'yyyy-MM-dd', + interval: 1, + intervalUnit: 'DAYS', + }, + }, +}); + +// Test ENUM partition projection +new glue.S3Table(stack, 'TableEnum', { + database, + tableName: 'enum_projection', + columns: [{ + name: 'data', + type: glue.Schema.STRING, + }], + partitionKeys: [{ + name: 'region', + type: glue.Schema.STRING, + }], + dataFormat: glue.DataFormat.JSON, + partitionProjection: { + region: { + type: glue.PartitionProjectionType.ENUM, + values: ['us-east-1', 'us-west-2', 'eu-west-1'], + }, + }, +}); + +// Test INJECTED partition projection +new glue.S3Table(stack, 'TableInjected', { + database, + tableName: 'injected_projection', + columns: [{ + name: 'data', + type: glue.Schema.STRING, + }], + partitionKeys: [{ + name: 'custom', + type: glue.Schema.STRING, + }], + dataFormat: glue.DataFormat.JSON, + partitionProjection: { + custom: { + type: glue.PartitionProjectionType.INJECTED, + }, + }, +}); + +// Test multiple partition projections +new glue.S3Table(stack, 'TableMultiple', { + database, + tableName: 'multiple_projection', + columns: [{ + name: 'data', + type: glue.Schema.STRING, + }], + partitionKeys: [ + { + name: 'year', + type: glue.Schema.INTEGER, + }, + { + name: 'month', + type: glue.Schema.INTEGER, + }, + { + name: 'region', + type: glue.Schema.STRING, + }, + ], + dataFormat: glue.DataFormat.JSON, + partitionProjection: { + year: { + type: glue.PartitionProjectionType.INTEGER, + range: [2020, 2023], + }, + month: { + type: glue.PartitionProjectionType.INTEGER, + range: [1, 12], + digits: 2, + }, + region: { + type: glue.PartitionProjectionType.ENUM, + values: ['us-east-1', 'us-west-2'], + }, + }, +}); + +new IntegTest(app, 'GluePartitionProjectionTest', { + testCases: [stack], +}); diff --git a/packages/@aws-cdk/aws-glue-alpha/test/table-base.test.ts b/packages/@aws-cdk/aws-glue-alpha/test/table-base.test.ts index f1354067db9cc..b77de78c26c80 100644 --- a/packages/@aws-cdk/aws-glue-alpha/test/table-base.test.ts +++ b/packages/@aws-cdk/aws-glue-alpha/test/table-base.test.ts @@ -584,6 +584,404 @@ test('can specify a description', () => { }); }); +describe('Partition Projection', () => { + test('creates table with INTEGER partition projection', () => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const database = new glue.Database(stack, 'Database'); + new glue.S3Table(stack, 'Table', { + database, + columns: [{ + name: 'col1', + type: glue.Schema.STRING, + }], + partitionKeys: [{ + name: 'year', + type: glue.Schema.INTEGER, + }], + dataFormat: glue.DataFormat.JSON, + partitionProjection: { + year: { + type: glue.PartitionProjectionType.INTEGER, + range: [2020, 2023], + interval: 1, + }, + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::Glue::Table', { + TableInput: { + Parameters: { + 'projection.enabled': 'true', + 'projection.year.type': 'integer', + 'projection.year.range': '2020,2023', + 'projection.year.interval': '1', + }, + }, + }); + }); + + test('creates table with INTEGER partition projection with digits', () => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const database = new glue.Database(stack, 'Database'); + new glue.S3Table(stack, 'Table', { + database, + columns: [{ + name: 'col1', + type: glue.Schema.STRING, + }], + partitionKeys: [{ + name: 'year', + type: glue.Schema.INTEGER, + }], + dataFormat: glue.DataFormat.JSON, + partitionProjection: { + year: { + type: glue.PartitionProjectionType.INTEGER, + range: [2020, 2023], + digits: 4, + }, + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::Glue::Table', { + TableInput: { + Parameters: { + 'projection.enabled': 'true', + 'projection.year.type': 'integer', + 'projection.year.range': '2020,2023', + 'projection.year.digits': '4', + }, + }, + }); + }); + + test('creates table with DATE partition projection', () => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const database = new glue.Database(stack, 'Database'); + new glue.S3Table(stack, 'Table', { + database, + columns: [{ + name: 'data', + type: glue.Schema.STRING, + }], + partitionKeys: [{ + name: 'date', + type: glue.Schema.STRING, + }], + dataFormat: glue.DataFormat.JSON, + partitionProjection: { + date: { + type: glue.PartitionProjectionType.DATE, + range: ['2020-01-01', '2023-12-31'], + format: 'yyyy-MM-dd', + interval: 1, + intervalUnit: 'DAYS', + }, + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::Glue::Table', { + TableInput: { + Parameters: { + 'projection.enabled': 'true', + 'projection.date.type': 'date', + 'projection.date.range': '2020-01-01,2023-12-31', + 'projection.date.format': 'yyyy-MM-dd', + 'projection.date.interval': '1', + 'projection.date.interval.unit': 'DAYS', + }, + }, + }); + }); + + test('creates table with ENUM partition projection', () => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const database = new glue.Database(stack, 'Database'); + new glue.S3Table(stack, 'Table', { + database, + columns: [{ + name: 'data', + type: glue.Schema.STRING, + }], + partitionKeys: [{ + name: 'region', + type: glue.Schema.STRING, + }], + dataFormat: glue.DataFormat.JSON, + partitionProjection: { + region: { + type: glue.PartitionProjectionType.ENUM, + values: ['us-east-1', 'us-west-2', 'eu-west-1'], + }, + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::Glue::Table', { + TableInput: { + Parameters: { + 'projection.enabled': 'true', + 'projection.region.type': 'enum', + 'projection.region.values': 'us-east-1,us-west-2,eu-west-1', + }, + }, + }); + }); + + test('creates table with INJECTED partition projection', () => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const database = new glue.Database(stack, 'Database'); + new glue.S3Table(stack, 'Table', { + database, + columns: [{ + name: 'data', + type: glue.Schema.STRING, + }], + partitionKeys: [{ + name: 'custom', + type: glue.Schema.STRING, + }], + dataFormat: glue.DataFormat.JSON, + partitionProjection: { + custom: { + type: glue.PartitionProjectionType.INJECTED, + }, + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::Glue::Table', { + TableInput: { + Parameters: { + 'projection.enabled': 'true', + 'projection.custom.type': 'injected', + }, + }, + }); + }); + + test('creates table with multiple partition projections', () => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const database = new glue.Database(stack, 'Database'); + new glue.S3Table(stack, 'Table', { + database, + columns: [{ + name: 'data', + type: glue.Schema.STRING, + }], + partitionKeys: [ + { + name: 'year', + type: glue.Schema.INTEGER, + }, + { + name: 'month', + type: glue.Schema.INTEGER, + }, + { + name: 'region', + type: glue.Schema.STRING, + }, + ], + dataFormat: glue.DataFormat.JSON, + partitionProjection: { + year: { + type: glue.PartitionProjectionType.INTEGER, + range: [2020, 2023], + }, + month: { + type: glue.PartitionProjectionType.INTEGER, + range: [1, 12], + digits: 2, + }, + region: { + type: glue.PartitionProjectionType.ENUM, + values: ['us-east-1', 'us-west-2'], + }, + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::Glue::Table', { + TableInput: { + Parameters: { + 'projection.enabled': 'true', + 'projection.year.type': 'integer', + 'projection.year.range': '2020,2023', + 'projection.month.type': 'integer', + 'projection.month.range': '1,12', + 'projection.month.digits': '2', + 'projection.region.type': 'enum', + 'projection.region.values': 'us-east-1,us-west-2', + }, + }, + }); + }); + + test('throws when partition projection column is not a partition key', () => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const database = new glue.Database(stack, 'Database'); + + expect(() => { + new glue.S3Table(stack, 'Table', { + database, + columns: [{ + name: 'col1', + type: glue.Schema.STRING, + }], + partitionKeys: [{ + name: 'year', + type: glue.Schema.INTEGER, + }], + dataFormat: glue.DataFormat.JSON, + partitionProjection: { + invalid_column: { + type: glue.PartitionProjectionType.INTEGER, + range: [2020, 2023], + }, + }, + }); + }).toThrow(/Partition projection column "invalid_column" must be a partition key/); + }); + + test('throws when table has no partition keys', () => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const database = new glue.Database(stack, 'Database'); + + expect(() => { + new glue.S3Table(stack, 'Table', { + database, + columns: [{ + name: 'col1', + type: glue.Schema.STRING, + }], + dataFormat: glue.DataFormat.JSON, + partitionProjection: { + year: { + type: glue.PartitionProjectionType.INTEGER, + range: [2020, 2023], + }, + }, + }); + }).toThrow(/The table must have partition keys to use partition projection/); + }); + + test('throws for invalid INTEGER range', () => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const database = new glue.Database(stack, 'Database'); + + expect(() => { + new glue.S3Table(stack, 'Table', { + database, + columns: [{ + name: 'col1', + type: glue.Schema.STRING, + }], + partitionKeys: [{ + name: 'year', + type: glue.Schema.INTEGER, + }], + dataFormat: glue.DataFormat.JSON, + partitionProjection: { + year: { + type: glue.PartitionProjectionType.INTEGER, + range: [2023, 2020], + }, + }, + }); + }).toThrow(/INTEGER partition projection range.*must be \[min, max\] where min <= max/); + }); + + test('throws for invalid INTEGER interval', () => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const database = new glue.Database(stack, 'Database'); + + expect(() => { + new glue.S3Table(stack, 'Table', { + database, + columns: [{ + name: 'col1', + type: glue.Schema.STRING, + }], + partitionKeys: [{ + name: 'year', + type: glue.Schema.INTEGER, + }], + dataFormat: glue.DataFormat.JSON, + partitionProjection: { + year: { + type: glue.PartitionProjectionType.INTEGER, + range: [2020, 2023], + interval: 0, + }, + }, + }); + }).toThrow(/INTEGER partition projection interval.*must be a positive integer/); + }); + + test('throws for invalid ENUM values (empty array)', () => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const database = new glue.Database(stack, 'Database'); + + expect(() => { + new glue.S3Table(stack, 'Table', { + database, + columns: [{ + name: 'col1', + type: glue.Schema.STRING, + }], + partitionKeys: [{ + name: 'region', + type: glue.Schema.STRING, + }], + dataFormat: glue.DataFormat.JSON, + partitionProjection: { + region: { + type: glue.PartitionProjectionType.ENUM, + values: [], + }, + }, + }); + }).toThrow(/ENUM partition projection values.*must be a non-empty array/); + }); + + test('throws for invalid DATE format (empty string)', () => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const database = new glue.Database(stack, 'Database'); + + expect(() => { + new glue.S3Table(stack, 'Table', { + database, + columns: [{ + name: 'col1', + type: glue.Schema.STRING, + }], + partitionKeys: [{ + name: 'date', + type: glue.Schema.STRING, + }], + dataFormat: glue.DataFormat.JSON, + partitionProjection: { + date: { + type: glue.PartitionProjectionType.DATE, + range: ['2020-01-01', '2023-12-31'], + format: '', + }, + }, + }); + }).toThrow(/DATE partition projection format.*must be a non-empty string/); + }); +}); + test('storage descriptor parameters', () => { const app = new cdk.App(); const stack = new cdk.Stack(app, 'Stack'); From 04e0cd436e61709905bf6a2bb42fa9cfd8fe61d7 Mon Sep 17 00:00:00 2001 From: Kazuho Cryer-Shinozuka Date: Sat, 4 Oct 2025 22:46:31 +0900 Subject: [PATCH 02/16] update --- packages/@aws-cdk/aws-glue-alpha/README.md | 38 +- .../lib/partition-projection.ts | 474 +++++++++++------- .../test/integ.partition-projection.ts | 36 +- .../test/partition-projection.test.ts | 211 ++++++++ .../aws-glue-alpha/test/table-base.test.ts | 201 +------- 5 files changed, 562 insertions(+), 398 deletions(-) create mode 100644 packages/@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts diff --git a/packages/@aws-cdk/aws-glue-alpha/README.md b/packages/@aws-cdk/aws-glue-alpha/README.md index 20d10e3196b6c..36c2ea749c2c5 100644 --- a/packages/@aws-cdk/aws-glue-alpha/README.md +++ b/packages/@aws-cdk/aws-glue-alpha/README.md @@ -737,12 +737,11 @@ new glue.S3Table(this, 'MyTable', { }], dataFormat: glue.DataFormat.JSON, partitionProjection: { - year: { - type: glue.PartitionProjectionType.INTEGER, + year: glue.PartitionProjectionConfiguration.Integer({ range: [2020, 2023], interval: 1, // optional, defaults to 1 digits: 4, // optional, pads with leading zeros - }, + }), }, }); ``` @@ -765,13 +764,12 @@ new glue.S3Table(this, 'MyTable', { }], dataFormat: glue.DataFormat.JSON, partitionProjection: { - date: { - type: glue.PartitionProjectionType.DATE, + date: glue.PartitionProjectionConfiguration.Date({ range: ['2020-01-01', '2023-12-31'], format: 'yyyy-MM-dd', - interval: 1, // optional, defaults to 1 - intervalUnit: 'DAYS', // optional: YEARS, MONTHS, WEEKS, DAYS, HOURS, MINUTES, SECONDS - }, + interval: 1, // optional, defaults to 1 + intervalUnit: glue.DateIntervalUnit.DAYS, // optional: YEARS, MONTHS, WEEKS, DAYS, HOURS, MINUTES, SECONDS + }), }, }); ``` @@ -794,10 +792,9 @@ new glue.S3Table(this, 'MyTable', { }], dataFormat: glue.DataFormat.JSON, partitionProjection: { - region: { - type: glue.PartitionProjectionType.ENUM, + region: glue.PartitionProjectionConfiguration.Enum({ values: ['us-east-1', 'us-west-2', 'eu-west-1'], - }, + }), }, }); ``` @@ -820,9 +817,7 @@ new glue.S3Table(this, 'MyTable', { }], dataFormat: glue.DataFormat.JSON, partitionProjection: { - custom: { - type: glue.PartitionProjectionType.INJECTED, - }, + custom: glue.PartitionProjectionConfiguration.Injected(), }, }); ``` @@ -855,19 +850,16 @@ new glue.S3Table(this, 'MyTable', { ], dataFormat: glue.DataFormat.JSON, partitionProjection: { - year: { - type: glue.PartitionProjectionType.INTEGER, + year: glue.PartitionProjectionConfiguration.Integer({ range: [2020, 2023], - }, - month: { - type: glue.PartitionProjectionType.INTEGER, + }), + month: glue.PartitionProjectionConfiguration.Integer({ range: [1, 12], digits: 2, - }, - region: { - type: glue.PartitionProjectionType.ENUM, + }), + region: glue.PartitionProjectionConfiguration.Enum({ values: ['us-east-1', 'us-west-2'], - }, + }), }, }); ``` diff --git a/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts b/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts index e8b98719e607a..d66464a5a3ff1 100644 --- a/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts +++ b/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts @@ -30,22 +30,55 @@ export enum PartitionProjectionType { } /** - * Configuration for INTEGER partition projection. + * Date interval unit for partition projection. * - * @see https://docs.aws.amazon.com/athena/latest/ug/partition-projection-supported-types.html#partition-projection-integer-type + * @see https://docs.aws.amazon.com/athena/latest/ug/partition-projection-supported-types.html#partition-projection-date-type */ -export interface IntegerPartitionConfiguration { +export enum DateIntervalUnit { + /** + * Year interval. + */ + YEARS = 'YEARS', + + /** + * Month interval. + */ + MONTHS = 'MONTHS', + + /** + * Week interval. + */ + WEEKS = 'WEEKS', + + /** + * Day interval (default). + */ + DAYS = 'DAYS', + + /** + * Hour interval. + */ + HOURS = 'HOURS', + + /** + * Minute interval. + */ + MINUTES = 'MINUTES', + /** - * The type of partition projection. + * Second interval. */ - readonly type: PartitionProjectionType.INTEGER; + SECONDS = 'SECONDS', +} +/** + * Properties for INTEGER partition projection configuration. + */ +export interface IntegerPartitionProjectionConfigurationProps { /** * Range of integer partition values [min, max] (inclusive). * * Array must contain exactly 2 elements: [min, max] - * - * @example [0, 100] */ readonly range: number[]; @@ -59,26 +92,17 @@ export interface IntegerPartitionConfiguration { /** * Number of digits to pad the partition value with leading zeros. * - * @default - no padding + * With digits: 4, partition values: 0001, 0002, ..., 0100 * - * @example - * // With digits: 4, partition values: 0001, 0002, ..., 0100 - * digits: 4 + * @default - no padding */ readonly digits?: number; } /** - * Configuration for DATE partition projection. - * - * @see https://docs.aws.amazon.com/athena/latest/ug/partition-projection-supported-types.html#partition-projection-date-type + * Properties for DATE partition projection configuration. */ -export interface DatePartitionConfiguration { - /** - * The type of partition projection. - */ - readonly type: PartitionProjectionType.DATE; - +export interface DatePartitionProjectionConfigurationProps { /** * Range of date partition values [start, end] (inclusive) in ISO 8601 format. * @@ -113,22 +137,15 @@ export interface DatePartitionConfiguration { /** * Unit for the interval. * - * @default DAYS + * @default DateIntervalUnit.DAYS */ - readonly intervalUnit?: 'YEARS' | 'MONTHS' | 'WEEKS' | 'DAYS' | 'HOURS' | 'MINUTES' | 'SECONDS'; + readonly intervalUnit?: DateIntervalUnit; } /** - * Configuration for ENUM partition projection. - * - * @see https://docs.aws.amazon.com/athena/latest/ug/partition-projection-supported-types.html#partition-projection-enum-type + * Properties for ENUM partition projection configuration. */ -export interface EnumPartitionConfiguration { - /** - * The type of partition projection. - */ - readonly type: PartitionProjectionType.ENUM; - +export interface EnumPartitionProjectionConfigurationProps { /** * Explicit list of partition values. * @@ -138,29 +155,200 @@ export interface EnumPartitionConfiguration { } /** - * Configuration for INJECTED partition projection. + * Factory class for creating partition projection configurations. + * + * Provides static factory methods for each partition projection type. + * + * @example + * // Integer partition + * const intConfig = PartitionProjectionConfiguration.Integer({ + * range: [2020, 2023], + * interval: 1, + * digits: 4, + * }); * - * Partition values are injected at query time through the query statement. + * // Date partition + * const dateConfig = PartitionProjectionConfiguration.Date({ + * range: ['2020-01-01', '2023-12-31'], + * format: 'yyyy-MM-dd', + * intervalUnit: DateIntervalUnit.DAYS, + * }); * - * @see https://docs.aws.amazon.com/athena/latest/ug/partition-projection-supported-types.html#partition-projection-injected-type + * // Enum partition + * const enumConfig = PartitionProjectionConfiguration.Enum({ + * values: ['us-east-1', 'us-west-2'], + * }); + * + * // Injected partition + * const injectedConfig = PartitionProjectionConfiguration.Injected(); */ -export interface InjectedPartitionConfiguration { +export class PartitionProjectionConfiguration { /** - * The type of partition projection. + * Create an INTEGER partition projection configuration. + * + * @param props Configuration properties */ - readonly type: PartitionProjectionType.INJECTED; -} + public static integer(props: IntegerPartitionProjectionConfigurationProps): PartitionProjectionConfiguration { + return new PartitionProjectionConfiguration( + PartitionProjectionType.INTEGER, + props.range, + props.interval, + props.digits, + undefined, + undefined, + undefined, + ); + } -/** - * Partition projection configuration. - * - * Discriminated union of partition projection types. - */ -export type PartitionConfiguration = - | IntegerPartitionConfiguration - | DatePartitionConfiguration - | EnumPartitionConfiguration - | InjectedPartitionConfiguration; + /** + * Create a DATE partition projection configuration. + * + * @param props Configuration properties + */ + public static date(props: DatePartitionProjectionConfigurationProps): PartitionProjectionConfiguration { + return new PartitionProjectionConfiguration( + PartitionProjectionType.DATE, + props.range, + props.interval, + undefined, + props.format, + props.intervalUnit, + undefined, + ); + } + + /** + * Create an ENUM partition projection configuration. + * + * @param props Configuration properties + */ + public static enum(props: EnumPartitionProjectionConfigurationProps): PartitionProjectionConfiguration { + return new PartitionProjectionConfiguration( + PartitionProjectionType.ENUM, + undefined, + undefined, + undefined, + undefined, + undefined, + props.values, + ); + } + + /** + * Create an INJECTED partition projection configuration. + * + * Partition values are injected at query time through the query statement. + * + * @see https://docs.aws.amazon.com/athena/latest/ug/partition-projection-supported-types.html#partition-projection-injected-type + */ + public static injected(): PartitionProjectionConfiguration { + return new PartitionProjectionConfiguration( + PartitionProjectionType.INJECTED, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ); + } + + private constructor( + /** + * The type of partition projection. + */ + public readonly type: PartitionProjectionType, + + /** + * Range of partition values. + * + * For INTEGER: [min, max] as numbers + * For DATE: [start, end] as ISO 8601 strings + */ + public readonly range?: number[] | string[], + + /** + * Interval between partition values. + */ + public readonly interval?: number, + + /** + * Number of digits to pad INTEGER partition values. + */ + public readonly digits?: number, + + /** + * Date format for DATE partition values (Java SimpleDateFormat). + */ + public readonly format?: string, + + /** + * Unit for DATE partition interval. + */ + public readonly intervalUnit?: DateIntervalUnit, + + /** + * Explicit list of values for ENUM partitions. + */ + public readonly values?: string[], + ) {} + + /** + * Renders CloudFormation parameters for this partition projection configuration. + * + * @param columnName - The partition column name + * @returns CloudFormation parameters as key-value pairs + * @internal + */ + public _renderParameters(columnName: string): { [key: string]: string } { + const params: { [key: string]: string } = { + [`projection.${columnName}.type`]: this.type, + }; + + switch (this.type) { + case PartitionProjectionType.INTEGER: { + const intRange = this.range as number[]; + params[`projection.${columnName}.range`] = `${intRange[0]},${intRange[1]}`; + if (this.interval !== undefined) { + params[`projection.${columnName}.interval`] = this.interval.toString(); + } + if (this.digits !== undefined) { + params[`projection.${columnName}.digits`] = this.digits.toString(); + } + break; + } + case PartitionProjectionType.DATE: { + const dateRange = this.range as string[]; + params[`projection.${columnName}.range`] = `${dateRange[0]},${dateRange[1]}`; + params[`projection.${columnName}.format`] = this.format!; + if (this.interval !== undefined) { + params[`projection.${columnName}.interval`] = this.interval.toString(); + } + if (this.intervalUnit !== undefined) { + params[`projection.${columnName}.interval.unit`] = this.intervalUnit; + } + break; + } + case PartitionProjectionType.ENUM: { + params[`projection.${columnName}.values`] = this.values!.join(','); + break; + } + case PartitionProjectionType.INJECTED: { + // INJECTED has no additional parameters + break; + } + default: { + // TypeScript exhaustiveness check + const exhaustiveCheck: never = this.type; + throw new UnscopedValidationError( + `Unknown partition projection type for "${columnName}": ${exhaustiveCheck}`, + ); + } + } + + return params; + } +} /** * Partition projection configuration for a table. @@ -170,34 +358,44 @@ export type PartitionConfiguration = * * @example * { - * year: { - * type: PartitionProjectionType.INTEGER, + * year: PartitionProjectionConfiguration.Integer({ * range: [2020, 2023], - * }, - * region: { - * type: PartitionProjectionType.ENUM, + * }), + * region: PartitionProjectionConfiguration.Enum({ * values: ['us-east-1', 'us-west-2'], - * }, + * }), * } */ -export type PartitionProjection = { [columnName: string]: PartitionConfiguration }; +export type PartitionProjection = { + [columnName: string]: PartitionProjectionConfiguration; +}; /** * Validates INTEGER partition projection configuration. * * @param columnName - The partition column name - * @param config - The INTEGER partition configuration + * @param config - The partition configuration * @throws {UnscopedValidationError} if the configuration is invalid */ -export function validateIntegerPartition(columnName: string, config: IntegerPartitionConfiguration): void { +export function validateIntegerPartition( + columnName: string, + config: PartitionProjectionConfiguration, +): void { + if (config.type !== PartitionProjectionType.INTEGER) { + throw new UnscopedValidationError( + `Expected INTEGER partition type for "${columnName}", but got ${config.type}`, + ); + } + // Validate range - if (config.range.length !== 2) { + if (!config.range || config.range.length !== 2) { throw new UnscopedValidationError( - `INTEGER partition projection range for "${columnName}" must be [min, max], but got array of length ${config.range.length}`, + `INTEGER partition projection range for "${columnName}" must be [min, max], but got array of length ${config.range?.length ?? 0}`, ); } - const [min, max] = config.range; + const range = config.range as number[]; + const [min, max] = range; if (!Number.isInteger(min) || !Number.isInteger(max)) { throw new UnscopedValidationError( `INTEGER partition projection range for "${columnName}" must contain integers, but got [${min}, ${max}]`, @@ -233,18 +431,28 @@ export function validateIntegerPartition(columnName: string, config: IntegerPart * Validates DATE partition projection configuration. * * @param columnName - The partition column name - * @param config - The DATE partition configuration + * @param config - The partition configuration * @throws {UnscopedValidationError} if the configuration is invalid */ -export function validateDatePartition(columnName: string, config: DatePartitionConfiguration): void { +export function validateDatePartition( + columnName: string, + config: PartitionProjectionConfiguration, +): void { + if (config.type !== PartitionProjectionType.DATE) { + throw new UnscopedValidationError( + `Expected DATE partition type for "${columnName}", but got ${config.type}`, + ); + } + // Validate range - if (config.range.length !== 2) { + if (!config.range || config.range.length !== 2) { throw new UnscopedValidationError( - `DATE partition projection range for "${columnName}" must be [start, end], but got array of length ${config.range.length}`, + `DATE partition projection range for "${columnName}" must be [start, end], but got array of length ${config.range?.length ?? 0}`, ); } - const [start, end] = config.range; + const range = config.range as string[]; + const [start, end] = range; if (typeof start !== 'string' || typeof end !== 'string') { throw new UnscopedValidationError( `DATE partition projection range for "${columnName}" must contain strings, but got [${typeof start}, ${typeof end}]`, @@ -275,7 +483,7 @@ export function validateDatePartition(columnName: string, config: DatePartitionC // Validate interval unit if (config.intervalUnit !== undefined) { - const validUnits = ['YEARS', 'MONTHS', 'WEEKS', 'DAYS', 'HOURS', 'MINUTES', 'SECONDS']; + const validUnits = Object.values(DateIntervalUnit); if (!validUnits.includes(config.intervalUnit)) { throw new UnscopedValidationError( `DATE partition projection interval unit for "${columnName}" must be one of ${validUnits.join(', ')}, but got ${config.intervalUnit}`, @@ -288,10 +496,19 @@ export function validateDatePartition(columnName: string, config: DatePartitionC * Validates ENUM partition projection configuration. * * @param columnName - The partition column name - * @param config - The ENUM partition configuration + * @param config - The partition configuration * @throws {UnscopedValidationError} if the configuration is invalid */ -export function validateEnumPartition(columnName: string, config: EnumPartitionConfiguration): void { +export function validateEnumPartition( + columnName: string, + config: PartitionProjectionConfiguration, +): void { + if (config.type !== PartitionProjectionType.ENUM) { + throw new UnscopedValidationError( + `Expected ENUM partition type for "${columnName}", but got ${config.type}`, + ); + } + // Validate values if (!Array.isArray(config.values) || config.values.length === 0) { throw new UnscopedValidationError( @@ -318,12 +535,20 @@ export function validateEnumPartition(columnName: string, config: EnumPartitionC * Validates INJECTED partition projection configuration. * * @param _columnName - The partition column name - * @param _config - The INJECTED partition configuration + * @param config - The partition configuration * @throws {UnscopedValidationError} if the configuration is invalid */ -export function validateInjectedPartition(_columnName: string, _config: InjectedPartitionConfiguration): void { +export function validateInjectedPartition( + _columnName: string, + config: PartitionProjectionConfiguration, +): void { + if (config.type !== PartitionProjectionType.INJECTED) { + throw new UnscopedValidationError( + `Expected INJECTED partition type for "${_columnName}", but got ${config.type}`, + ); + } + // INJECTED type has no additional properties to validate - // This function exists for completeness and future extensibility } /** @@ -333,7 +558,10 @@ export function validateInjectedPartition(_columnName: string, _config: Injected * @param config - The partition configuration * @throws {UnscopedValidationError} if the configuration is invalid */ -export function validatePartitionConfiguration(columnName: string, config: PartitionConfiguration): void { +export function validatePartitionConfiguration( + columnName: string, + config: PartitionProjectionConfiguration, +): void { switch (config.type) { case PartitionProjectionType.INTEGER: validateIntegerPartition(columnName, config); @@ -347,91 +575,16 @@ export function validatePartitionConfiguration(columnName: string, config: Parti case PartitionProjectionType.INJECTED: validateInjectedPartition(columnName, config); break; - default: + default: { // TypeScript exhaustiveness check - const exhaustiveCheck: never = config; + const exhaustiveCheck: never = config.type; throw new UnscopedValidationError( - `Unknown partition projection type for "${columnName}": ${(exhaustiveCheck as any).type}`, + `Unknown partition projection type for "${columnName}": ${exhaustiveCheck}`, ); + } } } -/** - * Generates CloudFormation parameters for INTEGER partition projection. - * - * @param columnName - The partition column name - * @param config - The INTEGER partition configuration - * @returns CloudFormation parameters - */ -function generateIntegerParameters(columnName: string, config: IntegerPartitionConfiguration): { [key: string]: string } { - const params: { [key: string]: string } = { - [`projection.${columnName}.type`]: 'integer', - [`projection.${columnName}.range`]: `${config.range[0]},${config.range[1]}`, - }; - - if (config.interval !== undefined) { - params[`projection.${columnName}.interval`] = config.interval.toString(); - } - - if (config.digits !== undefined) { - params[`projection.${columnName}.digits`] = config.digits.toString(); - } - - return params; -} - -/** - * Generates CloudFormation parameters for DATE partition projection. - * - * @param columnName - The partition column name - * @param config - The DATE partition configuration - * @returns CloudFormation parameters - */ -function generateDateParameters(columnName: string, config: DatePartitionConfiguration): { [key: string]: string } { - const params: { [key: string]: string } = { - [`projection.${columnName}.type`]: 'date', - [`projection.${columnName}.range`]: `${config.range[0]},${config.range[1]}`, - [`projection.${columnName}.format`]: config.format, - }; - - if (config.interval !== undefined) { - params[`projection.${columnName}.interval`] = config.interval.toString(); - } - - if (config.intervalUnit !== undefined) { - params[`projection.${columnName}.interval.unit`] = config.intervalUnit; - } - - return params; -} - -/** - * Generates CloudFormation parameters for ENUM partition projection. - * - * @param columnName - The partition column name - * @param config - The ENUM partition configuration - * @returns CloudFormation parameters - */ -function generateEnumParameters(columnName: string, config: EnumPartitionConfiguration): { [key: string]: string } { - return { - [`projection.${columnName}.type`]: 'enum', - [`projection.${columnName}.values`]: config.values.join(','), - }; -} - -/** - * Generates CloudFormation parameters for INJECTED partition projection. - * - * @param columnName - The partition column name - * @param _config - The INJECTED partition configuration - * @returns CloudFormation parameters - */ -function generateInjectedParameters(columnName: string, _config: InjectedPartitionConfiguration): { [key: string]: string } { - return { - [`projection.${columnName}.type`]: 'injected', - }; -} - /** * Generates CloudFormation parameters for partition projection configuration. * @@ -441,22 +594,7 @@ function generateInjectedParameters(columnName: string, _config: InjectedPartiti */ export function generatePartitionProjectionParameters( columnName: string, - config: PartitionConfiguration, + config: PartitionProjectionConfiguration, ): { [key: string]: string } { - switch (config.type) { - case PartitionProjectionType.INTEGER: - return generateIntegerParameters(columnName, config); - case PartitionProjectionType.DATE: - return generateDateParameters(columnName, config); - case PartitionProjectionType.ENUM: - return generateEnumParameters(columnName, config); - case PartitionProjectionType.INJECTED: - return generateInjectedParameters(columnName, config); - default: - // TypeScript exhaustiveness check - const exhaustiveCheck: never = config; - throw new UnscopedValidationError( - `Unknown partition projection type for "${columnName}": ${(exhaustiveCheck as any).type}`, - ); - } + return config._renderParameters(columnName); } diff --git a/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.ts b/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.ts index 0ffd6e621211f..39169f740ad17 100644 --- a/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.ts +++ b/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.ts @@ -23,12 +23,11 @@ new glue.S3Table(stack, 'TableInteger', { }], dataFormat: glue.DataFormat.JSON, partitionProjection: { - year: { - type: glue.PartitionProjectionType.INTEGER, + year: glue.PartitionProjectionConfiguration.integer({ range: [2020, 2023], interval: 1, digits: 4, - }, + }), }, }); @@ -46,13 +45,12 @@ new glue.S3Table(stack, 'TableDate', { }], dataFormat: glue.DataFormat.JSON, partitionProjection: { - date: { - type: glue.PartitionProjectionType.DATE, + date: glue.PartitionProjectionConfiguration.date({ range: ['2020-01-01', '2023-12-31'], format: 'yyyy-MM-dd', interval: 1, - intervalUnit: 'DAYS', - }, + intervalUnit: glue.DateIntervalUnit.DAYS, + }), }, }); @@ -70,10 +68,9 @@ new glue.S3Table(stack, 'TableEnum', { }], dataFormat: glue.DataFormat.JSON, partitionProjection: { - region: { - type: glue.PartitionProjectionType.ENUM, + region: glue.PartitionProjectionConfiguration.enum({ values: ['us-east-1', 'us-west-2', 'eu-west-1'], - }, + }), }, }); @@ -91,9 +88,7 @@ new glue.S3Table(stack, 'TableInjected', { }], dataFormat: glue.DataFormat.JSON, partitionProjection: { - custom: { - type: glue.PartitionProjectionType.INJECTED, - }, + custom: glue.PartitionProjectionConfiguration.injected(), }, }); @@ -121,19 +116,16 @@ new glue.S3Table(stack, 'TableMultiple', { ], dataFormat: glue.DataFormat.JSON, partitionProjection: { - year: { - type: glue.PartitionProjectionType.INTEGER, + year: glue.PartitionProjectionConfiguration.integer({ range: [2020, 2023], - }, - month: { - type: glue.PartitionProjectionType.INTEGER, + }), + month: glue.PartitionProjectionConfiguration.integer({ range: [1, 12], digits: 2, - }, - region: { - type: glue.PartitionProjectionType.ENUM, + }), + region: glue.PartitionProjectionConfiguration.enum({ values: ['us-east-1', 'us-west-2'], - }, + }), }, }); diff --git a/packages/@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts b/packages/@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts new file mode 100644 index 0000000000000..af80b10c664dd --- /dev/null +++ b/packages/@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts @@ -0,0 +1,211 @@ +import * as glue from '../lib'; + +describe('DateIntervalUnit', () => { + test('has correct enum values', () => { + expect(glue.DateIntervalUnit.YEARS).toBe('YEARS'); + expect(glue.DateIntervalUnit.MONTHS).toBe('MONTHS'); + expect(glue.DateIntervalUnit.WEEKS).toBe('WEEKS'); + expect(glue.DateIntervalUnit.DAYS).toBe('DAYS'); + expect(glue.DateIntervalUnit.HOURS).toBe('HOURS'); + expect(glue.DateIntervalUnit.MINUTES).toBe('MINUTES'); + expect(glue.DateIntervalUnit.SECONDS).toBe('SECONDS'); + }); + + test('has all 7 interval units', () => { + const values = Object.values(glue.DateIntervalUnit); + expect(values).toHaveLength(7); + expect(values).toContain('YEARS'); + expect(values).toContain('MONTHS'); + expect(values).toContain('WEEKS'); + expect(values).toContain('DAYS'); + expect(values).toContain('HOURS'); + expect(values).toContain('MINUTES'); + expect(values).toContain('SECONDS'); + }); +}); + +describe('PartitionProjectionConfiguration', () => { + describe('Integer', () => { + test('creates INTEGER configuration with required fields only', () => { + const config = glue.PartitionProjectionConfiguration.integer({ + range: [0, 100], + }); + + expect(config.type).toBe(glue.PartitionProjectionType.INTEGER); + expect(config.range).toEqual([0, 100]); + expect(config.interval).toBeUndefined(); + expect(config.digits).toBeUndefined(); + expect(config.format).toBeUndefined(); + expect(config.intervalUnit).toBeUndefined(); + expect(config.values).toBeUndefined(); + }); + + test('creates INTEGER configuration with all fields', () => { + const config = glue.PartitionProjectionConfiguration.integer({ + range: [2020, 2023], + interval: 1, + digits: 4, + }); + + expect(config.type).toBe(glue.PartitionProjectionType.INTEGER); + expect(config.range).toEqual([2020, 2023]); + expect(config.interval).toBe(1); + expect(config.digits).toBe(4); + }); + }); + + describe('Date', () => { + test('creates DATE configuration with required fields only', () => { + const config = glue.PartitionProjectionConfiguration.date({ + range: ['2020-01-01', '2023-12-31'], + format: 'yyyy-MM-dd', + }); + + expect(config.type).toBe(glue.PartitionProjectionType.DATE); + expect(config.range).toEqual(['2020-01-01', '2023-12-31']); + expect(config.format).toBe('yyyy-MM-dd'); + expect(config.interval).toBeUndefined(); + expect(config.intervalUnit).toBeUndefined(); + }); + + test('creates DATE configuration with all fields', () => { + const config = glue.PartitionProjectionConfiguration.date({ + range: ['2020-01-01', '2023-12-31'], + format: 'yyyy-MM-dd', + interval: 1, + intervalUnit: glue.DateIntervalUnit.WEEKS, + }); + + expect(config.type).toBe(glue.PartitionProjectionType.DATE); + expect(config.range).toEqual(['2020-01-01', '2023-12-31']); + expect(config.format).toBe('yyyy-MM-dd'); + expect(config.interval).toBe(1); + expect(config.intervalUnit).toBe(glue.DateIntervalUnit.WEEKS); + }); + + test('accepts DateIntervalUnit enum values', () => { + const config = glue.PartitionProjectionConfiguration.date({ + range: ['2020-01-01', '2023-12-31'], + format: 'yyyy-MM-dd', + intervalUnit: glue.DateIntervalUnit.DAYS, + }); + + expect(config.intervalUnit).toBe('DAYS'); + }); + }); + + describe('Enum', () => { + test('creates ENUM configuration', () => { + const config = glue.PartitionProjectionConfiguration.enum({ + values: ['us-east-1', 'us-west-2', 'eu-west-1'], + }); + + expect(config.type).toBe(glue.PartitionProjectionType.ENUM); + expect(config.values).toEqual(['us-east-1', 'us-west-2', 'eu-west-1']); + expect(config.range).toBeUndefined(); + expect(config.interval).toBeUndefined(); + expect(config.format).toBeUndefined(); + }); + }); + + describe('Injected', () => { + test('creates INJECTED configuration', () => { + const config = glue.PartitionProjectionConfiguration.injected(); + + expect(config.type).toBe(glue.PartitionProjectionType.INJECTED); + expect(config.range).toBeUndefined(); + expect(config.interval).toBeUndefined(); + expect(config.values).toBeUndefined(); + expect(config.format).toBeUndefined(); + }); + }); + + describe('renderParameters', () => { + test('renders INTEGER parameters with all fields', () => { + const config = glue.PartitionProjectionConfiguration.integer({ + range: [2020, 2023], + interval: 1, + digits: 4, + }); + + const params = config._renderParameters('year'); + + expect(params).toEqual({ + 'projection.year.type': 'integer', + 'projection.year.range': '2020,2023', + 'projection.year.interval': '1', + 'projection.year.digits': '4', + }); + }); + + test('renders INTEGER parameters with required fields only', () => { + const config = glue.PartitionProjectionConfiguration.integer({ + range: [0, 100], + }); + + const params = config._renderParameters('year'); + + expect(params).toEqual({ + 'projection.year.type': 'integer', + 'projection.year.range': '0,100', + }); + }); + + test('renders DATE parameters with all fields', () => { + const config = glue.PartitionProjectionConfiguration.date({ + range: ['2020-01-01', '2023-12-31'], + format: 'yyyy-MM-dd', + interval: 1, + intervalUnit: glue.DateIntervalUnit.DAYS, + }); + + const params = config._renderParameters('date'); + + expect(params).toEqual({ + 'projection.date.type': 'date', + 'projection.date.range': '2020-01-01,2023-12-31', + 'projection.date.format': 'yyyy-MM-dd', + 'projection.date.interval': '1', + 'projection.date.interval.unit': 'DAYS', + }); + }); + + test('renders DATE parameters with required fields only', () => { + const config = glue.PartitionProjectionConfiguration.date({ + range: ['2020-01-01', '2023-12-31'], + format: 'yyyy-MM-dd', + }); + + const params = config._renderParameters('date'); + + expect(params).toEqual({ + 'projection.date.type': 'date', + 'projection.date.range': '2020-01-01,2023-12-31', + 'projection.date.format': 'yyyy-MM-dd', + }); + }); + + test('renders ENUM parameters', () => { + const config = glue.PartitionProjectionConfiguration.enum({ + values: ['us-east-1', 'us-west-2'], + }); + + const params = config._renderParameters('region'); + + expect(params).toEqual({ + 'projection.region.type': 'enum', + 'projection.region.values': 'us-east-1,us-west-2', + }); + }); + + test('renders INJECTED parameters', () => { + const config = glue.PartitionProjectionConfiguration.injected(); + + const params = config._renderParameters('custom'); + + expect(params).toEqual({ + 'projection.custom.type': 'injected', + }); + }); + }); +}); diff --git a/packages/@aws-cdk/aws-glue-alpha/test/table-base.test.ts b/packages/@aws-cdk/aws-glue-alpha/test/table-base.test.ts index b77de78c26c80..a9c27adac1154 100644 --- a/packages/@aws-cdk/aws-glue-alpha/test/table-base.test.ts +++ b/packages/@aws-cdk/aws-glue-alpha/test/table-base.test.ts @@ -601,11 +601,10 @@ describe('Partition Projection', () => { }], dataFormat: glue.DataFormat.JSON, partitionProjection: { - year: { - type: glue.PartitionProjectionType.INTEGER, + year: glue.PartitionProjectionConfiguration.integer({ range: [2020, 2023], interval: 1, - }, + }), }, }); @@ -637,11 +636,10 @@ describe('Partition Projection', () => { }], dataFormat: glue.DataFormat.JSON, partitionProjection: { - year: { - type: glue.PartitionProjectionType.INTEGER, + year: glue.PartitionProjectionConfiguration.integer({ range: [2020, 2023], digits: 4, - }, + }), }, }); @@ -673,13 +671,12 @@ describe('Partition Projection', () => { }], dataFormat: glue.DataFormat.JSON, partitionProjection: { - date: { - type: glue.PartitionProjectionType.DATE, + date: glue.PartitionProjectionConfiguration.date({ range: ['2020-01-01', '2023-12-31'], format: 'yyyy-MM-dd', interval: 1, - intervalUnit: 'DAYS', - }, + intervalUnit: glue.DateIntervalUnit.DAYS, + }), }, }); @@ -713,10 +710,9 @@ describe('Partition Projection', () => { }], dataFormat: glue.DataFormat.JSON, partitionProjection: { - region: { - type: glue.PartitionProjectionType.ENUM, + region: glue.PartitionProjectionConfiguration.enum({ values: ['us-east-1', 'us-west-2', 'eu-west-1'], - }, + }), }, }); @@ -747,9 +743,7 @@ describe('Partition Projection', () => { }], dataFormat: glue.DataFormat.JSON, partitionProjection: { - custom: { - type: glue.PartitionProjectionType.INJECTED, - }, + custom: glue.PartitionProjectionConfiguration.injected(), }, }); @@ -789,19 +783,16 @@ describe('Partition Projection', () => { ], dataFormat: glue.DataFormat.JSON, partitionProjection: { - year: { - type: glue.PartitionProjectionType.INTEGER, + year: glue.PartitionProjectionConfiguration.integer({ range: [2020, 2023], - }, - month: { - type: glue.PartitionProjectionType.INTEGER, + }), + month: glue.PartitionProjectionConfiguration.integer({ range: [1, 12], digits: 2, - }, - region: { - type: glue.PartitionProjectionType.ENUM, + }), + region: glue.PartitionProjectionConfiguration.enum({ values: ['us-east-1', 'us-west-2'], - }, + }), }, }); @@ -820,166 +811,6 @@ describe('Partition Projection', () => { }, }); }); - - test('throws when partition projection column is not a partition key', () => { - const app = new cdk.App(); - const stack = new cdk.Stack(app, 'Stack'); - const database = new glue.Database(stack, 'Database'); - - expect(() => { - new glue.S3Table(stack, 'Table', { - database, - columns: [{ - name: 'col1', - type: glue.Schema.STRING, - }], - partitionKeys: [{ - name: 'year', - type: glue.Schema.INTEGER, - }], - dataFormat: glue.DataFormat.JSON, - partitionProjection: { - invalid_column: { - type: glue.PartitionProjectionType.INTEGER, - range: [2020, 2023], - }, - }, - }); - }).toThrow(/Partition projection column "invalid_column" must be a partition key/); - }); - - test('throws when table has no partition keys', () => { - const app = new cdk.App(); - const stack = new cdk.Stack(app, 'Stack'); - const database = new glue.Database(stack, 'Database'); - - expect(() => { - new glue.S3Table(stack, 'Table', { - database, - columns: [{ - name: 'col1', - type: glue.Schema.STRING, - }], - dataFormat: glue.DataFormat.JSON, - partitionProjection: { - year: { - type: glue.PartitionProjectionType.INTEGER, - range: [2020, 2023], - }, - }, - }); - }).toThrow(/The table must have partition keys to use partition projection/); - }); - - test('throws for invalid INTEGER range', () => { - const app = new cdk.App(); - const stack = new cdk.Stack(app, 'Stack'); - const database = new glue.Database(stack, 'Database'); - - expect(() => { - new glue.S3Table(stack, 'Table', { - database, - columns: [{ - name: 'col1', - type: glue.Schema.STRING, - }], - partitionKeys: [{ - name: 'year', - type: glue.Schema.INTEGER, - }], - dataFormat: glue.DataFormat.JSON, - partitionProjection: { - year: { - type: glue.PartitionProjectionType.INTEGER, - range: [2023, 2020], - }, - }, - }); - }).toThrow(/INTEGER partition projection range.*must be \[min, max\] where min <= max/); - }); - - test('throws for invalid INTEGER interval', () => { - const app = new cdk.App(); - const stack = new cdk.Stack(app, 'Stack'); - const database = new glue.Database(stack, 'Database'); - - expect(() => { - new glue.S3Table(stack, 'Table', { - database, - columns: [{ - name: 'col1', - type: glue.Schema.STRING, - }], - partitionKeys: [{ - name: 'year', - type: glue.Schema.INTEGER, - }], - dataFormat: glue.DataFormat.JSON, - partitionProjection: { - year: { - type: glue.PartitionProjectionType.INTEGER, - range: [2020, 2023], - interval: 0, - }, - }, - }); - }).toThrow(/INTEGER partition projection interval.*must be a positive integer/); - }); - - test('throws for invalid ENUM values (empty array)', () => { - const app = new cdk.App(); - const stack = new cdk.Stack(app, 'Stack'); - const database = new glue.Database(stack, 'Database'); - - expect(() => { - new glue.S3Table(stack, 'Table', { - database, - columns: [{ - name: 'col1', - type: glue.Schema.STRING, - }], - partitionKeys: [{ - name: 'region', - type: glue.Schema.STRING, - }], - dataFormat: glue.DataFormat.JSON, - partitionProjection: { - region: { - type: glue.PartitionProjectionType.ENUM, - values: [], - }, - }, - }); - }).toThrow(/ENUM partition projection values.*must be a non-empty array/); - }); - - test('throws for invalid DATE format (empty string)', () => { - const app = new cdk.App(); - const stack = new cdk.Stack(app, 'Stack'); - const database = new glue.Database(stack, 'Database'); - - expect(() => { - new glue.S3Table(stack, 'Table', { - database, - columns: [{ - name: 'col1', - type: glue.Schema.STRING, - }], - partitionKeys: [{ - name: 'date', - type: glue.Schema.STRING, - }], - dataFormat: glue.DataFormat.JSON, - partitionProjection: { - date: { - type: glue.PartitionProjectionType.DATE, - range: ['2020-01-01', '2023-12-31'], - format: '', - }, - }, - }); - }).toThrow(/DATE partition projection format.*must be a non-empty string/); - }); }); test('storage descriptor parameters', () => { From 9fec278697260238ade31428816f174b9c69ff0b Mon Sep 17 00:00:00 2001 From: Kazuho Cryer-Shinozuka Date: Sun, 5 Oct 2025 00:20:15 +0900 Subject: [PATCH 03/16] refactor --- packages/@aws-cdk/aws-glue-alpha/README.md | 53 +++++++++++---- .../lib/partition-projection.ts | 68 ++++++++++++++----- .../test/integ.partition-projection.ts | 12 ++-- .../test/partition-projection.test.ts | 37 +++++++--- .../aws-glue-alpha/test/table-base.test.ts | 15 ++-- 5 files changed, 137 insertions(+), 48 deletions(-) diff --git a/packages/@aws-cdk/aws-glue-alpha/README.md b/packages/@aws-cdk/aws-glue-alpha/README.md index 36c2ea749c2c5..272e2c8478af3 100644 --- a/packages/@aws-cdk/aws-glue-alpha/README.md +++ b/packages/@aws-cdk/aws-glue-alpha/README.md @@ -737,8 +737,9 @@ new glue.S3Table(this, 'MyTable', { }], dataFormat: glue.DataFormat.JSON, partitionProjection: { - year: glue.PartitionProjectionConfiguration.Integer({ - range: [2020, 2023], + year: glue.PartitionProjectionConfiguration.integer({ + min: 2020, + max: 2023, interval: 1, // optional, defaults to 1 digits: 4, // optional, pads with leading zeros }), @@ -748,7 +749,7 @@ new glue.S3Table(this, 'MyTable', { #### DATE Projection -For partition keys with date or timestamp values: +For partition keys with date or timestamp values. Supports both fixed dates and relative dates using `NOW`: ```ts declare const myDatabase: glue.Database; @@ -764,8 +765,9 @@ new glue.S3Table(this, 'MyTable', { }], dataFormat: glue.DataFormat.JSON, partitionProjection: { - date: glue.PartitionProjectionConfiguration.Date({ - range: ['2020-01-01', '2023-12-31'], + date: glue.PartitionProjectionConfiguration.date({ + min: '2020-01-01', + max: '2023-12-31', format: 'yyyy-MM-dd', interval: 1, // optional, defaults to 1 intervalUnit: glue.DateIntervalUnit.DAYS, // optional: YEARS, MONTHS, WEEKS, DAYS, HOURS, MINUTES, SECONDS @@ -774,6 +776,31 @@ new glue.S3Table(this, 'MyTable', { }); ``` +You can also use relative dates with `NOW`: + +```ts +declare const myDatabase: glue.Database; +new glue.S3Table(this, 'MyTable', { + database: myDatabase, + columns: [{ + name: 'data', + type: glue.Schema.STRING, + }], + partitionKeys: [{ + name: 'date', + type: glue.Schema.STRING, + }], + dataFormat: glue.DataFormat.JSON, + partitionProjection: { + date: glue.PartitionProjectionConfiguration.date({ + min: 'NOW-3YEARS', + max: 'NOW', + format: 'yyyy-MM-dd', + }), + }, +}); +``` + #### ENUM Projection For partition keys with a known set of values: @@ -792,7 +819,7 @@ new glue.S3Table(this, 'MyTable', { }], dataFormat: glue.DataFormat.JSON, partitionProjection: { - region: glue.PartitionProjectionConfiguration.Enum({ + region: glue.PartitionProjectionConfiguration.enum({ values: ['us-east-1', 'us-west-2', 'eu-west-1'], }), }, @@ -817,7 +844,7 @@ new glue.S3Table(this, 'MyTable', { }], dataFormat: glue.DataFormat.JSON, partitionProjection: { - custom: glue.PartitionProjectionConfiguration.Injected(), + custom: glue.PartitionProjectionConfiguration.injected(), }, }); ``` @@ -850,14 +877,16 @@ new glue.S3Table(this, 'MyTable', { ], dataFormat: glue.DataFormat.JSON, partitionProjection: { - year: glue.PartitionProjectionConfiguration.Integer({ - range: [2020, 2023], + year: glue.PartitionProjectionConfiguration.integer({ + min: 2020, + max: 2023, }), - month: glue.PartitionProjectionConfiguration.Integer({ - range: [1, 12], + month: glue.PartitionProjectionConfiguration.integer({ + min: 1, + max: 12, digits: 2, }), - region: glue.PartitionProjectionConfiguration.Enum({ + region: glue.PartitionProjectionConfiguration.enum({ values: ['us-east-1', 'us-west-2'], }), }, diff --git a/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts b/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts index d66464a5a3ff1..7ae33834c4ea6 100644 --- a/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts +++ b/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts @@ -76,11 +76,20 @@ export enum DateIntervalUnit { */ export interface IntegerPartitionProjectionConfigurationProps { /** - * Range of integer partition values [min, max] (inclusive). + * Minimum value for the integer partition range (inclusive). * - * Array must contain exactly 2 elements: [min, max] + * @example 2020 + * @example 0 */ - readonly range: number[]; + readonly min: number; + + /** + * Maximum value for the integer partition range (inclusive). + * + * @example 2023 + * @example 100 + */ + readonly max: number; /** * Interval between partition values. @@ -104,14 +113,36 @@ export interface IntegerPartitionProjectionConfigurationProps { */ export interface DatePartitionProjectionConfigurationProps { /** - * Range of date partition values [start, end] (inclusive) in ISO 8601 format. + * Start date for the partition range (inclusive). + * + * Can be either: + * - Fixed date in the format specified by `format` property + * (e.g., '2020-01-01' for format 'yyyy-MM-dd') + * - Relative date using NOW syntax + * (e.g., 'NOW', 'NOW-3YEARS', 'NOW+1MONTH') + * + * @see https://docs.aws.amazon.com/athena/latest/ug/partition-projection-supported-types.html#partition-projection-date-type + * + * @example '2020-01-01' + * @example 'NOW-3YEARS' + * @example '2020-01-01-00-00-00' + */ + readonly min: string; + + /** + * End date for the partition range (inclusive). + * + * Can be either: + * - Fixed date in the format specified by `format` property + * - Relative date using NOW syntax * - * Array must contain exactly 2 elements: [start, end] + * Same format constraints as `min`. * - * @example ['2020-01-01', '2023-12-31'] - * @example ['2020-01-01-00-00-00', '2023-12-31-23-59-59'] + * @example '2023-12-31' + * @example 'NOW' + * @example 'NOW+1MONTH' */ - readonly range: string[]; + readonly max: string; /** * Date format for partition values. @@ -120,10 +151,9 @@ export interface DatePartitionProjectionConfigurationProps { * * @see https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html * - * @example - * 'yyyy-MM-dd' - * 'yyyy-MM-dd-HH-mm-ss' - * 'yyyyMMdd' + * @example 'yyyy-MM-dd' + * @example 'yyyy-MM-dd-HH-mm-ss' + * @example 'yyyyMMdd' */ readonly format: string; @@ -161,15 +191,17 @@ export interface EnumPartitionProjectionConfigurationProps { * * @example * // Integer partition - * const intConfig = PartitionProjectionConfiguration.Integer({ - * range: [2020, 2023], + * const intConfig = PartitionProjectionConfiguration.integer({ + * min: 2020, + * max: 2023, * interval: 1, * digits: 4, * }); * * // Date partition - * const dateConfig = PartitionProjectionConfiguration.Date({ - * range: ['2020-01-01', '2023-12-31'], + * const dateConfig = PartitionProjectionConfiguration.date({ + * min: '2020-01-01', + * max: '2023-12-31', * format: 'yyyy-MM-dd', * intervalUnit: DateIntervalUnit.DAYS, * }); @@ -191,7 +223,7 @@ export class PartitionProjectionConfiguration { public static integer(props: IntegerPartitionProjectionConfigurationProps): PartitionProjectionConfiguration { return new PartitionProjectionConfiguration( PartitionProjectionType.INTEGER, - props.range, + [props.min, props.max], props.interval, props.digits, undefined, @@ -208,7 +240,7 @@ export class PartitionProjectionConfiguration { public static date(props: DatePartitionProjectionConfigurationProps): PartitionProjectionConfiguration { return new PartitionProjectionConfiguration( PartitionProjectionType.DATE, - props.range, + [props.min, props.max], props.interval, undefined, props.format, diff --git a/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.ts b/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.ts index 39169f740ad17..ecce674509d40 100644 --- a/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.ts +++ b/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.ts @@ -24,7 +24,8 @@ new glue.S3Table(stack, 'TableInteger', { dataFormat: glue.DataFormat.JSON, partitionProjection: { year: glue.PartitionProjectionConfiguration.integer({ - range: [2020, 2023], + min: 2020, + max: 2023, interval: 1, digits: 4, }), @@ -46,7 +47,8 @@ new glue.S3Table(stack, 'TableDate', { dataFormat: glue.DataFormat.JSON, partitionProjection: { date: glue.PartitionProjectionConfiguration.date({ - range: ['2020-01-01', '2023-12-31'], + min: '2020-01-01', + max: '2023-12-31', format: 'yyyy-MM-dd', interval: 1, intervalUnit: glue.DateIntervalUnit.DAYS, @@ -117,10 +119,12 @@ new glue.S3Table(stack, 'TableMultiple', { dataFormat: glue.DataFormat.JSON, partitionProjection: { year: glue.PartitionProjectionConfiguration.integer({ - range: [2020, 2023], + min: 2020, + max: 2023, }), month: glue.PartitionProjectionConfiguration.integer({ - range: [1, 12], + min: 1, + max: 12, digits: 2, }), region: glue.PartitionProjectionConfiguration.enum({ diff --git a/packages/@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts b/packages/@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts index af80b10c664dd..1b306a14d2d94 100644 --- a/packages/@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts +++ b/packages/@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts @@ -28,7 +28,8 @@ describe('PartitionProjectionConfiguration', () => { describe('Integer', () => { test('creates INTEGER configuration with required fields only', () => { const config = glue.PartitionProjectionConfiguration.integer({ - range: [0, 100], + min: 0, + max: 100, }); expect(config.type).toBe(glue.PartitionProjectionType.INTEGER); @@ -42,7 +43,8 @@ describe('PartitionProjectionConfiguration', () => { test('creates INTEGER configuration with all fields', () => { const config = glue.PartitionProjectionConfiguration.integer({ - range: [2020, 2023], + min: 2020, + max: 2023, interval: 1, digits: 4, }); @@ -57,7 +59,8 @@ describe('PartitionProjectionConfiguration', () => { describe('Date', () => { test('creates DATE configuration with required fields only', () => { const config = glue.PartitionProjectionConfiguration.date({ - range: ['2020-01-01', '2023-12-31'], + min: '2020-01-01', + max: '2023-12-31', format: 'yyyy-MM-dd', }); @@ -70,7 +73,8 @@ describe('PartitionProjectionConfiguration', () => { test('creates DATE configuration with all fields', () => { const config = glue.PartitionProjectionConfiguration.date({ - range: ['2020-01-01', '2023-12-31'], + min: '2020-01-01', + max: '2023-12-31', format: 'yyyy-MM-dd', interval: 1, intervalUnit: glue.DateIntervalUnit.WEEKS, @@ -85,13 +89,24 @@ describe('PartitionProjectionConfiguration', () => { test('accepts DateIntervalUnit enum values', () => { const config = glue.PartitionProjectionConfiguration.date({ - range: ['2020-01-01', '2023-12-31'], + min: '2020-01-01', + max: '2023-12-31', format: 'yyyy-MM-dd', intervalUnit: glue.DateIntervalUnit.DAYS, }); expect(config.intervalUnit).toBe('DAYS'); }); + + test('supports NOW relative dates', () => { + const config = glue.PartitionProjectionConfiguration.date({ + min: 'NOW-3YEARS', + max: 'NOW', + format: 'yyyy-MM-dd', + }); + + expect(config.range).toEqual(['NOW-3YEARS', 'NOW']); + }); }); describe('Enum', () => { @@ -123,7 +138,8 @@ describe('PartitionProjectionConfiguration', () => { describe('renderParameters', () => { test('renders INTEGER parameters with all fields', () => { const config = glue.PartitionProjectionConfiguration.integer({ - range: [2020, 2023], + min: 2020, + max: 2023, interval: 1, digits: 4, }); @@ -140,7 +156,8 @@ describe('PartitionProjectionConfiguration', () => { test('renders INTEGER parameters with required fields only', () => { const config = glue.PartitionProjectionConfiguration.integer({ - range: [0, 100], + min: 0, + max: 100, }); const params = config._renderParameters('year'); @@ -153,7 +170,8 @@ describe('PartitionProjectionConfiguration', () => { test('renders DATE parameters with all fields', () => { const config = glue.PartitionProjectionConfiguration.date({ - range: ['2020-01-01', '2023-12-31'], + min: '2020-01-01', + max: '2023-12-31', format: 'yyyy-MM-dd', interval: 1, intervalUnit: glue.DateIntervalUnit.DAYS, @@ -172,7 +190,8 @@ describe('PartitionProjectionConfiguration', () => { test('renders DATE parameters with required fields only', () => { const config = glue.PartitionProjectionConfiguration.date({ - range: ['2020-01-01', '2023-12-31'], + min: '2020-01-01', + max: '2023-12-31', format: 'yyyy-MM-dd', }); diff --git a/packages/@aws-cdk/aws-glue-alpha/test/table-base.test.ts b/packages/@aws-cdk/aws-glue-alpha/test/table-base.test.ts index a9c27adac1154..12d2eff32e635 100644 --- a/packages/@aws-cdk/aws-glue-alpha/test/table-base.test.ts +++ b/packages/@aws-cdk/aws-glue-alpha/test/table-base.test.ts @@ -602,7 +602,8 @@ describe('Partition Projection', () => { dataFormat: glue.DataFormat.JSON, partitionProjection: { year: glue.PartitionProjectionConfiguration.integer({ - range: [2020, 2023], + min: 2020, + max: 2023, interval: 1, }), }, @@ -637,7 +638,8 @@ describe('Partition Projection', () => { dataFormat: glue.DataFormat.JSON, partitionProjection: { year: glue.PartitionProjectionConfiguration.integer({ - range: [2020, 2023], + min: 2020, + max: 2023, digits: 4, }), }, @@ -672,7 +674,8 @@ describe('Partition Projection', () => { dataFormat: glue.DataFormat.JSON, partitionProjection: { date: glue.PartitionProjectionConfiguration.date({ - range: ['2020-01-01', '2023-12-31'], + min: '2020-01-01', + max: '2023-12-31', format: 'yyyy-MM-dd', interval: 1, intervalUnit: glue.DateIntervalUnit.DAYS, @@ -784,10 +787,12 @@ describe('Partition Projection', () => { dataFormat: glue.DataFormat.JSON, partitionProjection: { year: glue.PartitionProjectionConfiguration.integer({ - range: [2020, 2023], + min: 2020, + max: 2023, }), month: glue.PartitionProjectionConfiguration.integer({ - range: [1, 12], + min: 1, + max: 12, digits: 2, }), region: glue.PartitionProjectionConfiguration.enum({ From 0e37abc3c2ef3144c140ff0480f9a2b1b6075932 Mon Sep 17 00:00:00 2001 From: Kazuho Cryer-Shinozuka Date: Sun, 5 Oct 2025 00:31:20 +0900 Subject: [PATCH 04/16] update --- .../aws-glue-alpha/lib/partition-projection.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts b/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts index 7ae33834c4ea6..26ffec5e5ed48 100644 --- a/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts +++ b/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts @@ -103,7 +103,7 @@ export interface IntegerPartitionProjectionConfigurationProps { * * With digits: 4, partition values: 0001, 0002, ..., 0100 * - * @default - no padding + * @default - no static number of digits and no leading zeroes */ readonly digits?: number; } @@ -160,14 +160,22 @@ export interface DatePartitionProjectionConfigurationProps { /** * Interval between partition values. * - * @default 1 + * When the provided dates are at single-day or single-month precision, + * the interval is optional and defaults to 1 day or 1 month, respectively. + * Otherwise, interval is required. + * + * @default - 1 for single-day or single-month precision, otherwise required */ readonly interval?: number; /** * Unit for the interval. * - * @default DateIntervalUnit.DAYS + * When the provided dates are at single-day or single-month precision, + * the intervalUnit is optional and defaults to 1 day or 1 month, respectively. + * Otherwise, the intervalUnit is required. + * + * @default - DAYS for single-day precision, MONTHS for single-month precision, otherwise required */ readonly intervalUnit?: DateIntervalUnit; } From 37fa29e278c7bfe88822cd306babbe88e3ba3437 Mon Sep 17 00:00:00 2001 From: Kazuho Cryer-Shinozuka Date: Sun, 5 Oct 2025 22:07:21 +0900 Subject: [PATCH 05/16] refactor --- .../lib/partition-projection.ts | 118 +++++++++--------- .../test/partition-projection.test.ts | 16 +-- 2 files changed, 65 insertions(+), 69 deletions(-) diff --git a/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts b/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts index 26ffec5e5ed48..81b30524ef7b3 100644 --- a/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts +++ b/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts @@ -194,44 +194,16 @@ export interface EnumPartitionProjectionConfigurationProps { /** * Factory class for creating partition projection configurations. - * - * Provides static factory methods for each partition projection type. - * - * @example - * // Integer partition - * const intConfig = PartitionProjectionConfiguration.integer({ - * min: 2020, - * max: 2023, - * interval: 1, - * digits: 4, - * }); - * - * // Date partition - * const dateConfig = PartitionProjectionConfiguration.date({ - * min: '2020-01-01', - * max: '2023-12-31', - * format: 'yyyy-MM-dd', - * intervalUnit: DateIntervalUnit.DAYS, - * }); - * - * // Enum partition - * const enumConfig = PartitionProjectionConfiguration.Enum({ - * values: ['us-east-1', 'us-west-2'], - * }); - * - * // Injected partition - * const injectedConfig = PartitionProjectionConfiguration.Injected(); */ export class PartitionProjectionConfiguration { /** * Create an INTEGER partition projection configuration. - * - * @param props Configuration properties */ public static integer(props: IntegerPartitionProjectionConfigurationProps): PartitionProjectionConfiguration { return new PartitionProjectionConfiguration( PartitionProjectionType.INTEGER, - [props.min, props.max], + [props.min, props.max], // integerRange + undefined, // dateRange props.interval, props.digits, undefined, @@ -242,13 +214,12 @@ export class PartitionProjectionConfiguration { /** * Create a DATE partition projection configuration. - * - * @param props Configuration properties */ public static date(props: DatePartitionProjectionConfigurationProps): PartitionProjectionConfiguration { return new PartitionProjectionConfiguration( PartitionProjectionType.DATE, - [props.min, props.max], + undefined, // integerRange + [props.min, props.max], // dateRange props.interval, undefined, props.format, @@ -259,13 +230,12 @@ export class PartitionProjectionConfiguration { /** * Create an ENUM partition projection configuration. - * - * @param props Configuration properties */ public static enum(props: EnumPartitionProjectionConfigurationProps): PartitionProjectionConfiguration { return new PartitionProjectionConfiguration( PartitionProjectionType.ENUM, - undefined, + undefined, // integerRange + undefined, // dateRange undefined, undefined, undefined, @@ -284,7 +254,8 @@ export class PartitionProjectionConfiguration { public static injected(): PartitionProjectionConfiguration { return new PartitionProjectionConfiguration( PartitionProjectionType.INJECTED, - undefined, + undefined, // integerRange + undefined, // dateRange undefined, undefined, undefined, @@ -300,12 +271,18 @@ export class PartitionProjectionConfiguration { public readonly type: PartitionProjectionType, /** - * Range of partition values. + * Range of partition values for INTEGER type. + * + * Array of [min, max] as numbers. + */ + public readonly integerRange?: number[], + + /** + * Range of partition values for DATE type. * - * For INTEGER: [min, max] as numbers - * For DATE: [start, end] as ISO 8601 strings + * Array of [start, end] as date strings. */ - public readonly range?: number[] | string[], + public readonly dateRange?: string[], /** * Interval between partition values. @@ -337,7 +314,6 @@ export class PartitionProjectionConfiguration { * Renders CloudFormation parameters for this partition projection configuration. * * @param columnName - The partition column name - * @returns CloudFormation parameters as key-value pairs * @internal */ public _renderParameters(columnName: string): { [key: string]: string } { @@ -347,8 +323,13 @@ export class PartitionProjectionConfiguration { switch (this.type) { case PartitionProjectionType.INTEGER: { - const intRange = this.range as number[]; - params[`projection.${columnName}.range`] = `${intRange[0]},${intRange[1]}`; + if (!this.integerRange) { + throw new UnscopedValidationError( + `INTEGER partition projection for "${columnName}" is missing integerRange configuration`, + ); + } + const [min, max] = this.integerRange; + params[`projection.${columnName}.range`] = `${min},${max}`; if (this.interval !== undefined) { params[`projection.${columnName}.interval`] = this.interval.toString(); } @@ -358,9 +339,19 @@ export class PartitionProjectionConfiguration { break; } case PartitionProjectionType.DATE: { - const dateRange = this.range as string[]; - params[`projection.${columnName}.range`] = `${dateRange[0]},${dateRange[1]}`; - params[`projection.${columnName}.format`] = this.format!; + if (!this.dateRange) { + throw new UnscopedValidationError( + `DATE partition projection for "${columnName}" is missing dateRange configuration`, + ); + } + if (!this.format) { + throw new UnscopedValidationError( + `DATE partition projection for "${columnName}" is missing format configuration`, + ); + } + const [start, end] = this.dateRange; + params[`projection.${columnName}.range`] = `${start},${end}`; + params[`projection.${columnName}.format`] = this.format; if (this.interval !== undefined) { params[`projection.${columnName}.interval`] = this.interval.toString(); } @@ -370,7 +361,12 @@ export class PartitionProjectionConfiguration { break; } case PartitionProjectionType.ENUM: { - params[`projection.${columnName}.values`] = this.values!.join(','); + if (!this.values) { + throw new UnscopedValidationError( + `ENUM partition projection for "${columnName}" is missing values configuration`, + ); + } + params[`projection.${columnName}.values`] = this.values.join(','); break; } case PartitionProjectionType.INJECTED: { @@ -427,24 +423,23 @@ export function validateIntegerPartition( ); } - // Validate range - if (!config.range || config.range.length !== 2) { + // Validate integerRange + if (!config.integerRange || config.integerRange.length !== 2) { throw new UnscopedValidationError( - `INTEGER partition projection range for "${columnName}" must be [min, max], but got array of length ${config.range?.length ?? 0}`, + `INTEGER partition projection integerRange for "${columnName}" must be [min, max], but got array of length ${config.integerRange?.length ?? 0}`, ); } - const range = config.range as number[]; - const [min, max] = range; + const [min, max] = config.integerRange; if (!Number.isInteger(min) || !Number.isInteger(max)) { throw new UnscopedValidationError( - `INTEGER partition projection range for "${columnName}" must contain integers, but got [${min}, ${max}]`, + `INTEGER partition projection integerRange for "${columnName}" must contain integers, but got [${min}, ${max}]`, ); } if (min > max) { throw new UnscopedValidationError( - `INTEGER partition projection range for "${columnName}" must be [min, max] where min <= max, but got [${min}, ${max}]`, + `INTEGER partition projection integerRange for "${columnName}" must be [min, max] where min <= max, but got [${min}, ${max}]`, ); } @@ -484,24 +479,23 @@ export function validateDatePartition( ); } - // Validate range - if (!config.range || config.range.length !== 2) { + // Validate dateRange + if (!config.dateRange || config.dateRange.length !== 2) { throw new UnscopedValidationError( - `DATE partition projection range for "${columnName}" must be [start, end], but got array of length ${config.range?.length ?? 0}`, + `DATE partition projection dateRange for "${columnName}" must be [start, end], but got array of length ${config.dateRange?.length ?? 0}`, ); } - const range = config.range as string[]; - const [start, end] = range; + const [start, end] = config.dateRange; if (typeof start !== 'string' || typeof end !== 'string') { throw new UnscopedValidationError( - `DATE partition projection range for "${columnName}" must contain strings, but got [${typeof start}, ${typeof end}]`, + `DATE partition projection dateRange for "${columnName}" must contain strings, but got [${typeof start}, ${typeof end}]`, ); } if (start.trim() === '' || end.trim() === '') { throw new UnscopedValidationError( - `DATE partition projection range for "${columnName}" must not contain empty strings`, + `DATE partition projection dateRange for "${columnName}" must not contain empty strings`, ); } diff --git a/packages/@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts b/packages/@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts index 1b306a14d2d94..e3cc4a6897478 100644 --- a/packages/@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts +++ b/packages/@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts @@ -33,7 +33,7 @@ describe('PartitionProjectionConfiguration', () => { }); expect(config.type).toBe(glue.PartitionProjectionType.INTEGER); - expect(config.range).toEqual([0, 100]); + expect(config.integerRange).toEqual([0, 100]); expect(config.interval).toBeUndefined(); expect(config.digits).toBeUndefined(); expect(config.format).toBeUndefined(); @@ -50,7 +50,7 @@ describe('PartitionProjectionConfiguration', () => { }); expect(config.type).toBe(glue.PartitionProjectionType.INTEGER); - expect(config.range).toEqual([2020, 2023]); + expect(config.integerRange).toEqual([2020, 2023]); expect(config.interval).toBe(1); expect(config.digits).toBe(4); }); @@ -65,7 +65,7 @@ describe('PartitionProjectionConfiguration', () => { }); expect(config.type).toBe(glue.PartitionProjectionType.DATE); - expect(config.range).toEqual(['2020-01-01', '2023-12-31']); + expect(config.dateRange).toEqual(['2020-01-01', '2023-12-31']); expect(config.format).toBe('yyyy-MM-dd'); expect(config.interval).toBeUndefined(); expect(config.intervalUnit).toBeUndefined(); @@ -81,7 +81,7 @@ describe('PartitionProjectionConfiguration', () => { }); expect(config.type).toBe(glue.PartitionProjectionType.DATE); - expect(config.range).toEqual(['2020-01-01', '2023-12-31']); + expect(config.dateRange).toEqual(['2020-01-01', '2023-12-31']); expect(config.format).toBe('yyyy-MM-dd'); expect(config.interval).toBe(1); expect(config.intervalUnit).toBe(glue.DateIntervalUnit.WEEKS); @@ -105,7 +105,7 @@ describe('PartitionProjectionConfiguration', () => { format: 'yyyy-MM-dd', }); - expect(config.range).toEqual(['NOW-3YEARS', 'NOW']); + expect(config.dateRange).toEqual(['NOW-3YEARS', 'NOW']); }); }); @@ -117,7 +117,8 @@ describe('PartitionProjectionConfiguration', () => { expect(config.type).toBe(glue.PartitionProjectionType.ENUM); expect(config.values).toEqual(['us-east-1', 'us-west-2', 'eu-west-1']); - expect(config.range).toBeUndefined(); + expect(config.integerRange).toBeUndefined(); + expect(config.dateRange).toBeUndefined(); expect(config.interval).toBeUndefined(); expect(config.format).toBeUndefined(); }); @@ -128,7 +129,8 @@ describe('PartitionProjectionConfiguration', () => { const config = glue.PartitionProjectionConfiguration.injected(); expect(config.type).toBe(glue.PartitionProjectionType.INJECTED); - expect(config.range).toBeUndefined(); + expect(config.integerRange).toBeUndefined(); + expect(config.dateRange).toBeUndefined(); expect(config.interval).toBeUndefined(); expect(config.values).toBeUndefined(); expect(config.format).toBeUndefined(); From 5c8213207402a4ff183704dfd1ad00181c2e03ff Mon Sep 17 00:00:00 2001 From: Kazuho Cryer-Shinozuka Date: Sun, 5 Oct 2025 22:12:35 +0900 Subject: [PATCH 06/16] refactor --- .../lib/partition-projection.ts | 208 +++++++++++------- 1 file changed, 123 insertions(+), 85 deletions(-) diff --git a/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts b/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts index 81b30524ef7b3..fba921a5d9100 100644 --- a/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts +++ b/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts @@ -192,6 +192,55 @@ export interface EnumPartitionProjectionConfigurationProps { readonly values: string[]; } +/** + * Internal properties for PartitionProjectionConfiguration. + */ +interface PartitionProjectionConfigurationProps { + /** + * The type of partition projection. + */ + readonly type: PartitionProjectionType; + + /** + * Range of partition values for INTEGER type. + * + * Array of [min, max] as numbers. + */ + readonly integerRange?: number[]; + + /** + * Range of partition values for DATE type. + * + * Array of [start, end] as date strings. + */ + readonly dateRange?: string[]; + + /** + * Interval between partition values. + */ + readonly interval?: number; + + /** + * Number of digits to pad INTEGER partition values. + */ + readonly digits?: number; + + /** + * Date format for DATE partition values (Java SimpleDateFormat). + */ + readonly format?: string; + + /** + * Unit for DATE partition interval. + */ + readonly intervalUnit?: DateIntervalUnit; + + /** + * Explicit list of values for ENUM partitions. + */ + readonly values?: string[]; +} + /** * Factory class for creating partition projection configurations. */ @@ -200,48 +249,35 @@ export class PartitionProjectionConfiguration { * Create an INTEGER partition projection configuration. */ public static integer(props: IntegerPartitionProjectionConfigurationProps): PartitionProjectionConfiguration { - return new PartitionProjectionConfiguration( - PartitionProjectionType.INTEGER, - [props.min, props.max], // integerRange - undefined, // dateRange - props.interval, - props.digits, - undefined, - undefined, - undefined, - ); + return new PartitionProjectionConfiguration({ + type: PartitionProjectionType.INTEGER, + integerRange: [props.min, props.max], + interval: props.interval, + digits: props.digits, + }); } /** * Create a DATE partition projection configuration. */ public static date(props: DatePartitionProjectionConfigurationProps): PartitionProjectionConfiguration { - return new PartitionProjectionConfiguration( - PartitionProjectionType.DATE, - undefined, // integerRange - [props.min, props.max], // dateRange - props.interval, - undefined, - props.format, - props.intervalUnit, - undefined, - ); + return new PartitionProjectionConfiguration({ + type: PartitionProjectionType.DATE, + dateRange: [props.min, props.max], + interval: props.interval, + format: props.format, + intervalUnit: props.intervalUnit, + }); } /** * Create an ENUM partition projection configuration. */ public static enum(props: EnumPartitionProjectionConfigurationProps): PartitionProjectionConfiguration { - return new PartitionProjectionConfiguration( - PartitionProjectionType.ENUM, - undefined, // integerRange - undefined, // dateRange - undefined, - undefined, - undefined, - undefined, - props.values, - ); + return new PartitionProjectionConfiguration({ + type: PartitionProjectionType.ENUM, + values: props.values, + }); } /** @@ -252,63 +288,65 @@ export class PartitionProjectionConfiguration { * @see https://docs.aws.amazon.com/athena/latest/ug/partition-projection-supported-types.html#partition-projection-injected-type */ public static injected(): PartitionProjectionConfiguration { - return new PartitionProjectionConfiguration( - PartitionProjectionType.INJECTED, - undefined, // integerRange - undefined, // dateRange - undefined, - undefined, - undefined, - undefined, - undefined, - ); + return new PartitionProjectionConfiguration({ + type: PartitionProjectionType.INJECTED, + }); } - private constructor( - /** - * The type of partition projection. - */ - public readonly type: PartitionProjectionType, - - /** - * Range of partition values for INTEGER type. - * - * Array of [min, max] as numbers. - */ - public readonly integerRange?: number[], - - /** - * Range of partition values for DATE type. - * - * Array of [start, end] as date strings. - */ - public readonly dateRange?: string[], - - /** - * Interval between partition values. - */ - public readonly interval?: number, - - /** - * Number of digits to pad INTEGER partition values. - */ - public readonly digits?: number, - - /** - * Date format for DATE partition values (Java SimpleDateFormat). - */ - public readonly format?: string, - - /** - * Unit for DATE partition interval. - */ - public readonly intervalUnit?: DateIntervalUnit, - - /** - * Explicit list of values for ENUM partitions. - */ - public readonly values?: string[], - ) {} + /** + * The type of partition projection. + */ + public readonly type: PartitionProjectionType; + + /** + * Range of partition values for INTEGER type. + * + * Array of [min, max] as numbers. + */ + public readonly integerRange?: number[]; + + /** + * Range of partition values for DATE type. + * + * Array of [start, end] as date strings. + */ + public readonly dateRange?: string[]; + + /** + * Interval between partition values. + */ + public readonly interval?: number; + + /** + * Number of digits to pad INTEGER partition values. + */ + public readonly digits?: number; + + /** + * Date format for DATE partition values (Java SimpleDateFormat). + */ + public readonly format?: string; + + /** + * Unit for DATE partition interval. + */ + public readonly intervalUnit?: DateIntervalUnit; + + /** + * Explicit list of values for ENUM partitions. + */ + public readonly values?: string[]; + + private constructor(props: PartitionProjectionConfigurationProps) { + this.type = props.type; + this.integerRange = props.integerRange; + this.dateRange = props.dateRange; + this.interval = props.interval; + this.digits = props.digits; + this.format = props.format; + this.intervalUnit = props.intervalUnit; + this.values = props.values; + } /** * Renders CloudFormation parameters for this partition projection configuration. From daa43b68f34a4112de7bfc8d810cf19a615102c8 Mon Sep 17 00:00:00 2001 From: Kazuho Cryer-Shinozuka Date: Thu, 9 Oct 2025 23:05:10 +0900 Subject: [PATCH 07/16] style --- .../lib/partition-projection.ts | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts b/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts index fba921a5d9100..9ed1974d4f07f 100644 --- a/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts +++ b/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts @@ -77,17 +77,11 @@ export enum DateIntervalUnit { export interface IntegerPartitionProjectionConfigurationProps { /** * Minimum value for the integer partition range (inclusive). - * - * @example 2020 - * @example 0 */ readonly min: number; /** * Maximum value for the integer partition range (inclusive). - * - * @example 2023 - * @example 100 */ readonly max: number; @@ -122,10 +116,6 @@ export interface DatePartitionProjectionConfigurationProps { * (e.g., 'NOW', 'NOW-3YEARS', 'NOW+1MONTH') * * @see https://docs.aws.amazon.com/athena/latest/ug/partition-projection-supported-types.html#partition-projection-date-type - * - * @example '2020-01-01' - * @example 'NOW-3YEARS' - * @example '2020-01-01-00-00-00' */ readonly min: string; @@ -137,10 +127,6 @@ export interface DatePartitionProjectionConfigurationProps { * - Relative date using NOW syntax * * Same format constraints as `min`. - * - * @example '2023-12-31' - * @example 'NOW' - * @example 'NOW+1MONTH' */ readonly max: string; @@ -150,10 +136,6 @@ export interface DatePartitionProjectionConfigurationProps { * Uses Java SimpleDateFormat patterns. * * @see https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html - * - * @example 'yyyy-MM-dd' - * @example 'yyyy-MM-dd-HH-mm-ss' - * @example 'yyyyMMdd' */ readonly format: string; @@ -429,16 +411,6 @@ export class PartitionProjectionConfiguration { * * Maps partition column names to their projection configurations. * The key is the partition column name, the value is the partition configuration. - * - * @example - * { - * year: PartitionProjectionConfiguration.Integer({ - * range: [2020, 2023], - * }), - * region: PartitionProjectionConfiguration.Enum({ - * values: ['us-east-1', 'us-west-2'], - * }), - * } */ export type PartitionProjection = { [columnName: string]: PartitionProjectionConfiguration; @@ -449,7 +421,6 @@ export type PartitionProjection = { * * @param columnName - The partition column name * @param config - The partition configuration - * @throws {UnscopedValidationError} if the configuration is invalid */ export function validateIntegerPartition( columnName: string, @@ -505,7 +476,6 @@ export function validateIntegerPartition( * * @param columnName - The partition column name * @param config - The partition configuration - * @throws {UnscopedValidationError} if the configuration is invalid */ export function validateDatePartition( columnName: string, @@ -569,7 +539,6 @@ export function validateDatePartition( * * @param columnName - The partition column name * @param config - The partition configuration - * @throws {UnscopedValidationError} if the configuration is invalid */ export function validateEnumPartition( columnName: string, @@ -608,7 +577,6 @@ export function validateEnumPartition( * * @param _columnName - The partition column name * @param config - The partition configuration - * @throws {UnscopedValidationError} if the configuration is invalid */ export function validateInjectedPartition( _columnName: string, @@ -628,7 +596,6 @@ export function validateInjectedPartition( * * @param columnName - The partition column name * @param config - The partition configuration - * @throws {UnscopedValidationError} if the configuration is invalid */ export function validatePartitionConfiguration( columnName: string, From ea65c7600fbd0dac69dc91d4aaeb01de7f9d73e2 Mon Sep 17 00:00:00 2001 From: Kazuho Cryer-Shinozuka Date: Sat, 11 Oct 2025 21:40:12 +0900 Subject: [PATCH 08/16] update unit test --- .../lib/partition-projection.ts | 308 +++++------------- .../@aws-cdk/aws-glue-alpha/lib/table-base.ts | 19 +- .../test/partition-projection.test.ts | 283 +++++----------- .../aws-glue-alpha/test/table-base.test.ts | 36 +- 4 files changed, 155 insertions(+), 491 deletions(-) diff --git a/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts b/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts index 9ed1974d4f07f..ce71e6c9240ce 100644 --- a/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts +++ b/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts @@ -231,6 +231,38 @@ export class PartitionProjectionConfiguration { * Create an INTEGER partition projection configuration. */ public static integer(props: IntegerPartitionProjectionConfigurationProps): PartitionProjectionConfiguration { + // Validate min/max are integers + if (!Number.isInteger(props.min) || !Number.isInteger(props.max)) { + throw new UnscopedValidationError( + `INTEGER partition projection range must contain integers, but got [${props.min}, ${props.max}]`, + ); + } + + // Validate min <= max + if (props.min > props.max) { + throw new UnscopedValidationError( + `INTEGER partition projection range must be [min, max] where min <= max, but got [${props.min}, ${props.max}]`, + ); + } + + // Validate interval + if (props.interval !== undefined) { + if (!Number.isInteger(props.interval) || props.interval <= 0) { + throw new UnscopedValidationError( + `INTEGER partition projection interval must be a positive integer, but got ${props.interval}`, + ); + } + } + + // Validate digits + if (props.digits !== undefined) { + if (!Number.isInteger(props.digits) || props.digits < 1) { + throw new UnscopedValidationError( + `INTEGER partition projection digits must be an integer >= 1, but got ${props.digits}`, + ); + } + } + return new PartitionProjectionConfiguration({ type: PartitionProjectionType.INTEGER, integerRange: [props.min, props.max], @@ -243,6 +275,29 @@ export class PartitionProjectionConfiguration { * Create a DATE partition projection configuration. */ public static date(props: DatePartitionProjectionConfigurationProps): PartitionProjectionConfiguration { + // Validate min/max are not empty + if (props.min.trim() === '' || props.max.trim() === '') { + throw new UnscopedValidationError( + 'DATE partition projection range must not contain empty strings', + ); + } + + // Validate format is not empty + if (props.format.trim() === '') { + throw new UnscopedValidationError( + 'DATE partition projection format must be a non-empty string', + ); + } + + // Validate interval + if (props.interval !== undefined) { + if (!Number.isInteger(props.interval) || props.interval <= 0) { + throw new UnscopedValidationError( + `DATE partition projection interval must be a positive integer, but got ${props.interval}`, + ); + } + } + return new PartitionProjectionConfiguration({ type: PartitionProjectionType.DATE, dateRange: [props.min, props.max], @@ -256,6 +311,23 @@ export class PartitionProjectionConfiguration { * Create an ENUM partition projection configuration. */ public static enum(props: EnumPartitionProjectionConfigurationProps): PartitionProjectionConfiguration { + // Validate values is not empty + if (props.values.length === 0) { + throw new UnscopedValidationError( + 'ENUM partition projection values must be a non-empty array', + ); + } + + // Validate no empty strings + for (let i = 0; i < props.values.length; i++) { + const value = props.values[i]; + if (value.trim() === '') { + throw new UnscopedValidationError( + 'ENUM partition projection values must not contain empty strings', + ); + } + } + return new PartitionProjectionConfiguration({ type: PartitionProjectionType.ENUM, values: props.values, @@ -343,12 +415,7 @@ export class PartitionProjectionConfiguration { switch (this.type) { case PartitionProjectionType.INTEGER: { - if (!this.integerRange) { - throw new UnscopedValidationError( - `INTEGER partition projection for "${columnName}" is missing integerRange configuration`, - ); - } - const [min, max] = this.integerRange; + const [min, max] = this.integerRange!; params[`projection.${columnName}.range`] = `${min},${max}`; if (this.interval !== undefined) { params[`projection.${columnName}.interval`] = this.interval.toString(); @@ -359,19 +426,9 @@ export class PartitionProjectionConfiguration { break; } case PartitionProjectionType.DATE: { - if (!this.dateRange) { - throw new UnscopedValidationError( - `DATE partition projection for "${columnName}" is missing dateRange configuration`, - ); - } - if (!this.format) { - throw new UnscopedValidationError( - `DATE partition projection for "${columnName}" is missing format configuration`, - ); - } - const [start, end] = this.dateRange; + const [start, end] = this.dateRange!; params[`projection.${columnName}.range`] = `${start},${end}`; - params[`projection.${columnName}.format`] = this.format; + params[`projection.${columnName}.format`] = this.format!; if (this.interval !== undefined) { params[`projection.${columnName}.interval`] = this.interval.toString(); } @@ -381,12 +438,7 @@ export class PartitionProjectionConfiguration { break; } case PartitionProjectionType.ENUM: { - if (!this.values) { - throw new UnscopedValidationError( - `ENUM partition projection for "${columnName}" is missing values configuration`, - ); - } - params[`projection.${columnName}.values`] = this.values.join(','); + params[`projection.${columnName}.values`] = this.values!.join(','); break; } case PartitionProjectionType.INJECTED: { @@ -416,214 +468,6 @@ export type PartitionProjection = { [columnName: string]: PartitionProjectionConfiguration; }; -/** - * Validates INTEGER partition projection configuration. - * - * @param columnName - The partition column name - * @param config - The partition configuration - */ -export function validateIntegerPartition( - columnName: string, - config: PartitionProjectionConfiguration, -): void { - if (config.type !== PartitionProjectionType.INTEGER) { - throw new UnscopedValidationError( - `Expected INTEGER partition type for "${columnName}", but got ${config.type}`, - ); - } - - // Validate integerRange - if (!config.integerRange || config.integerRange.length !== 2) { - throw new UnscopedValidationError( - `INTEGER partition projection integerRange for "${columnName}" must be [min, max], but got array of length ${config.integerRange?.length ?? 0}`, - ); - } - - const [min, max] = config.integerRange; - if (!Number.isInteger(min) || !Number.isInteger(max)) { - throw new UnscopedValidationError( - `INTEGER partition projection integerRange for "${columnName}" must contain integers, but got [${min}, ${max}]`, - ); - } - - if (min > max) { - throw new UnscopedValidationError( - `INTEGER partition projection integerRange for "${columnName}" must be [min, max] where min <= max, but got [${min}, ${max}]`, - ); - } - - // Validate interval - if (config.interval !== undefined) { - if (!Number.isInteger(config.interval) || config.interval <= 0) { - throw new UnscopedValidationError( - `INTEGER partition projection interval for "${columnName}" must be a positive integer, but got ${config.interval}`, - ); - } - } - - // Validate digits - if (config.digits !== undefined) { - if (!Number.isInteger(config.digits) || config.digits < 1) { - throw new UnscopedValidationError( - `INTEGER partition projection digits for "${columnName}" must be an integer >= 1, but got ${config.digits}`, - ); - } - } -} - -/** - * Validates DATE partition projection configuration. - * - * @param columnName - The partition column name - * @param config - The partition configuration - */ -export function validateDatePartition( - columnName: string, - config: PartitionProjectionConfiguration, -): void { - if (config.type !== PartitionProjectionType.DATE) { - throw new UnscopedValidationError( - `Expected DATE partition type for "${columnName}", but got ${config.type}`, - ); - } - - // Validate dateRange - if (!config.dateRange || config.dateRange.length !== 2) { - throw new UnscopedValidationError( - `DATE partition projection dateRange for "${columnName}" must be [start, end], but got array of length ${config.dateRange?.length ?? 0}`, - ); - } - - const [start, end] = config.dateRange; - if (typeof start !== 'string' || typeof end !== 'string') { - throw new UnscopedValidationError( - `DATE partition projection dateRange for "${columnName}" must contain strings, but got [${typeof start}, ${typeof end}]`, - ); - } - - if (start.trim() === '' || end.trim() === '') { - throw new UnscopedValidationError( - `DATE partition projection dateRange for "${columnName}" must not contain empty strings`, - ); - } - - // Validate format - if (typeof config.format !== 'string' || config.format.trim() === '') { - throw new UnscopedValidationError( - `DATE partition projection format for "${columnName}" must be a non-empty string`, - ); - } - - // Validate interval - if (config.interval !== undefined) { - if (!Number.isInteger(config.interval) || config.interval <= 0) { - throw new UnscopedValidationError( - `DATE partition projection interval for "${columnName}" must be a positive integer, but got ${config.interval}`, - ); - } - } - - // Validate interval unit - if (config.intervalUnit !== undefined) { - const validUnits = Object.values(DateIntervalUnit); - if (!validUnits.includes(config.intervalUnit)) { - throw new UnscopedValidationError( - `DATE partition projection interval unit for "${columnName}" must be one of ${validUnits.join(', ')}, but got ${config.intervalUnit}`, - ); - } - } -} - -/** - * Validates ENUM partition projection configuration. - * - * @param columnName - The partition column name - * @param config - The partition configuration - */ -export function validateEnumPartition( - columnName: string, - config: PartitionProjectionConfiguration, -): void { - if (config.type !== PartitionProjectionType.ENUM) { - throw new UnscopedValidationError( - `Expected ENUM partition type for "${columnName}", but got ${config.type}`, - ); - } - - // Validate values - if (!Array.isArray(config.values) || config.values.length === 0) { - throw new UnscopedValidationError( - `ENUM partition projection values for "${columnName}" must be a non-empty array`, - ); - } - - for (let i = 0; i < config.values.length; i++) { - const value = config.values[i]; - if (typeof value !== 'string') { - throw new UnscopedValidationError( - `ENUM partition projection values for "${columnName}" must contain only strings, but found ${typeof value} at index ${i}`, - ); - } - if (value.trim() === '') { - throw new UnscopedValidationError( - `ENUM partition projection values for "${columnName}" must not contain empty strings`, - ); - } - } -} - -/** - * Validates INJECTED partition projection configuration. - * - * @param _columnName - The partition column name - * @param config - The partition configuration - */ -export function validateInjectedPartition( - _columnName: string, - config: PartitionProjectionConfiguration, -): void { - if (config.type !== PartitionProjectionType.INJECTED) { - throw new UnscopedValidationError( - `Expected INJECTED partition type for "${_columnName}", but got ${config.type}`, - ); - } - - // INJECTED type has no additional properties to validate -} - -/** - * Validates partition projection configuration based on its type. - * - * @param columnName - The partition column name - * @param config - The partition configuration - */ -export function validatePartitionConfiguration( - columnName: string, - config: PartitionProjectionConfiguration, -): void { - switch (config.type) { - case PartitionProjectionType.INTEGER: - validateIntegerPartition(columnName, config); - break; - case PartitionProjectionType.DATE: - validateDatePartition(columnName, config); - break; - case PartitionProjectionType.ENUM: - validateEnumPartition(columnName, config); - break; - case PartitionProjectionType.INJECTED: - validateInjectedPartition(columnName, config); - break; - default: { - // TypeScript exhaustiveness check - const exhaustiveCheck: never = config.type; - throw new UnscopedValidationError( - `Unknown partition projection type for "${columnName}": ${exhaustiveCheck}`, - ); - } - } -} - /** * Generates CloudFormation parameters for partition projection configuration. * diff --git a/packages/@aws-cdk/aws-glue-alpha/lib/table-base.ts b/packages/@aws-cdk/aws-glue-alpha/lib/table-base.ts index 4610f9c6558b1..87f09dd2fe6f8 100644 --- a/packages/@aws-cdk/aws-glue-alpha/lib/table-base.ts +++ b/packages/@aws-cdk/aws-glue-alpha/lib/table-base.ts @@ -6,7 +6,7 @@ import { AwsCustomResource } from 'aws-cdk-lib/custom-resources'; import { Construct } from 'constructs'; import { DataFormat } from './data-format'; import { IDatabase } from './database'; -import { validatePartitionConfiguration, generatePartitionProjectionParameters } from './partition-projection'; +import { generatePartitionProjectionParameters, PartitionProjection } from './partition-projection'; import { Column } from './schema'; import { StorageParameter } from './storage-parameter'; @@ -169,7 +169,7 @@ export interface TableBaseProps { * * @default - No partition projection */ - readonly partitionProjection?: import('./partition-projection').PartitionProjection; + readonly partitionProjection?: PartitionProjection; } /** @@ -239,7 +239,7 @@ export abstract class TableBase extends Resource implements ITable { /** * This table's partition projection configuration if enabled. */ - public readonly partitionProjection?: import('./partition-projection').PartitionProjection; + public readonly partitionProjection?: PartitionProjection; /** * The tables' properties associated with the table. @@ -274,10 +274,7 @@ export abstract class TableBase extends Resource implements ITable { this.compressed = props.compressed ?? false; - // Validate and generate partition projection parameters - if (this.partitionProjection) { - this.validateAndGeneratePartitionProjection(); - } + this.validateAndGeneratePartitionProjection(); } public abstract grantRead(grantee: iam.IGrantable): iam.Grant; @@ -352,10 +349,7 @@ export abstract class TableBase extends Resource implements ITable { } /** - * Validate partition projection configuration and merge generated - * parameters into this.parameters. - * - * @throws {ValidationError} if partition projection configuration is invalid + * Validate partition projection configuration and merge generated parameters into this.parameters. */ private validateAndGeneratePartitionProjection(): void { if (!this.partitionProjection) { @@ -383,9 +377,6 @@ export abstract class TableBase extends Resource implements ITable { ); } - // Validate the configuration based on type - validatePartitionConfiguration(columnName, config); - // Generate CloudFormation parameters const generatedParams = generatePartitionProjectionParameters(columnName, config); diff --git a/packages/@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts b/packages/@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts index e3cc4a6897478..f900dece38b2c 100644 --- a/packages/@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts +++ b/packages/@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts @@ -1,232 +1,95 @@ import * as glue from '../lib'; -describe('DateIntervalUnit', () => { - test('has correct enum values', () => { - expect(glue.DateIntervalUnit.YEARS).toBe('YEARS'); - expect(glue.DateIntervalUnit.MONTHS).toBe('MONTHS'); - expect(glue.DateIntervalUnit.WEEKS).toBe('WEEKS'); - expect(glue.DateIntervalUnit.DAYS).toBe('DAYS'); - expect(glue.DateIntervalUnit.HOURS).toBe('HOURS'); - expect(glue.DateIntervalUnit.MINUTES).toBe('MINUTES'); - expect(glue.DateIntervalUnit.SECONDS).toBe('SECONDS'); - }); - - test('has all 7 interval units', () => { - const values = Object.values(glue.DateIntervalUnit); - expect(values).toHaveLength(7); - expect(values).toContain('YEARS'); - expect(values).toContain('MONTHS'); - expect(values).toContain('WEEKS'); - expect(values).toContain('DAYS'); - expect(values).toContain('HOURS'); - expect(values).toContain('MINUTES'); - expect(values).toContain('SECONDS'); - }); -}); - -describe('PartitionProjectionConfiguration', () => { - describe('Integer', () => { - test('creates INTEGER configuration with required fields only', () => { - const config = glue.PartitionProjectionConfiguration.integer({ - min: 0, - max: 100, - }); - - expect(config.type).toBe(glue.PartitionProjectionType.INTEGER); - expect(config.integerRange).toEqual([0, 100]); - expect(config.interval).toBeUndefined(); - expect(config.digits).toBeUndefined(); - expect(config.format).toBeUndefined(); - expect(config.intervalUnit).toBeUndefined(); - expect(config.values).toBeUndefined(); - }); - - test('creates INTEGER configuration with all fields', () => { - const config = glue.PartitionProjectionConfiguration.integer({ - min: 2020, - max: 2023, - interval: 1, - digits: 4, - }); - - expect(config.type).toBe(glue.PartitionProjectionType.INTEGER); - expect(config.integerRange).toEqual([2020, 2023]); - expect(config.interval).toBe(1); - expect(config.digits).toBe(4); - }); - }); - - describe('Date', () => { - test('creates DATE configuration with required fields only', () => { - const config = glue.PartitionProjectionConfiguration.date({ - min: '2020-01-01', - max: '2023-12-31', - format: 'yyyy-MM-dd', - }); - - expect(config.type).toBe(glue.PartitionProjectionType.DATE); - expect(config.dateRange).toEqual(['2020-01-01', '2023-12-31']); - expect(config.format).toBe('yyyy-MM-dd'); - expect(config.interval).toBeUndefined(); - expect(config.intervalUnit).toBeUndefined(); +describe('PartitionProjectionConfiguration Validation', () => { + describe('INTEGER validation', () => { + test.each([ + [1.5, 10], + [1, 10.5], + ])('throws when min=%p or max=%p is not an integer', (min, max) => { + expect(() => { + glue.PartitionProjectionConfiguration.integer({ min, max }); + }).toThrow(`INTEGER partition projection range must contain integers, but got [${min}, ${max}]`); }); - test('creates DATE configuration with all fields', () => { - const config = glue.PartitionProjectionConfiguration.date({ - min: '2020-01-01', - max: '2023-12-31', - format: 'yyyy-MM-dd', - interval: 1, - intervalUnit: glue.DateIntervalUnit.WEEKS, - }); - - expect(config.type).toBe(glue.PartitionProjectionType.DATE); - expect(config.dateRange).toEqual(['2020-01-01', '2023-12-31']); - expect(config.format).toBe('yyyy-MM-dd'); - expect(config.interval).toBe(1); - expect(config.intervalUnit).toBe(glue.DateIntervalUnit.WEEKS); + test('throws when min > max', () => { + expect(() => { + glue.PartitionProjectionConfiguration.integer({ + min: 10, + max: 5, + }); + }).toThrow('INTEGER partition projection range must be [min, max] where min <= max, but got [10, 5]'); }); - test('accepts DateIntervalUnit enum values', () => { - const config = glue.PartitionProjectionConfiguration.date({ - min: '2020-01-01', - max: '2023-12-31', - format: 'yyyy-MM-dd', - intervalUnit: glue.DateIntervalUnit.DAYS, - }); - - expect(config.intervalUnit).toBe('DAYS'); + test.each([0, -1, 1.5])('throws when interval=%p is invalid', (interval) => { + expect(() => { + glue.PartitionProjectionConfiguration.integer({ + min: 1, + max: 10, + interval, + }); + }).toThrow(`INTEGER partition projection interval must be a positive integer, but got ${interval}`); }); - test('supports NOW relative dates', () => { - const config = glue.PartitionProjectionConfiguration.date({ - min: 'NOW-3YEARS', - max: 'NOW', - format: 'yyyy-MM-dd', - }); - - expect(config.dateRange).toEqual(['NOW-3YEARS', 'NOW']); + test.each([0, -1, 1.5])('throws when digits=%p is invalid', (digits) => { + expect(() => { + glue.PartitionProjectionConfiguration.integer({ + min: 1, + max: 10, + digits, + }); + }).toThrow(`INTEGER partition projection digits must be an integer >= 1, but got ${digits}`); }); }); - describe('Enum', () => { - test('creates ENUM configuration', () => { - const config = glue.PartitionProjectionConfiguration.enum({ - values: ['us-east-1', 'us-west-2', 'eu-west-1'], - }); - - expect(config.type).toBe(glue.PartitionProjectionType.ENUM); - expect(config.values).toEqual(['us-east-1', 'us-west-2', 'eu-west-1']); - expect(config.integerRange).toBeUndefined(); - expect(config.dateRange).toBeUndefined(); - expect(config.interval).toBeUndefined(); - expect(config.format).toBeUndefined(); + describe('DATE validation', () => { + test.each([ + ['', '2023-12-31'], + [' ', '2023-12-31'], + ['2020-01-01', ''], + ])('throws when min=%p or max=%p is empty', (min, max) => { + expect(() => { + glue.PartitionProjectionConfiguration.date({ min, max, format: 'yyyy-MM-dd' }); + }).toThrow('DATE partition projection range must not contain empty strings'); }); - }); - describe('Injected', () => { - test('creates INJECTED configuration', () => { - const config = glue.PartitionProjectionConfiguration.injected(); - - expect(config.type).toBe(glue.PartitionProjectionType.INJECTED); - expect(config.integerRange).toBeUndefined(); - expect(config.dateRange).toBeUndefined(); - expect(config.interval).toBeUndefined(); - expect(config.values).toBeUndefined(); - expect(config.format).toBeUndefined(); + test.each(['', ' '])('throws when format=%p is empty', (format) => { + expect(() => { + glue.PartitionProjectionConfiguration.date({ + min: '2020-01-01', + max: '2023-12-31', + format, + }); + }).toThrow('DATE partition projection format must be a non-empty string'); }); - }); - describe('renderParameters', () => { - test('renders INTEGER parameters with all fields', () => { - const config = glue.PartitionProjectionConfiguration.integer({ - min: 2020, - max: 2023, - interval: 1, - digits: 4, - }); - - const params = config._renderParameters('year'); - - expect(params).toEqual({ - 'projection.year.type': 'integer', - 'projection.year.range': '2020,2023', - 'projection.year.interval': '1', - 'projection.year.digits': '4', - }); - }); - - test('renders INTEGER parameters with required fields only', () => { - const config = glue.PartitionProjectionConfiguration.integer({ - min: 0, - max: 100, - }); - - const params = config._renderParameters('year'); - - expect(params).toEqual({ - 'projection.year.type': 'integer', - 'projection.year.range': '0,100', - }); - }); - - test('renders DATE parameters with all fields', () => { - const config = glue.PartitionProjectionConfiguration.date({ - min: '2020-01-01', - max: '2023-12-31', - format: 'yyyy-MM-dd', - interval: 1, - intervalUnit: glue.DateIntervalUnit.DAYS, - }); - - const params = config._renderParameters('date'); - - expect(params).toEqual({ - 'projection.date.type': 'date', - 'projection.date.range': '2020-01-01,2023-12-31', - 'projection.date.format': 'yyyy-MM-dd', - 'projection.date.interval': '1', - 'projection.date.interval.unit': 'DAYS', - }); + test.each([0, -1, 1.5,])('throws when interval=%p is invalid', (interval) => { + expect(() => { + glue.PartitionProjectionConfiguration.date({ + min: '2020-01-01', + max: '2023-12-31', + format: 'yyyy-MM-dd', + interval, + }); + }).toThrow(`DATE partition projection interval must be a positive integer, but got ${interval}`); }); + }); - test('renders DATE parameters with required fields only', () => { - const config = glue.PartitionProjectionConfiguration.date({ - min: '2020-01-01', - max: '2023-12-31', - format: 'yyyy-MM-dd', - }); - - const params = config._renderParameters('date'); - - expect(params).toEqual({ - 'projection.date.type': 'date', - 'projection.date.range': '2020-01-01,2023-12-31', - 'projection.date.format': 'yyyy-MM-dd', - }); - }); - - test('renders ENUM parameters', () => { - const config = glue.PartitionProjectionConfiguration.enum({ - values: ['us-east-1', 'us-west-2'], - }); - - const params = config._renderParameters('region'); - - expect(params).toEqual({ - 'projection.region.type': 'enum', - 'projection.region.values': 'us-east-1,us-west-2', - }); + describe('ENUM validation', () => { + test('throws when values is empty array', () => { + expect(() => { + glue.PartitionProjectionConfiguration.enum({ + values: [], + }); + }).toThrow('ENUM partition projection values must be a non-empty array'); }); - test('renders INJECTED parameters', () => { - const config = glue.PartitionProjectionConfiguration.injected(); - - const params = config._renderParameters('custom'); - - expect(params).toEqual({ - 'projection.custom.type': 'injected', - }); + test.each([ + [['us-east-1', '', 'us-west-2']], + [['us-east-1', ' ', 'us-west-2']], + ])('throws when values=%p contains empty string', (values) => { + expect(() => { + glue.PartitionProjectionConfiguration.enum({ values }); + }).toThrow( 'ENUM partition projection values must not contain empty strings'); }); }); }); diff --git a/packages/@aws-cdk/aws-glue-alpha/test/table-base.test.ts b/packages/@aws-cdk/aws-glue-alpha/test/table-base.test.ts index 12d2eff32e635..eaeb9fbb175b2 100644 --- a/packages/@aws-cdk/aws-glue-alpha/test/table-base.test.ts +++ b/packages/@aws-cdk/aws-glue-alpha/test/table-base.test.ts @@ -605,41 +605,6 @@ describe('Partition Projection', () => { min: 2020, max: 2023, interval: 1, - }), - }, - }); - - Template.fromStack(stack).hasResourceProperties('AWS::Glue::Table', { - TableInput: { - Parameters: { - 'projection.enabled': 'true', - 'projection.year.type': 'integer', - 'projection.year.range': '2020,2023', - 'projection.year.interval': '1', - }, - }, - }); - }); - - test('creates table with INTEGER partition projection with digits', () => { - const app = new cdk.App(); - const stack = new cdk.Stack(app, 'Stack'); - const database = new glue.Database(stack, 'Database'); - new glue.S3Table(stack, 'Table', { - database, - columns: [{ - name: 'col1', - type: glue.Schema.STRING, - }], - partitionKeys: [{ - name: 'year', - type: glue.Schema.INTEGER, - }], - dataFormat: glue.DataFormat.JSON, - partitionProjection: { - year: glue.PartitionProjectionConfiguration.integer({ - min: 2020, - max: 2023, digits: 4, }), }, @@ -651,6 +616,7 @@ describe('Partition Projection', () => { 'projection.enabled': 'true', 'projection.year.type': 'integer', 'projection.year.range': '2020,2023', + 'projection.year.interval': '1', 'projection.year.digits': '4', }, }, From 0bff68843b657cf2631c7a17a1f1836775d61bbb Mon Sep 17 00:00:00 2001 From: Kazuho Cryer-Shinozuka Date: Sat, 11 Oct 2025 21:41:25 +0900 Subject: [PATCH 09/16] fix --- .../@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts b/packages/@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts index f900dece38b2c..1a1b7efcf88a1 100644 --- a/packages/@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts +++ b/packages/@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts @@ -62,7 +62,7 @@ describe('PartitionProjectionConfiguration Validation', () => { }).toThrow('DATE partition projection format must be a non-empty string'); }); - test.each([0, -1, 1.5,])('throws when interval=%p is invalid', (interval) => { + test.each([0, -1, 1.5])('throws when interval=%p is invalid', (interval) => { expect(() => { glue.PartitionProjectionConfiguration.date({ min: '2020-01-01', @@ -89,7 +89,7 @@ describe('PartitionProjectionConfiguration Validation', () => { ])('throws when values=%p contains empty string', (values) => { expect(() => { glue.PartitionProjectionConfiguration.enum({ values }); - }).toThrow( 'ENUM partition projection values must not contain empty strings'); + }).toThrow('ENUM partition projection values must not contain empty strings'); }); }); }); From 26c3ed6740043d101db6b3f4e8fac660f7050452 Mon Sep 17 00:00:00 2001 From: Kazuho Cryer-Shinozuka Date: Sun, 18 Jan 2026 10:49:37 +0900 Subject: [PATCH 10/16] chore: add .gitignore and project configuration for Serena --- .../@aws-cdk/aws-glue-alpha/test/integ.partition-projection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.ts b/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.ts index ecce674509d40..d3143bcca7188 100644 --- a/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.ts +++ b/packages/@aws-cdk/aws-glue-alpha/test/integ.partition-projection.ts @@ -1,5 +1,5 @@ -import * as cdk from 'aws-cdk-lib'; import { IntegTest } from '@aws-cdk/integ-tests-alpha'; +import * as cdk from 'aws-cdk-lib'; import * as glue from '../lib'; const app = new cdk.App(); From 06ad88b1330be31e80c1077201e97e7b6613e027 Mon Sep 17 00:00:00 2001 From: Kazuho Cryer-Shinozuka Date: Wed, 4 Feb 2026 00:23:18 +0900 Subject: [PATCH 11/16] fix(glue): add validation for partition projection conflicts with manual parameters --- .../lib/partition-projection.ts | 78 +++++++++++-------- .../@aws-cdk/aws-glue-alpha/lib/table-base.ts | 19 +++++ .../aws-glue-alpha/test/table-base.test.ts | 60 ++++++++++++++ 3 files changed, 124 insertions(+), 33 deletions(-) diff --git a/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts b/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts index ce71e6c9240ce..d0c8e5f3bed67 100644 --- a/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts +++ b/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts @@ -1,4 +1,4 @@ -import { UnscopedValidationError } from 'aws-cdk-lib'; +import { Token, UnscopedValidationError } from 'aws-cdk-lib'; /** * Partition projection type. @@ -232,37 +232,43 @@ export class PartitionProjectionConfiguration { */ public static integer(props: IntegerPartitionProjectionConfigurationProps): PartitionProjectionConfiguration { // Validate min/max are integers - if (!Number.isInteger(props.min) || !Number.isInteger(props.max)) { - throw new UnscopedValidationError( - `INTEGER partition projection range must contain integers, but got [${props.min}, ${props.max}]`, - ); - } - - // Validate min <= max - if (props.min > props.max) { - throw new UnscopedValidationError( - `INTEGER partition projection range must be [min, max] where min <= max, but got [${props.min}, ${props.max}]`, - ); - } - - // Validate interval - if (props.interval !== undefined) { - if (!Number.isInteger(props.interval) || props.interval <= 0) { + if (!Token.isUnresolved(props.min) && !Token.isUnresolved(props.max)) { + if (!Number.isInteger(props.min) || !Number.isInteger(props.max)) { throw new UnscopedValidationError( - `INTEGER partition projection interval must be a positive integer, but got ${props.interval}`, + `INTEGER partition projection range must contain integers, but got [${props.min}, ${props.max}]`, ); } - } - // Validate digits - if (props.digits !== undefined) { - if (!Number.isInteger(props.digits) || props.digits < 1) { + // Validate min <= max + if (props.min > props.max) { throw new UnscopedValidationError( - `INTEGER partition projection digits must be an integer >= 1, but got ${props.digits}`, + `INTEGER partition projection range must be [min, max] where min <= max, but got [${props.min}, ${props.max}]`, ); } } + // Validate interval + if ( + props.interval !== undefined && + !Token.isUnresolved(props.interval) && + (!Number.isInteger(props.interval) || props.interval <= 0) + ) { + throw new UnscopedValidationError( + `INTEGER partition projection interval must be a positive integer, but got ${props.interval}`, + ); + } + + // Validate digits + if ( + props.digits !== undefined && + !Token.isUnresolved(props.digits) && + (!Number.isInteger(props.digits) || props.digits < 1) + ) { + throw new UnscopedValidationError( + `INTEGER partition projection digits must be an integer >= 1, but got ${props.digits}`, + ); + } + return new PartitionProjectionConfiguration({ type: PartitionProjectionType.INTEGER, integerRange: [props.min, props.max], @@ -276,26 +282,32 @@ export class PartitionProjectionConfiguration { */ public static date(props: DatePartitionProjectionConfigurationProps): PartitionProjectionConfiguration { // Validate min/max are not empty - if (props.min.trim() === '' || props.max.trim() === '') { + if ( + !Token.isUnresolved(props.min) && + !Token.isUnresolved(props.max) && + (props.min.trim() === '' || props.max.trim() === '') + ) { throw new UnscopedValidationError( 'DATE partition projection range must not contain empty strings', ); } // Validate format is not empty - if (props.format.trim() === '') { + if (!Token.isUnresolved(props.format) && props.format.trim() === '') { throw new UnscopedValidationError( 'DATE partition projection format must be a non-empty string', ); } // Validate interval - if (props.interval !== undefined) { - if (!Number.isInteger(props.interval) || props.interval <= 0) { - throw new UnscopedValidationError( - `DATE partition projection interval must be a positive integer, but got ${props.interval}`, - ); - } + if ( + props.interval !== undefined && + !Token.isUnresolved(props.interval) && + (!Number.isInteger(props.interval) || props.interval <= 0) + ) { + throw new UnscopedValidationError( + `DATE partition projection interval must be a positive integer, but got ${props.interval}`, + ); } return new PartitionProjectionConfiguration({ @@ -318,10 +330,10 @@ export class PartitionProjectionConfiguration { ); } - // Validate no empty strings + // Validate no empty strings (skip tokens) for (let i = 0; i < props.values.length; i++) { const value = props.values[i]; - if (value.trim() === '') { + if (!Token.isUnresolved(value) && value.trim() === '') { throw new UnscopedValidationError( 'ENUM partition projection values must not contain empty strings', ); diff --git a/packages/@aws-cdk/aws-glue-alpha/lib/table-base.ts b/packages/@aws-cdk/aws-glue-alpha/lib/table-base.ts index 5adc3f0ebff83..26ad562271a54 100644 --- a/packages/@aws-cdk/aws-glue-alpha/lib/table-base.ts +++ b/packages/@aws-cdk/aws-glue-alpha/lib/table-base.ts @@ -381,10 +381,29 @@ export abstract class TableBase extends Resource implements ITable { // Generate CloudFormation parameters const generatedParams = generatePartitionProjectionParameters(columnName, config); + // Check for conflicts with manually specified parameters + const conflictingKeys = Object.keys(generatedParams).filter(key => key in this.parameters); + if (conflictingKeys.length > 0) { + throw new ValidationError( + `Partition projection parameters conflict with manually specified parameters: ${conflictingKeys.join(', ')}. ` + + 'Use the partitionProjection property instead of manually specifying projection parameters.', + this, + ); + } + // Merge into this.parameters Object.assign(this.parameters, generatedParams); } + // Check for conflict with projection.enabled + if ('projection.enabled' in this.parameters) { + throw new ValidationError( + 'Parameter "projection.enabled" conflicts with partitionProjection configuration. ' + + 'Use the partitionProjection property instead of manually specifying projection.enabled.', + this, + ); + } + // Enable partition projection globally this.parameters['projection.enabled'] = 'true'; } diff --git a/packages/@aws-cdk/aws-glue-alpha/test/table-base.test.ts b/packages/@aws-cdk/aws-glue-alpha/test/table-base.test.ts index eaeb9fbb175b2..69c396adf6c01 100644 --- a/packages/@aws-cdk/aws-glue-alpha/test/table-base.test.ts +++ b/packages/@aws-cdk/aws-glue-alpha/test/table-base.test.ts @@ -782,6 +782,66 @@ describe('Partition Projection', () => { }, }); }); + + test('throws when partition projection conflicts with manual parameters', () => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const database = new glue.Database(stack, 'Database'); + + expect(() => { + new glue.S3Table(stack, 'Table', { + database, + columns: [{ + name: 'col1', + type: glue.Schema.STRING, + }], + partitionKeys: [{ + name: 'year', + type: glue.Schema.INTEGER, + }], + dataFormat: glue.DataFormat.JSON, + parameters: { + 'projection.year.type': 'integer', + }, + partitionProjection: { + year: glue.PartitionProjectionConfiguration.integer({ + min: 2020, + max: 2023, + }), + }, + }); + }).toThrow('Partition projection parameters conflict with manually specified parameters: projection.year.type. Use the partitionProjection property instead of manually specifying projection parameters.'); + }); + + test('throws when projection.enabled conflicts with partitionProjection', () => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const database = new glue.Database(stack, 'Database'); + + expect(() => { + new glue.S3Table(stack, 'Table', { + database, + columns: [{ + name: 'col1', + type: glue.Schema.STRING, + }], + partitionKeys: [{ + name: 'year', + type: glue.Schema.INTEGER, + }], + dataFormat: glue.DataFormat.JSON, + parameters: { + 'projection.enabled': 'true', + }, + partitionProjection: { + year: glue.PartitionProjectionConfiguration.integer({ + min: 2020, + max: 2023, + }), + }, + }); + }).toThrow('Parameter "projection.enabled" conflicts with partitionProjection configuration. Use the partitionProjection property instead of manually specifying projection.enabled.'); + }); }); test('storage descriptor parameters', () => { From 673818a0179afaa1944becdb671f1f72ce78f1bb Mon Sep 17 00:00:00 2001 From: Kazuho Cryer-Shinozuka Date: Wed, 4 Feb 2026 00:30:39 +0900 Subject: [PATCH 12/16] chore: add .gitignore and project.yml for Serena configuration --- packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts b/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts index d0c8e5f3bed67..19b9401ac916e 100644 --- a/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts +++ b/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts @@ -330,7 +330,7 @@ export class PartitionProjectionConfiguration { ); } - // Validate no empty strings (skip tokens) + // Validate no empty strings for (let i = 0; i < props.values.length; i++) { const value = props.values[i]; if (!Token.isUnresolved(value) && value.trim() === '') { From 52009e4ee80726a438f2168d349d699177296ad9 Mon Sep 17 00:00:00 2001 From: Kazuho Cryer-Shinozuka Date: Wed, 4 Feb 2026 22:14:11 +0900 Subject: [PATCH 13/16] fix(partition-projection): add validation for commas in ENUM partition projection values --- .../aws-glue-alpha/lib/partition-projection.ts | 16 +++++++++++----- .../test/partition-projection.test.ts | 9 +++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts b/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts index 19b9401ac916e..27058264144c5 100644 --- a/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts +++ b/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts @@ -330,13 +330,19 @@ export class PartitionProjectionConfiguration { ); } - // Validate no empty strings for (let i = 0; i < props.values.length; i++) { const value = props.values[i]; - if (!Token.isUnresolved(value) && value.trim() === '') { - throw new UnscopedValidationError( - 'ENUM partition projection values must not contain empty strings', - ); + if (!Token.isUnresolved(value)) { + if (value.trim() === '') { + throw new UnscopedValidationError( + 'ENUM partition projection values must not contain empty strings', + ); + } + if (value.includes(',')) { + throw new UnscopedValidationError( + `ENUM partition projection values must not contain commas because the values are serialized as a comma-separated list, got: '${value}'`, + ); + } } } diff --git a/packages/@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts b/packages/@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts index 1a1b7efcf88a1..216956f0b9835 100644 --- a/packages/@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts +++ b/packages/@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts @@ -91,5 +91,14 @@ describe('PartitionProjectionConfiguration Validation', () => { glue.PartitionProjectionConfiguration.enum({ values }); }).toThrow('ENUM partition projection values must not contain empty strings'); }); + + test.each([ + [['value,with,commas', 'normal'], 0], + [['normal', 'also,bad'], 1], + ])('throws when values=%p contains comma at index %p', (values, index) => { + expect(() => { + glue.PartitionProjectionConfiguration.enum({ values }); + }).toThrow(`ENUM partition projection values must not contain commas because the values are serialized as a comma-separated list, got: '${values[index]}'`); + }); }); }); From 9ce53265ae847d9dd201a9be98168b9931119520 Mon Sep 17 00:00:00 2001 From: Kazuho Cryer-Shinozuka Date: Wed, 4 Feb 2026 23:24:02 +0900 Subject: [PATCH 14/16] fix(partition-projection): enhance validation for DATE format in partition projection --- .../lib/partition-projection.ts | 48 +++++++++++++++++-- .../test/partition-projection.test.ts | 38 +++++++++++++++ 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts b/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts index 27058264144c5..5181f6bf39546 100644 --- a/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts +++ b/packages/@aws-cdk/aws-glue-alpha/lib/partition-projection.ts @@ -292,11 +292,49 @@ export class PartitionProjectionConfiguration { ); } - // Validate format is not empty - if (!Token.isUnresolved(props.format) && props.format.trim() === '') { - throw new UnscopedValidationError( - 'DATE partition projection format must be a non-empty string', - ); + // Validate format + if (!Token.isUnresolved(props.format)) { + // Validate format is not empty + if (props.format.trim() === '') { + throw new UnscopedValidationError( + 'DATE partition projection format must be a non-empty string', + ); + } + + // Validate format pattern characters (Java 8 DateTimeFormatter) + const validPatternLetters = 'GyuYDMLdQqwWEecFahKkHmsSAnNVzOXxZp'; + const format = props.format; + let inQuote = false; + const invalidChars: string[] = []; + + for (let i = 0; i < format.length; i++) { + const ch = format[i]; + if (ch === "'") { + if (i + 1 < format.length && format[i + 1] === "'") { + // '' is an escaped single quote literal, skip both + i++; + } else { + inQuote = !inQuote; + } + } else if (!inQuote && /[a-zA-Z]/.test(ch)) { + if (!validPatternLetters.includes(ch)) { + invalidChars.push(ch); + } + } + } + + if (inQuote) { + throw new UnscopedValidationError( + `DATE partition projection format has an unclosed single quote: '${format}'`, + ); + } + + if (invalidChars.length > 0) { + const unique = [...new Set(invalidChars)]; + throw new UnscopedValidationError( + `DATE partition projection format contains invalid pattern characters: ${unique.join(', ')}. Must use Java DateTimeFormatter valid pattern letters.`, + ); + } } // Validate interval diff --git a/packages/@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts b/packages/@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts index 216956f0b9835..d6af68a4a1a6f 100644 --- a/packages/@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts +++ b/packages/@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts @@ -62,6 +62,44 @@ describe('PartitionProjectionConfiguration Validation', () => { }).toThrow('DATE partition projection format must be a non-empty string'); }); + test.each([ + 'yyyy-MM-dd', + 'yyyy/MM/dd/HH', + "yyyyMMdd'T'HHmmss", + "yyyy-MM-dd''HH", + ])('accepts valid format=%p', (format) => { + expect(() => { + glue.PartitionProjectionConfiguration.date({ + min: '2020-01-01', + max: '2023-12-31', + format, + }); + }).not.toThrow(); + }); + + test.each([ + ['yyyy-bb-dd', ['b']], + ['yyyy-MM-ddJ', ['J']], + ])('throws when format=%p contains invalid characters %p', (format, invalidChars) => { + expect(() => { + glue.PartitionProjectionConfiguration.date({ + min: '2020-01-01', + max: '2023-12-31', + format, + }); + }).toThrow(`DATE partition projection format contains invalid pattern characters: ${invalidChars.join(', ')}. Must use Java DateTimeFormatter valid pattern letters.`); + }); + + test("throws when format has unclosed single quote", () => { + expect(() => { + glue.PartitionProjectionConfiguration.date({ + min: '2020-01-01', + max: '2023-12-31', + format: "yyyy-MM-dd'T", + }); + }).toThrow("DATE partition projection format has an unclosed single quote: 'yyyy-MM-dd'T'"); + }); + test.each([0, -1, 1.5])('throws when interval=%p is invalid', (interval) => { expect(() => { glue.PartitionProjectionConfiguration.date({ From b83259d22b5c6bc1ddbef9282a34497364535bb4 Mon Sep 17 00:00:00 2001 From: Kazuho Cryer-Shinozuka Date: Wed, 4 Feb 2026 23:34:35 +0900 Subject: [PATCH 15/16] fix(table-base): update import statements to use type imports for better clarity --- packages/@aws-cdk/aws-glue-alpha/lib/table-base.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-glue-alpha/lib/table-base.ts b/packages/@aws-cdk/aws-glue-alpha/lib/table-base.ts index 26ad562271a54..3fd84f1b2ad6d 100644 --- a/packages/@aws-cdk/aws-glue-alpha/lib/table-base.ts +++ b/packages/@aws-cdk/aws-glue-alpha/lib/table-base.ts @@ -3,13 +3,13 @@ import * as iam from 'aws-cdk-lib/aws-iam'; import type { IResource } from 'aws-cdk-lib/core'; import { ArnFormat, Fn, Lazy, Names, Resource, Stack, UnscopedValidationError, ValidationError } from 'aws-cdk-lib/core'; import * as cr from 'aws-cdk-lib/custom-resources'; -import { AwsCustomResource } from 'aws-cdk-lib/custom-resources'; -import { Construct } from 'constructs'; -import { DataFormat } from './data-format'; -import { IDatabase } from './database'; -import { generatePartitionProjectionParameters, PartitionProjection } from './partition-projection'; -import { Column } from './schema'; -import { StorageParameter } from './storage-parameter'; +import type { AwsCustomResource } from 'aws-cdk-lib/custom-resources'; +import type { Construct } from 'constructs'; +import type { DataFormat } from './data-format'; +import type { IDatabase } from './database'; +import { generatePartitionProjectionParameters, type PartitionProjection } from './partition-projection'; +import type { Column } from './schema'; +import type { StorageParameter } from './storage-parameter'; /** * Properties of a Partition Index. From c576d77de201939455ebf8a9d2e83ee60c5c9ba8 Mon Sep 17 00:00:00 2001 From: Kazuho Cryer-Shinozuka Date: Fri, 6 Feb 2026 00:20:06 +0900 Subject: [PATCH 16/16] fix(partition-projection): standardize quote style in DATE validation test --- .../@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts b/packages/@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts index d6af68a4a1a6f..148f67c5d49fb 100644 --- a/packages/@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts +++ b/packages/@aws-cdk/aws-glue-alpha/test/partition-projection.test.ts @@ -90,7 +90,7 @@ describe('PartitionProjectionConfiguration Validation', () => { }).toThrow(`DATE partition projection format contains invalid pattern characters: ${invalidChars.join(', ')}. Must use Java DateTimeFormatter valid pattern letters.`); }); - test("throws when format has unclosed single quote", () => { + test('throws when format has unclosed single quote', () => { expect(() => { glue.PartitionProjectionConfiguration.date({ min: '2020-01-01',