fix(memory-driver-duckdb): map rows, un-exported bundles for CDN uses vector (vss) check

This commit is contained in:
Neko Ayaka
2025-02-10 01:07:00 +07:00
parent 0703aad8ff
commit 609173cdbe
15 changed files with 361 additions and 75 deletions
+3
View File
@@ -10,6 +10,7 @@ words:
- Attributify
- audioworklet
- Ayaka
- bigserial
- Bitstream
- bumpp
- catppuccin
@@ -97,8 +98,10 @@ words:
- shikijs
- silero
- sizecheck
- smallserial
- Sniglet
- supergroup
- superjson
- tamagotchi
- taze
- tresjs
+13
View File
@@ -11,5 +11,18 @@ export default antfu(
'packages/stage-tamagotchi/out/**',
'**/drizzle/**',
],
// rules: {
// 'import/order': [
// 'error',
// {
// 'groups': [
// ['type'],
// ['builtin', 'external'],
// ['parent', 'sibling', 'index'],
// ],
// 'newlines-between': 'always',
// },
// ],
// },
},
)
+2 -1
View File
@@ -57,7 +57,8 @@
"date-fns": "^4.1.0",
"defu": "^6.1.4",
"drizzle-orm": "^0.39.2",
"es-toolkit": "^1.32.0"
"es-toolkit": "^1.32.0",
"superjson": "^2.2.2"
},
"devDependencies": {
"@unocss/reset": "^65.4.3",
@@ -1,8 +1,47 @@
import { sql } from 'drizzle-orm'
import { pgTable, uuid } from 'drizzle-orm/pg-core'
import {
bigint,
// bit,
boolean,
char,
date,
decimal,
doublePrecision,
integer,
interval,
json,
numeric,
pgTable,
real,
smallint,
text,
time,
timestamp,
uuid,
varchar,
vector,
} from 'drizzle-orm/pg-core'
export const users = pgTable('users', () => {
return {
id: uuid().primaryKey().unique().default(sql`gen_random_uuid()`),
}
})
export const users = pgTable('users', () => ({
id: uuid().primaryKey().unique().default(sql`gen_random_uuid()`),
int16: smallint().notNull().default(0),
int32: integer().notNull().default(0),
int64: bigint({ mode: 'number' }).notNull().default(sql`0`),
int64BigInt: bigint({ mode: 'bigint' }).notNull().default(sql`0`),
decimal: decimal().notNull().default(sql`0`),
numeric: numeric().notNull().default(sql`0`),
real: real().notNull().default(sql`0`),
double: doublePrecision().notNull().default(sql`0`),
vector: vector({ dimensions: 4 }).notNull().default(sql`[0,0,0,0]`),
// Unimplemented type for cast (INTEGER[] -> BIT) from @duckdb/duckdb-wasm
// bit: bit({ dimensions: 4 }).notNull().default(sql`[0,0,0,0]`),
bool: boolean().notNull().default(false),
char: char().notNull().default(''),
varchar: varchar().notNull().default(''),
text: text().notNull().default(''),
json: json().notNull().default(sql`'{}'`),
date: date().notNull().default(sql`'2020-01-01'`),
time: time().notNull().default(sql`'00:00:00'`),
timestamp: timestamp().notNull().default(sql`'2020-01-01 00:00:00'`),
interval: interval().notNull().default(sql`'1 day'`),
}))
@@ -0,0 +1,22 @@
CREATE TABLE "users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"int16" smallint DEFAULT 0 NOT NULL,
"int32" integer DEFAULT 0 NOT NULL,
"int64" bigint DEFAULT 0 NOT NULL,
"int64BigInt" bigint DEFAULT 0 NOT NULL,
"decimal" numeric DEFAULT 0 NOT NULL,
"numeric" numeric DEFAULT 0 NOT NULL,
"real" real DEFAULT 0 NOT NULL,
"double" double precision DEFAULT 0 NOT NULL,
"vector" FLOAT[4] DEFAULT [0,0,0,0] NOT NULL,
"bool" boolean DEFAULT false NOT NULL,
"char" char DEFAULT '' NOT NULL,
"varchar" varchar DEFAULT '' NOT NULL,
"text" text DEFAULT '' NOT NULL,
"json" json DEFAULT '{}' NOT NULL,
"date" date DEFAULT '2020-01-01' NOT NULL,
"time" time DEFAULT '00:00:00' NOT NULL,
"timestamp" timestamp DEFAULT '2020-01-01 00:00:00' NOT NULL,
"interval" interval DEFAULT '0 day' NOT NULL,
CONSTRAINT "users_id_unique" UNIQUE("id")
);
@@ -1,4 +0,0 @@
CREATE TABLE "users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
CONSTRAINT "users_id_unique" UNIQUE("id")
);
@@ -1,5 +1,5 @@
{
"id": "b731cb60-cb64-4cb0-8262-52b1c3fc467e",
"id": "5305094b-45db-4a61-a74f-340b2b7da250",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
@@ -14,6 +14,132 @@
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"int16": {
"name": "int16",
"type": "smallint",
"primaryKey": false,
"notNull": true,
"default": 0
},
"int32": {
"name": "int32",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"int64": {
"name": "int64",
"type": "bigint",
"primaryKey": false,
"notNull": true,
"default": "0"
},
"int64BigInt": {
"name": "int64BigInt",
"type": "bigint",
"primaryKey": false,
"notNull": true,
"default": "0"
},
"decimal": {
"name": "decimal",
"type": "numeric",
"primaryKey": false,
"notNull": true,
"default": "0"
},
"numeric": {
"name": "numeric",
"type": "numeric",
"primaryKey": false,
"notNull": true,
"default": "0"
},
"real": {
"name": "real",
"type": "real",
"primaryKey": false,
"notNull": true,
"default": "0"
},
"double": {
"name": "double",
"type": "double precision",
"primaryKey": false,
"notNull": true,
"default": "0"
},
"vector": {
"name": "vector",
"type": "vector(4)",
"primaryKey": false,
"notNull": true,
"default": "[0,0,0,0]"
},
"bool": {
"name": "bool",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"char": {
"name": "char",
"type": "char",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"varchar": {
"name": "varchar",
"type": "varchar",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"text": {
"name": "text",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"json": {
"name": "json",
"type": "json",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"date": {
"name": "date",
"type": "date",
"primaryKey": false,
"notNull": true,
"default": "'2020-01-01'"
},
"time": {
"name": "time",
"type": "time",
"primaryKey": false,
"notNull": true,
"default": "'00:00:00'"
},
"timestamp": {
"name": "timestamp",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "'2020-01-01 00:00:00'"
},
"interval": {
"name": "interval",
"type": "interval",
"primaryKey": false,
"notNull": true,
"default": "'0'"
}
},
"indexes": {},
@@ -5,8 +5,8 @@
{
"idx": 0,
"version": "7",
"when": 1739072522344,
"tag": "0000_legal_gauntlet",
"when": 1739121102268,
"tag": "0000_cute_kulan_gath",
"breakpoints": true
}
]
@@ -1,10 +1,14 @@
<script setup lang="ts">
import type { DuckDBWasmDrizzleDatabase } from '../../src/drizzle-orm'
import { useDebounceFn } from '@vueuse/core'
import { serialize } from 'superjson'
import { onMounted, onUnmounted, ref, watch } from 'vue'
import { drizzle } from '../../src/drizzle-orm'
import * as schema from '../db/schema'
import { users } from '../db/schema'
import migration from '../drizzle/0000_legal_gauntlet.sql?raw'
import migration1 from '../drizzle/0000_cute_kulan_gath.sql?raw'
const db = ref<DuckDBWasmDrizzleDatabase<typeof schema>>()
const results = ref<Record<string, unknown>[]>()
@@ -12,21 +16,34 @@ const schemaResults = ref<Record<string, unknown>[]>()
const query = ref(`SELECT 1 + 1 AS result`)
onMounted(async () => {
db.value = drizzle('duckdb-wasm://?bundles=worker-url', { schema })
await db.value?.execute(migration)
db.value = drizzle('duckdb-wasm://?bundles=import-url', { schema })
await db.value?.execute('INSTALL vss;')
await db.value?.execute('LOAD vss;')
await db.value?.execute(migration1)
results.value = await db.value?.execute(query.value)
await db.value.insert(users).values({ id: '9449af72-faad-4c97-8a45-69f9f1ca1b05' })
schemaResults.value = await db.value.select().from(users)
await db.value.insert(users).values({
id: '9449af72-faad-4c97-8a45-69f9f1ca1b05',
decimal: '1.23456',
numeric: '1.23456',
real: 1.23456,
double: 1.23456,
interval: '365 day',
})
const usersResults = await db.value.select().from(users)
schemaResults.value = usersResults
})
onUnmounted(() => {
db.value?.$client.then(client => client.close())
})
watch(query, async () => {
watch(query, useDebounceFn(async () => {
results.value = await db.value?.execute(query.value)
})
}, 1000))
</script>
<template>
@@ -39,7 +56,7 @@ watch(query, async () => {
Executing
</h2>
<div>
<textarea v-model="query" h-full w-full rounded-lg bg="gray-100 dark:gray-800" p-4 font-mono />
<textarea v-model="query" h-full w-full rounded-lg bg="neutral-100 dark:neutral-800" p-4 font-mono />
</div>
</div>
<div flex flex-col gap-2>
@@ -47,7 +64,7 @@ watch(query, async () => {
Results
</h2>
<div whitespace-pre-wrap p-4 font-mono>
{{ JSON.stringify(results, null, 2) }}
{{ JSON.stringify(serialize(results).json, null, 2) }}
</div>
</div>
<div flex flex-col gap-2>
@@ -55,7 +72,7 @@ watch(query, async () => {
Executing
</h2>
<div>
<pre whitespace-pre-wrap p-4 font-mono bg="gray-100 dark:gray-800">
<pre whitespace-pre-wrap rounded-lg p-4 font-mono bg="neutral-100 dark:neutral-800">
await db.insert(users).values({ id: '9449af72-faad-4c97-8a45-69f9f1ca1b05' })
await db.select().from(users)
</pre>
@@ -66,7 +83,7 @@ await db.select().from(users)
Schema Results
</h2>
<div whitespace-pre-wrap p-4 font-mono>
{{ JSON.stringify(schemaResults, null, 2) }}
{{ JSON.stringify(serialize(schemaResults).json, null, 2) }}
</div>
</div>
</div>
@@ -1,7 +1,9 @@
import type { TZDate } from '@date-fns/tz'
import type { Field, StructRow } from 'apache-arrow'
import type { DataType } from './duckdb-types'
import { TZDate } from '@date-fns/tz'
import { Struct, TimeUnit, util } from 'apache-arrow'
import { TZDateMini } from '@date-fns/tz'
import { DataType as ArrowDataType, IntervalUnit, Struct, TimeUnit, util } from 'apache-arrow'
import {
addDays,
addHours,
@@ -16,17 +18,19 @@ import {
formatDuration as dateFormatDuration,
fromUnixTime,
setDay,
transpose,
} from 'date-fns'
import { trimEnd } from 'es-toolkit'
import { isNullOrUndefined, notNullOrUndefined } from './duckdb-common'
import {
isBooleanType,
isDatetimeType,
isDateType,
isDecimalType,
isDurationType,
isFloatType,
isIntegerType,
isIntervalType,
isListType,
isObjectType,
@@ -284,7 +288,7 @@ export function formatDate(date: number | Date): string {
/**
* Format datetime value from Arrow to string.
*/
export function formatDatetime(date: number | Date, field?: Field): string {
export function formatDatetime(date: number | Date, field?: Field): Date | null {
// Datetime values from arrow are already converted to a date object
// or a timestamp in milliseconds even if the field unit might indicate a
// different unit.
@@ -297,25 +301,34 @@ export function formatDatetime(date: number | Date, field?: Field): string {
)
) {
console.warn(`Unsupported datetime value: ${date}`)
return String(date)
return null
}
let datetime: TZDate
const timezone = field?.type?.timezone
// Impact by upstream changes, see:
// - How to replace date-fns-tz · Issue #9 - https://github.com/date-fns/tz/issues/9
// - Inconsistencies from date-fns-tz · Issue #6 - https://github.com/date-fns/tz/issues/6
if (typeof date === 'number') {
datetime = new TZDate(date, 'UTC')
if (timezone) {
datetime = new TZDateMini(date, timezone)
}
else {
datetime = new TZDateMini(date)
}
}
else {
datetime = new TZDate(date, 'UTC')
if (timezone) {
datetime = new TZDateMini(date, timezone)
}
else {
datetime = new TZDateMini(date)
}
}
const timezone = field?.type?.timezone
if (timezone) {
datetime = datetime.withTimeZone(timezone)
return dateFormat(datetime, 'yyyy-MM-dd HH:mm:ss OOOO')
}
// Return the timestamp without timezone information
return dateFormat(datetime, 'yyyy-MM-dd HH:mm:ss')
return transpose(datetime, Date)
}
/**
@@ -329,7 +342,7 @@ export function formatDatetime(date: number | Date, field?: Field): string {
* https://github.com/apache/arrow/issues/28804
* https://github.com/apache/arrow/issues/35745
*/
export function formatDecimal(value: Uint32Array, field?: Field): string {
export function formatDecimal(value: Uint32Array | Int32Array, field?: Field): string {
const scale = field?.type?.scale || 0
// Format Uint32Array to a numerical string and pad it with zeros
@@ -382,7 +395,30 @@ export function formatFloat(num: number): string {
/**
* Formats an interval value from arrow to string.
*/
export function formatInterval(x: StructRow, field?: Field): string {
export function formatInterval(x: StructRow | Int32Array, field?: Field): string {
// TODO: still buggy, the return value of interval related fields are always
// [0, 0] as Int32Array, should follow the issue to resolve this.
if (ArrowDataType.isInterval(field?.type)) {
const value = formatDecimal(x as Int32Array, field)
const unit = field?.type.unit
switch (unit) {
case IntervalUnit.MONTH_DAY_NANO:
// In Python:
// pa.scalar((1, 15, -30), type=pa.month_day_nano_interval())
// <pyarrow.MonthDayNanoIntervalScalar: MonthDayNano(months=1, days=15, nanoseconds=-30)>
//
// see: pyarrow.month_day_nano_interval — Apache Arrow v19.0.0
// https://arrow.apache.org/docs/python/generated/pyarrow.month_day_nano_interval.html
return `${value} months`
case IntervalUnit.DAY_TIME:
return `${value} days`
case IntervalUnit.YEAR_MONTH:
return `${value} years`
default:
return value
}
}
// Serialization for pandas.Interval is provided by Arrow extensions
// https://github.com/pandas-dev/pandas/blob/235d9009b571c21b353ab215e1e675b1924ae55c/
// pandas/core/arrays/arrow/extension_types.py#L17
@@ -401,8 +437,8 @@ export function formatInterval(x: StructRow, field?: Field): string {
const leftBracket = closed === 'both' || closed === 'left' ? '[' : '('
const rightBracket = closed === 'both' || closed === 'right' ? ']' : ')'
const leftInterval = format(interval.left, (field.type as Struct)?.children?.[0])
const rightInterval = format(interval.right, (field.type as Struct)?.children?.[1])
const leftInterval = mapColumnData(interval.left, (field.type as Struct)?.children?.[0])
const rightInterval = mapColumnData(interval.right, (field.type as Struct)?.children?.[1])
return `${leftBracket + leftInterval}, ${rightInterval + rightBracket}`
}
@@ -468,31 +504,36 @@ export function formatPeriod(duration: number | bigint, field?: Field): string {
* @param field The field metadata from arrow containing metadata about the column.
* @returns The formatted JSON string.
*/
export function formatObject(object: any, field?: Field): string {
export function formatObject(object: any, field?: Field): unknown {
if (field?.type instanceof Struct) {
// This type is used by python dictionary values
return JSON.stringify(object, (_key, value) => {
if (!notNullOrUndefined(value)) {
return JSON.parse(
JSON.stringify(object, (_key, value) => {
if (!notNullOrUndefined(value)) {
// Workaround: Arrow JS adds all properties from all cells
// as fields. When you convert to string, it will contain lots of fields with
// null values. To mitigate this, we filter out null values.
return undefined
}
if (typeof value === 'bigint') {
return undefined
}
if (typeof value === 'bigint') {
// JSON.stringify fails to serialize bigint values, therefore we have to
// handle them manually.
// TODO(lukasmasuch): Would it be better to serialize it to a string to
// not lose precision?
return Number(value)
}
return value
})
return Number(value)
}
return value
}),
)
}
// TODO(lukasmasuch): Investigate if we can unify this with the logic above.
return JSON.stringify(object, (_key, value) =>
typeof value === 'bigint' ? Number(value) : value)
return JSON.parse(
JSON.stringify(object, (_key, value) =>
typeof value === 'bigint' ? Number(value) : value),
)
}
/**
@@ -506,47 +547,61 @@ export function formatObject(object: any, field?: Field): string {
* @param field The field metadata from arrow containing metadata about the column.
* @returns The formatted cell value.
*/
export function format(x: DataType, field?: Field): string {
export function mapColumnData<T = unknown>(x: DataType, field?: Field): T {
if (isNullOrUndefined(x)) {
return ''
return null as null as unknown as T
}
const isDate = x instanceof Date || Number.isFinite(x)
if (isDate && isDateType(field)) {
return formatDate(x as Date | number)
return formatDate(x as Date | number) as string as T
}
if (typeof x === 'bigint' && isTimeType(field)) {
return formatTime(Number(x), field)
return formatTime(Number(x), field) as string as T
}
if (isDate && isDatetimeType(field)) {
return formatDatetime(x as Date | number, field)
return formatDatetime(x as Date | number, field) as Date as T
}
if (isPeriodType(field)) {
return formatPeriod(x as bigint, field)
// Not supported yet by Postgres and DuckDB
throw new Error('Period type is not supported yet')
}
if (isIntervalType(field)) {
return formatInterval(x as StructRow, field)
return formatInterval(x as StructRow, field) as string as T
}
if (isDurationType(field)) {
return formatDuration(x as number | bigint, field)
// Not supported yet by Postgres and DuckDB
throw new Error('Duration type is not supported yet')
}
if (isDecimalType(field)) {
return formatDecimal(x as Uint32Array, field)
// 'numeric' in Postgres also applies here
return formatDecimal(x as Uint32Array, field) as string as T
}
if (isFloatType(field) && Number.isFinite(x)) {
return formatFloat(x as number)
return x as number as T
}
if (isIntegerType(field)) {
// If int64 or uint64, it should be bigint already,
// if int32 or uint32, it should be number already,
// therefore we can just return the value as is.
return x as number as T
}
if (isObjectType(field) || isListType(field)) {
return formatObject(x, field)
return formatObject(x, field) as T
}
return String(x)
if (isBooleanType(field)) {
return Boolean(x) as boolean as T
}
return String(x) as T
}
@@ -239,7 +239,8 @@ export function isIntervalType(type?: Field): boolean {
// ArrowDataType.isInterval checks for a different (unsupported) type and not related
// to the pandas interval extension type.
return (
(ArrowDataType.isStruct(type.type)
&& type.metadata.get('ARROW:extension:name') === 'interval')
((ArrowDataType.isStruct(type.type)
&& type.metadata.get('ARROW:extension:name') === 'interval'))
|| ArrowDataType.isInterval(type.type)
)
}
@@ -2,6 +2,7 @@ import type { DuckDBBundles, Logger } from '@duckdb/duckdb-wasm'
import type { DrizzleConfig, RelationalSchemaConfig, TablesRelationalConfig } from 'drizzle-orm'
import type { DuckDBWasmClient } from './dialect'
import type { DuckDBWasmQueryResultHKT } from './session'
import { ConsoleLogger } from '@duckdb/duckdb-wasm'
import { createTableRelationsHelpers, DefaultLogger, entityKind, extractTablesRelationalConfig, isConfig } from 'drizzle-orm'
import { PgDatabase, PgDialect } from 'drizzle-orm/pg-core'
@@ -68,11 +69,12 @@ export function drizzle<
): DuckDBWasmDrizzleDatabase<TSchema, TClient> {
if (typeof params[0] === 'string') {
const parsedDSN = new URL(params[0] as string)
if (parsedDSN.searchParams.get('bundles') === 'worker-url') {
if (parsedDSN.searchParams.get('bundles') === 'import-url') {
const logger = parsedDSN.searchParams.get('logger') === 'true' ? new ConsoleLogger() : undefined
return construct(new Promise<DuckDBWasmClient>((resolve) => {
import('./dialect/duckdb-vite-bundles')
.then(res => res.getViteBundles())
.then(bundles => connect({ bundles }))
.then(bundles => connect({ bundles, logger }))
.then(resolve)
}), params[1]) as any
}
@@ -100,7 +102,7 @@ export function drizzle<
if (typeof connection === 'object' && connection.url !== undefined) {
const { url } = connection
const parsedDSN = new URL(url)
if (parsedDSN.searchParams.get('bundles') === 'worker-url') {
if (parsedDSN.searchParams.get('bundles') === 'import-url') {
return construct(new Promise<DuckDBWasmClient>((resolve) => {
import('./dialect/duckdb-vite-bundles')
.then(res => res.getViteBundles())
@@ -1,7 +1,7 @@
export type { ConnectOptionalOptions, ConnectOptions, ConnectRequiredOptions, DuckDBWasmClient } from './dialect'
export { connect, format, getBundles } from './dialect'
export { connect, getBundles, mapColumnData } from './dialect'
export * from './driver'
export * from './migrator'
export * from './session'
export type { AsyncDuckDBConnection, DuckDBBundles, Logger } from '@duckdb/duckdb-wasm'
export { AsyncDuckDB, ConsoleLogger, selectBundle, VoidLogger } from '@duckdb/duckdb-wasm'
export { AsyncDuckDB, ConsoleLogger, getJsDelivrBundles, selectBundle, VoidLogger } from '@duckdb/duckdb-wasm'
@@ -5,7 +5,7 @@ import type { DuckDBWasmClient } from './dialect'
import { entityKind, fillPlaceholders, NoopLogger } from 'drizzle-orm'
import { PgPreparedQuery, PgSession, PgTransaction } from 'drizzle-orm/pg-core'
import { beginTransaction, format, withSavepoint } from './dialect'
import { beginTransaction, mapColumnData, withSavepoint } from './dialect'
export type Row = Record<string, any>
@@ -16,7 +16,7 @@ function toJSRepresentedRows<T extends { toArray: () => StructRow[], schema: Sch
const jsRepresentedRows = rows.map((row) => {
results.schema.fields.forEach((field) => {
return row[field.name] = format(row[field.name], field)
return row[field.name] = mapColumnData(row[field.name], field)
})
return row
+11
View File
@@ -155,6 +155,9 @@ importers:
es-toolkit:
specifier: ^1.32.0
version: 1.32.0
superjson:
specifier: ^2.2.2
version: 2.2.2
devDependencies:
'@unocss/reset':
specifier: ^65.4.3
@@ -9281,6 +9284,10 @@ packages:
resolution: {integrity: sha512-8iGv75BYOa0xRJHK5vRLEjE2H/i4lulTjzpUXic3Eg8akftYjkmQDa8JARQ42rlczXyFR3IeRoeFCc7RxHsYZA==}
engines: {node: '>=16'}
superjson@2.2.2:
resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==}
engines: {node: '>=16'}
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
@@ -20435,6 +20442,10 @@ snapshots:
dependencies:
copy-anything: 3.0.5
superjson@2.2.2:
dependencies:
copy-anything: 3.0.5
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0