DolphinDB Data Import Guide
Use this skill when the user asks about importing, loading, ingesting, or migrating data into DolphinDB, including CSV/text files, JSON, binary files, external databases, Parquet/HDF5 files, or IoT real-time data streams.
Import Method Decision Tree
- CSV / text file → memory table: use
loadText(single-thread) orploadText(parallel, faster for files > 16 MB). - CSV / text file → distributed table: use
loadTextEx(recommended, direct-to-disk, avoids OOM for large files). - Very large CSV file (> memory): use
textChunkDS+mrto chunk and import. - JSON string: use
fromStdJson(standard JSON) orparseJsonTable(JSON to table). - Binary file with string columns: use
loadRecord(filename, schema). Schema must be provided by user. - Binary file without string columns: use
readRecord!(handle, holder). Preferred for pure numeric files — simpler and safer. - Parquet file → memory table: use
parquet::loadParquet(filePath, [schema], [columnsToLoad], [startRowGroup], [rowGroupNum]). Supports partial column loading viacolumnsToLoadand row group selection viastartRowGroup/rowGroupNum. - Parquet file → distributed table: use
parquet::loadParquetEx(dbHandle, tableName, partitionColumns, fileName, [schema], [columnsToLoad], [startRowGroup], [rowGroupNum], [transform]). Note: nosortColumnsparameter; if sortColumns is needed, useparquet::loadParquet+replaceColumn!+append!. - HDF5 file → memory table: use
hdf5::loadHDF5(fileName, datasetName, [schema], [startRow], [rowNum]). Supports row range selection viastartRowandrowNum. - HDF5 file → distributed table: use
hdf5::loadHDF5Ex(dbHandle, tableName, [partitionColumns], fileName, datasetName, [schema], [startRow], [rowNum], [transform]). Note: nosortColumnsparameter; if sortColumns is needed, usehdf5::loadHDF5+replaceColumn!+pt.append!(data). - External database (MySQL, ODBC, etc.): use the corresponding plugin.
- IoT real-time data (MQTT): use MQTT plugin to subscribe to broker topics and write to stream tables.
- IoT real-time data (OPC UA): use OPC UA plugin to subscribe to server nodes and write to stream tables.
- IoT point management: use IOTDB engine with IOTANY column and latestKeyCache for millisecond-level latest-value queries.
Quick Reference: Core Import Functions
| Function | Target | Key Feature |
|---|---|---|
loadText |
Memory table | Single-thread; schema, skipRows, delimiter, partial columns via schema.col |
ploadText |
Memory table | Parallel; faster for > 16 MB |
loadTextEx |
Database table | Direct-to-disk; transform, sortColumns, skipRows |
textChunkDS |
Chunks for mr | Splits large file; use with mr |
extractTextSchema |
Schema preview | Auto-detect column types and delimiter |
loadRecord |
Memory table | Binary import with string support; schema required |
readRecord! |
Memory table | Binary import, no string support; holder required |
parquet::loadParquet |
Memory table | columnsToLoad, startRowGroup, rowGroupNum |
parquet::loadParquetEx |
Database table | Direct-to-disk; columnsToLoad, startRowGroup, rowGroupNum, transform (no sortColumns) |
hdf5::loadHDF5 |
Memory table | Schema override; startRow, rowNum |
hdf5::loadHDF5Ex |
Database table | Direct-to-disk; startRow, rowNum, transform (no sortColumns) |
Critical Rules
- loadText/loadTextEx do NOT support rowNum parameter (⚠️ CRITICAL — #1 cause of wasted React rounds):
loadTextandloadTextExdo NOT have arowNumparameter. To preview data, useselect top N * from loadText(...)or load first thenselect top N. NEVER attempt to passrowNumto these functions — it will cause an error and waste a React round. - Delimiter auto-detection: When
extractTextSchemareturns all columns merged into a single STRING column, it means the delimiter was not correctly detected. Automatically try these delimiters in order: (1) Tab:char(9)(most common for TXT files), (2) Semicolon:;, (3) Pipe:|. Re-runextractTextSchema(filename, delimiter=char(9))etc. until columns are properly separated. - skipRows semantics:
skipRows=Nskips the first N lines of the file, INCLUDING the header row. When usingskipRows > 0, you MUST provide a schema parameter (fromextractTextSchema) because the header row will be skipped. If the user wants to skip N data rows but keep the header, do NOT useskipRows— instead, load all data withloadText, then useselect * from t where rowNo(t) >= Nor filter after import.skipRowsdoes NOT count the header row separately — it counts from line 1 of the file. - Array vector import: When a CSV column contains delimited values (e.g., "1.0,2.0,3.0"), it should be imported as an array vector. Steps: (1)
extractTextSchema→ (2) modify schema type toDOUBLE[](orINT[]etc.) → (3) useloadText(path, schema=schema, arrayDelimiter=","). ThearrayDelimiterparameter tells DolphinDB how to split the values within each cell. - temporalParse with insufficient digit count: When converting INT time values (e.g., 93000000) to TIME/TIMESTAMP using
temporalParse, the string representation must match the format pattern length exactly. If the INT value has fewer digits than the format pattern requires, uselpad(string(col), N, "0")to pad with leading zeros.lpadis safe: it only pads strings shorter than N, and leaves strings already at length N or longer unchanged. Example: INT93000000(8 digits) →lpad(string(93000000), 9, "0")→"093000000"→temporalParse("093000000", "HHmmssSSS")→ 09:30:00.000. INT100000000(9 digits) →lpad(string(100000000), 9, "0")→"100000000"(unchanged) →temporalParse("100000000", "HHmmssSSS")→ 10:00:00.000. Always check digit count before applying temporalParse. - File path MUST be provided first: If the user's message does NOT contain a file path (no string matching patterns like
/path/,.csv,.txt,.parquet,.h5,.bin), you MUST useask_userto ask for the file path BEFORE doing anything else. Do NOT attempt to execute any DolphinDB scripts until you have a file path. This is the FIRST step of the workflow and cannot be skipped. - Always preview schema first: use
extractTextSchema(filename)to check auto-detected types before importing. If types are wrong, modify the schema and pass it to the import function. - Date literals in DolphinDB: use
2024.01.01format, NOT'2024-01-01'. - Long integer timestamps: import as LONG first, then convert with
timestamp()function. Do NOT specify LONG timestamps as TIMESTAMP type directly in schema — it will produce NULL values. - Timezone handling: use
localtime()orconvertTZ()to convert UTC timestamps to local time after import. - Header with numbers: if column names start with digits, set
containHeader=trueto preserve names (system prefixes "c"). - Partition conflict: avoid parallel writes to the same partition. Use serial import or set
atomic='CHUNK'. - Partition size: recommended 100 MB to 1 GB per partition (uncompressed).
- Null value handling: use
nullFill/nullFill!(fill with specific value),ffill(forward fill),bfill/bfill!(backward fill),interpolate(linear/pad/nearest/krogh interpolation), orlfill!(linear fill between endpoints). InloadTextEx, usetransformparameter for one-step null filling during import. - FLOAT and DOUBLE cannot be used as partition columns; convert STRING to SYMBOL for partitioning.
- Script blocks are not function libraries: copy script blocks from examples and replace placeholder variables (file paths, database names, table names); do not run entire example files, do not define or call custom functions.
- Interactive decision-making: when creating a distributed table, use ask_user to confirm storage engine and partition scheme before proceeding. Provide options with a recommended default. Do NOT ask about things you can determine yourself (e.g., file format from extension, schema from extractTextSchema, import method from file size).
- Partial column loading: For text files, use schema's
colcolumn to specify column indices (must be ascending). For Parquet, usecolumnsToLoadparameter (integer vector of zero-based column indices). For HDF5, load all columns then select. - Row skipping: For text files, use
skipRows(0-1024). For binary files, useskipBytesinloadRecordoroffset/lengthinreadRecord!. For Parquet, usestartRowGroupandrowGroupNumto read specific row groups. For HDF5, usestartRowandrowNumparameters inloadHDF5/loadHDF5Ex. - Binary file schema is mandatory: DolphinDB cannot auto-detect binary file structure. ALWAYS obtain schema from user before using
loadRecordorreadRecord!. If user provides the writeRecord script, derive schema from it. - Data import workflow: Follow the 10-step workflow (file path → format → metadata → user confirm → import method → preprocessing → final form → execute → verify → summary). Skip steps only when information is already provided or can be reliably inferred.
- Schema confirmation is MANDATORY (Step 4): NEVER skip schema confirmation, even when all types appear correct and no conversion is needed. The user may want to adjust types, and their confirmation takes priority.
- Memory table variable name confirmation: When importing to a memory table, you MUST ask the user to confirm the variable name (e.g.,
tint = loadText(...)). Suggest a default name based on the file name (e.g.,tradesfortrades.csv). Before assigning, check if the variable name already exists usingtry { objByName("varName", false) } catch(ex) { ... }. If the name exists: (i) Ask the user whether to overwrite (useundef(\varName)to delete the old variable, then reassign) or rename (provide a new name and validate uniqueness again). (ii) NEVER overwrite a variable without user confirmation. **⚠️ API note**:existsShareVariableonly checks shared variables — for local variables, use thetry { objByName(...) } catch(ex)pattern. To delete a local variable, useundef(`varName)`. - Existing database/table handling (⚠️ CRITICAL): Before creating a distributed table, check if the database and table already exist. Use
existsDatabase("dfs://path")to check database, thenexistsTable("dfs://path", "tableName")to check table. ⚠️ API pitfall:existsTableandlistTablestake a path string (e.g.,"dfs://sensor_db"), NOT adatabase()object.db.listTables()will cause "db isn't an instance of a class" error — always uselistTables("dfs://path")instead. If the database exists: (i) IfexistsTablereturns false (table name differs from any existing table), import directly into the existing database without dropping it. (ii) IfexistsTablereturns true (table name matches an existing table), determine the import mode by analyzing the prompt content: Append import mode (prompt explicitly mentions "append"/"追加", OR the table existed before this test flow — e.g., user pre-created the table for historical data accumulation): verify schema consistency between source data and existing table, adjust source data structure to match existing table schema, then append data. If schema mismatch or import fails, create a new table (preferably in the same database) and import there. New table import mode (prompt does NOT mention append, OR the table was created by a previous executor attempt in the same test flow): usetruncate(dbPath, tableName)to clear existing data before import to avoid duplication from repeated executor attempts. In interactive mode, ask the user whether to append or overwrite. NEVER drop an existing database or table without explicit user confirmation. - Non-interactive mode (⚠️ CRITICAL — for automated testing/batch processing): When the user's initial prompt provides ALL necessary information (file path, target form, database path, table name, engine, partition scheme, schema, type conversions, etc.), SKIP all
ask_userconfirmation steps and execute directly. If the target database already exists and the table name conflicts, use a new database path (e.g., append a suffix) instead of dropping the old database — this avoids irreversible operations. ⚠️ Testing platform restriction: The DolphinX testing platform PROHIBITSdropDatabaseanddropTableoperations. NEVER use these functions in testing scenarios — always useexistsDatabase+existsTableconditional creation logic, or use a unique database path to avoid conflicts. ⚠️ Import mode handling when table exists: When the table already exists, determine the import mode by analyzing the prompt: (i) If the prompt explicitly mentions "append"/"追加" → append import mode: verify schema consistency between source data and existing table (column count, partition column inclusion, column type compatibility), adjust source data structure to match existing table schema, then append data. If schema mismatch or import fails, create a new table (preferably in the same database with a new name) and import there. (ii) If the prompt does NOT mention append → new table import mode: usetruncate(dbPath, tableName)to clear existing data before import to avoid duplication from repeated executor attempts within the same test flow.truncatepreserves the table structure and only clears data — it is NOT a drop operation and is safe to use on the testing platform. Iftruncatefails, append to existing data and report a warning. - DB/Table existence 4-branch handling (⚠️ CRITICAL): When using
loadTextEx+transform(or any import function that requires a pre-created table), MUST handle all 4 combinations of database/table existence:
Without this 4-branch logic, repeated runs will fail with "transform requires pre-created table" or "database already exists" errors. ⚠️ Data duplication prevention: In non-interactive mode, if the table already exists and the prompt does NOT explicitly request append import, usedbExists = existsDatabase(dbPath) tableExists = iif(dbExists, existsTable(dbPath, tableName), false) if (!dbExists) { // Branch 1: DB doesn't exist — create new DB and table db = database(dbPath, VALUE, ..., engine="OLAP") db.createPartitionedTable(schemaTable, tableName, partitionColumns) } else if (dbExists && !tableExists) { // Branch 2: DB exists but table doesn't — create table in existing DB db = database(dbPath) db.createPartitionedTable(schemaTable, tableName, partitionColumns) } else { // Branch 3 & 4: DB and table both exist db = database(dbPath) // Determine import mode by analyzing prompt: // - Append import (table existed before this test flow): verify schema, then append // - New table import (table from previous executor attempt): truncate to avoid duplication // Non-interactive mode defaults: // - If prompt mentions "append"/"追加": append mode (verify schema, then append) // - Otherwise: truncate mode (clear data, then import fresh) try { truncate(dbPath, tableName) } catch(ex) { /* truncate failed, append to existing data */ } } loadTextEx(db, tableName, partitionColumns, filePath, schema=schema, transform=transformFunc)truncate(dbPath, tableName)to clear existing data before import. This prevents data duplication caused by repeated executor attempts within the same test flow.truncatepreserves the table structure and only clears data — it is NOT a drop operation and is safe on the testing platform. Iftruncatefails (e.g., permission error), append to existing data and report a warning. - transform function mutable rules (⚠️ CRITICAL): The
transformfunction parameter is a constant reference by default.- Using
update t set col = ...syntax: NOmutablekeyword needed (recommended, simpler) - Using
replaceColumn!(t, ...)or other mutable functions: parameter MUST be declared asmutable t - Example 1 (update syntax):
def cleanTransform(t) { update t set volume = nullFill(volume, 0); return t } - Example 2 (mutable syntax):
def convertMarketData(mutable t) { replaceColumn!(t,ts, timestamp(exec ts from t)); return t }` - WRONG:
def convert(t) { replaceColumn!(t,col, ...) }` — will fail with "Constant variable [t] can't be used as argument for mutable function replaceColumn!"
- Using
- transform requires pre-created table (⚠️ CRITICAL): When using
transformparameter withloadTextEx/loadParquetEx/loadHDF5Ex, the partitioned table MUST be created BEFORE calling the import function usingdb.createPartitionedTable(schemaTable, tableName, partitionColumns, [sortColumns]).loadTextExwithtransformdoes NOT auto-create the table. Withouttransform,loadTextExauto-creates the table. - TSDB engine requires sortColumns (⚠️ CRITICAL): When using
engine="TSDB",sortColumnsparameter is MANDATORY increatePartitionedTableandloadTextEx. IfsortColumnsis not needed, useengine="OLAP"instead. OLAP engine does NOT support array vector types (DOUBLE[], INT[]) — for array vectors, MUST use TSDB withsortColumns. - Engine-aware import verification (⚠️ CRITICAL — row count equality check fails for PKEY / TSDB+LAST/FIRST): After import, NEVER verify data correctness by simply comparing
afterCount - beforeCount == expectedImportCountfor ALL engines — this equality check is ONLY valid for non-overwrite scenarios (OLAP engine, or TSDB withkeepDuplicates=ALL). For PKEY engine or TSDB withkeepDuplicates=LAST/keepDuplicates=FIRST, the engine OVERWRITES existing rows with the same primary key (PKEY Merge-on-Write) or DEDUPLICATES rows with the samesortColumnsvalue (TSDB+LAST/FIRST keeps only the last/first occurrence), soafterCount - beforeCountmay be LESS thanexpectedImportCounteven though the import succeeded. For these overwrite/deduplication scenarios, MUST use the three-step key-existence check (Strategy B) detailed in Step 9 of the workflow: (B1)afterCount >= beforeCount; (B2)afterCount - beforeCount <= expectedImportCount; (B3) extract unique key combinations viaselect distinct keyCols from sourceData, extract the same key combinations from the target table, useej(uniqueKeys, targetKeys,keyCols)to find matched keys, and verifycount(matchedKeys) == count(uniqueKeys). **⚠️ NEVER rely ontableInsert/append!/loadTextExreturn values for verification**: For PKEY engines and key-value tables,tableInsertreturn value only counts NEW rows inserted (NOT rows updated due to key conflicts);append!andloadTextExdo not return inserted count at all. Always use SQLselect count(*)` and key-existence checks instead. - Key columns for engine-aware verification: Determine the key columns for Strategy B based on the storage engine used at table creation time. (i) PKEY engine: use the columns declared in the
primaryKeyparameter ofcreatePartitionedTable. (ii) TSDB engine with keepDuplicates=LAST/FIRST: use the columns declared in thesortColumnsparameter. IfsortColumnscontains only ONE column that coincides with the partition column, append the partition column to form a composite key. (iii) For OLAP engine or TSDB with keepDuplicates=ALL, Strategy A (row count difference equality) is sufficient — no key-existence check needed. - sortColumns format (⚠️ CRITICAL):
sortColumnsaccepts column name vector, NOT comma-separated string.- Single column:
sortColumns=date`` orsortColumns="date"(string scalar) - Multiple columns:
sortColumns=date`sym`` (backtick vector, recommended) - Variable reference:
sortColumns=colName(variable value is string or symbol) - WRONG:
sortColumns="date,sym"(interpreted as single column name "date,sym") - WRONG:
sortColumns=["date","sym"](string array, should use backticks)
- Single column:
- loadHDF5Ex partition column type CANNOT be transformed (⚠️ CRITICAL):
loadHDF5Exuses the HDF5 file's schema to create the partitioned table, so the partition column type is FIXED to the HDF5 type (e.g., INT). Thetransformfunction only transforms data AFTER it's loaded, but the partition scheme is determined at table creation time.- If HDF5 has INT
tradingDaybut database requires DATE partition:loadHDF5Exwill ALWAYS fail with "partitioning column type doesn't match" - Solution: MUST use
hdf5::loadHDF5+replaceColumn!+createPartitionedTable(with correct DATE type) +pt.append! - Same rule applies to
parquet::loadParquetEx
- If HDF5 has INT
- IOTDB create table syntax (⚠️ CRITICAL — #1 cause of IOTDB import failure): IOTANY column can ONLY be created via SQL
create tablestatement, NOT viatable()function orcreatePartitionedTable.- Full syntax (SQL statement executed directly in DolphinDB script):
create table `dfs://iot_db`.`sensors` ( deviceId INT, location SYMBOL, timestamp TIMESTAMP, value IOTANY ) partitioned by deviceId, timestamp sortColumns=`deviceId`location`timestamp latestKeyCache=true - Identifiers MUST use backticks (`), NOT double quotes (")
partitioned byis followed by comma-separated column names (NOT backtick vector)sortColumnsuses backtick vectorlatestKeyCache=trueis a key-value pair connected with=- Common error:
schema must be provided to create tableindicates syntax parsing failure — check backticks and keywords
- Full syntax (SQL statement executed directly in DolphinDB script):
- HDF5 plugin: ls vs lsTable (⚠️ CRITICAL):
hdf5::lsandhdf5::lsTablereturn DIFFERENT column structures:hdf5::ls(path)returns table with columns [objName, objType] — for exploring file structurehdf5::lsTable(path)returns table with column [tableName] — for listing datasets- DO NOT mix column names:
lshas nonameortableNamecolumn;lsTablehas noobjNameorobjTypecolumn - Recommended: use
hdf5::lsTable(path)to get dataset names, then usetableName[0]as scalar
- getLoadedPlugins() column names (⚠️ CRITICAL):
getLoadedPlugins()returns a TABLE with EXACTLY these columns: [plugin, version, user, time].- Correct:
exec plugin from getLoadedPlugins() where plugin = "HDF5" - WRONG:
exec name from getLoadedPlugins()(nonamecolumn) - WRONG:
exec pluginName from getLoadedPlugins()(nopluginNamecolumn) - WRONG:
getLoadedPlugins().has_key("HDF5")(returns table not dict)
- Correct:
- HDF5 plugin: datasetName must be string scalar (⚠️ CRITICAL):
hdf5::extractHDF5Schema(path, datasetName)andhdf5::loadHDF5(path, datasetName)requiredatasetNameto be a STRING SCALAR, not a vector.- WRONG:
dsName = exec tableName from datasets limit 1(returns vector) - Correct:
dsName = exec tableName from datasets limit 1; dsName = dsName[0](take first element as scalar) - Correct:
dsName = datasets.tableName[0](directly take first element)
- WRONG:
- dict() construction rules (⚠️ CRITICAL):
- Syntax:
dict(keyVector, valueVector)ordict(\key1`key2, [val1, val2])` - key and value count MUST be equal
- WRONG:
dict(["a","b"], [v1])(count mismatch) - WRONG:
dict(["a","b"], [exec count(*) from pt, exec sum(x) from pt])(embedding exec SQL causes parser confusion) - Correct:
rowCount = exec count(*) from pt; dict(["rows"], [rowCount])(store in variable first) - WRONG:
dict(["a","b"] as \a`b, ...)` (as syntax not applicable to dict)
- Syntax:
- DolphinDB reserved words (⚠️ CRITICAL): DO NOT use these as variable names:
name,type,tuple,dict,table,select,exec,from,where,by,update,insert,delete- Use
colNameinstead ofname,dataTupleinstead oftuple - In schema tables,
nameandtypeare column names, not variable names
- DolphinDB has NO .limit() method (⚠️ CRITICAL): Tables in DolphinDB do NOT have a
.limit()method. To get top N rows:- Correct:
select top 10 * from tableName - Correct:
tableName[:10](take first 10 rows) - WRONG:
tableName.limit(10)(no such method) - WRONG:
tableName.head(10)(no such method)
- Correct:
- DolphinDB function name pitfalls: DolphinDB built-in functions are LOWERCASE and case-sensitive. Common mistakes:
strLen→ should bestrlenIsValid→ should beisvalid(but isvalid doesn't exist, use other methods)Count→ should becountExists→ should beexistsDatabase/existsTable/existsShareVariablevalid()function does NOT exist — uset.size() > 0orcount(t) > 0to check vector emptiness
- loadParquetEx parameter count (⚠️ CRITICAL):
parquet::loadParquetExaccepts 4~9 arguments ONLY. Parameter order:(dbHandle, tableName, partitionColumns, fileName, [schema], [columnsToLoad], [startRowGroup], [rowGroupNum], [transform]).transformis the 9th (last) parameter. Do NOT pass more than 9 arguments. - schema() function usage:
schema(table)returns a schema object (NOT a table).- Correct:
schema(pt)returns directly - Correct:
schema(pt).colDefsgets column definition table - WRONG:
select name, type from schema(pt)(schema() cannot be used in FROM clause)
- Correct:
- extractTextSchema returns metadata table, NOT schema table (⚠️ CRITICAL):
extractTextSchemareturns a table with columns [name, type, ...] describing the schema. To use it withcreatePartitionedTable, convert it to an empty table first:schemaTable = table(1:0, schema.name, schema.type). NEVER passextractTextSchemaresult directly tocreatePartitionedTable. - Derived partition column: When the source data has no suitable partition column but has a timestamp column, derive a DATE column from the timestamp for partitioning.
- Example:
addColumn(t,trade_date, DATE); t[trade_date] = date(exec timestamp from t) - Then use
trade_dateas the partition column
- Example:
- File deletion in DolphinDB:
rmdir(path): deletes EMPTY directory only, NOT filesdeleteFile(path): deletes a file (correct function for file deletion)- For temporary files, use
try { deleteFile(path) } catch(ex) {}pattern
- Plugin function parse-time recognition: Plugin functions with
::prefix (e.g.,parquet::extractParquetSchema,hdf5::lsTable) are recognized at PARSE time. If the plugin is not loaded before parsing, the function call will fail with "Cannot recognize function".- Solution 1: Use
use pluginNameafterloadPluginto import namespace, then call bare function names - Solution 2: Ensure
loadPluginis called BEFORE the script block containing::functions - Solution 3: For compile probe, wrap plugin calls in try/catch or use
useapproach
- Solution 1: Use
- JSON serialization limitations:
toStdJson()andinternalToStdJsondo NOT support all data forms:- Supported: STRING, INT, DOUBLE, BOOL, DATE, TIMESTAMP and other scalars and vectors
- NOT supported: PAIR, MATRIX, DICT, SET and other complex data forms
- Solution: convert complex types to simple vectors before serialization, or use
string()conversion
Confirmation Strategy
Data import involves irreversible operations. Follow these confirmation rules:
Mandatory Confirmation (must stop and ask user before proceeding):
- If the user has not provided a file path: ask via ask_user to provide one. If the user's message does NOT contain a file path (no string matching patterns like
/path/,.csv,.txt,.parquet,.h5,.bin), you MUST useask_userto ask for the file path BEFORE doing anything else. Do NOT attempt to execute any DolphinDB scripts until you have a file path. This is the FIRST step of the workflow and cannot be skipped. If the user does not provide a file path, end the workflow gracefully. - If the user has not specified the target form (memory table vs distributed table): ask via ask_user. Default is memory table.
- Memory table variable name confirmation: When importing to a memory table, you MUST ask the user to confirm the variable name (e.g.,
tint = loadText(...)). Suggest a default name based on the file name (e.g.,tradesfortrades.csv). Before assigning, check if the variable name already exists using:
If the name exists, ask the user whether to overwrite (usetry { objByName("varName", false); varExists = true } catch(ex) { varExists = false }undef(\varName)` then reassign) or rename. NEVER overwrite a variable without user confirmation. - Before creating a distributed table: output engine selection, partition scheme, schema mapping, and confirmation question; wait for user confirmation before executing CREATE DATABASE/TABLE. NEVER assume a database path — always ask the user to confirm.
- Existing database/table handling (⚠️ CRITICAL): Before creating a distributed table, check both database and table existence. Use the following code pattern:
⚠️ API pitfall:dbExists = existsDatabase("dfs://path") tableExists = iif(dbExists, existsTable("dfs://path", "tableName"), false)existsTable("dfs://path", "tableName")andlistTables("dfs://path")take a path string, NOT adatabase()object.db.listTables()will cause "db isn't an instance of a class" error. If the database exists: (i) IfexistsTablereturns false (table name differs from any existing table), import directly into the existing database without dropping it. (ii) IfexistsTablereturns true (table name matches an existing table), ask the user whether to overwrite (drop and recreate) or append. NEVER drop an existing database or table without explicit user confirmation. - Before binary file import: output inferred schema and data preview; wait for user confirmation that the schema is correct before executing loadRecord or readRecord!.
- Schema confirmation is MANDATORY (Step 4): After parsing metadata, present the COMPLETE schema (with auto-detected time-type suggestions) to the user via ask_user and wait for confirmation. NEVER skip this step, even when all types appear correct and no conversion is needed — the user may want to adjust types, and their confirmation takes priority.
Conditional Confirmation (stop and ask when unclear):
- Storage engine is not specified and multiple reasonable choices exist.
- Partition scheme has multiple reasonable options — present specific choices based on data columns and selected engine (e.g., VALUE(time_col), HASH(category_col, N), COMPO(VALUE + HASH)). For IOTDB, only COMPO with time as LAST dimension.
- Column type is ambiguous: STRING columns containing delimited values that may be array vectors (e.g., "1.0,2.0,3.0"), or LONG/INT columns containing values that look like dates (8-digit integers) or timestamps (10-13 digit integers). Use ask_user to confirm the intended type and any conversion format.
- Date/time format cannot be inferred from data sample.
- Null handling strategy may affect business semantics.
Can Continue But Must Explain:
- File format inferred from extension — state the inference basis.
- Schema correction following skill rules (e.g., LONG timestamp → timestamp()) — state what was corrected.
- Import method auto-selected by file size — state the selection basis.
Quick Path
- When user instruction already specifies engine, partition scheme, or target table, skip corresponding confirmation points and proceed directly.
- When user requests quick import, use defaults (OLAP engine + VALUE partition) and skip non-mandatory confirmations; state which confirmations were skipped.
- When user provides the writeRecord script that created a binary file, derive schema from that script directly; no need to ask about schema.
Workflow
Follow these steps in order. For each step, if the information is already provided by the user or can be reliably inferred, proceed without asking. If confirmation is needed, use ask_user.
Confirm file path: Verify the data file path provided by the user. If the user's message does NOT contain a file path (no string matching patterns like
/path/,.csv,.txt,.parquet,.h5,.bin), you MUST useask_userto ask for the file path BEFORE doing anything else. Do NOT attempt to execute any DolphinDB scripts until you have a file path. This is the FIRST step of the workflow and cannot be skipped. If the user does not provide a file path, end the workflow gracefully.Identify file format: Auto-detect format from file extension (.csv, .txt, .bin, .parquet, .h5/.hdf5) or file header. If ambiguous, ask the user.
Parse metadata: For text files, use
extractTextSchema(filename, [delimiter])to preview column names, types, and delimiter. For Parquet, useparquet::extractParquetSchema. For HDF5, usehdf5::extractHDF5Schema(after loading plugin and exploring withhdf5::lsTable). For binary files, there is no auto-detection function — the schema MUST be provided by the user (see Binary File Schema below).Auto-detect time-type columns and present schema for user confirmation: Apply the Time-Type Auto-Detection rules below to identify columns that may need type conversion. Present the COMPLETE schema (including auto-detected type suggestions) to the user via ask_user. The user MUST confirm the schema before proceeding. This step is MANDATORY — never skip it, even when all types appear correct and no conversion is needed. The user may want to adjust types, and their confirmation takes priority.
Confirm target form: If the user has not specified the target form, ask: memory table or distributed table? Default is memory table if not specified. If memory table:
- Ask user to confirm the variable name (e.g.,
tint = loadText(...)). Suggest a default name based on the file name (e.g.,tradesfortrades.csv). - Before assigning, check if the variable name already exists using:
If the name exists, ask the user whether to overwrite (usetry { objByName("varName", false); varExists = true } catch(ex) { varExists = false }undef(\varName)` then reassign) or rename. NEVER overwrite a variable without user confirmation. If distributed table: - Ask user to confirm database path and table name (suggest defaults based on file name). NEVER assume a database path without user confirmation.
- Before creating, check both database and table existence using:
⚠️ API pitfall:dbExists = existsDatabase("dfs://path") tableExists = iif(dbExists, existsTable("dfs://path", "tableName"), false)existsTableandlistTablestake a path string (e.g.,"dfs://sensor_db"), NOT adatabase()object.db.listTables()will cause "db isn't an instance of a class" error — always uselistTables("dfs://path")instead. If the database exists: (i) IfexistsTablereturns false (table name differs from any existing table), import directly into the existing database without dropping it. (ii) IfexistsTablereturns true (table name matches an existing table), ask the user whether to overwrite (drop and recreate) or append via ask_user. NEVER drop an existing database or table without explicit user confirmation. - Ask user to confirm storage engine (OLAP/TSDB/PKEY/IOTDB) with recommended default.
- Ask user to confirm partition scheme with specific options based on data columns.
- Ask user to confirm the variable name (e.g.,
Confirm import method: Based on file format and target, select the appropriate import function:
- Text → memory table:
loadTextorploadText - Text → distributed table:
loadTextEx - Text → very large file:
textChunkDS+mr - Binary with string columns:
loadRecord - Binary without string columns:
readRecord!(preferred) - Parquet → memory table:
parquet::loadParquet - Parquet → distributed table (no sortColumns needed):
parquet::loadParquetEx - Parquet → distributed table (sortColumns needed):
parquet::loadParquet+replaceColumn!+pt.append!(data) - HDF5 → memory table:
hdf5::loadHDF5 - HDF5 → distributed table (no sortColumns needed):
hdf5::loadHDF5Ex - HDF5 → distributed table (sortColumns needed):
hdf5::loadHDF5+replaceColumn!+pt.append!(data)
- Text → memory table:
Data preprocessing: Apply any user-specified or default preprocessing before or during import:
- Specify date/time format for date columns (via schema
formatcolumn orreplaceColumn!after import) - Select specific columns to import (via schema
colcolumn for text files,columnsToLoadfor Parquet) - Skip rows (via
skipRowsfor text files,skipBytesfor binary files) - Handle null values (via
transformparameter withnullFill!,ffill!, etc.) - For Parquet: specify
startRowGroupandrowGroupNumto read specific row groups; specifycolumnsToLoadto load specific columns - For HDF5: use
startRowandrowNumparameters inhdf5::loadHDF5/hdf5::loadHDF5Exfor efficient row range selection
- Specify date/time format for date columns (via schema
Execute import: Run the import script. If errors occur, diagnose and fix (e.g., schema mismatch, partition conflict, plugin not loaded).
Verify result: Execute
select count(*) from tableName(NOTtableName.count()) andselect top 10 * from tableName. For distributed tables,tableName.count()may return 0 or incorrect results — always use SQLselect count(*).⚠️ Verification mode depends on import mode (determined by prompt content and table existence check — see "Existing database/table handling" and "Non-interactive mode" rules):
Non-append import mode (DEFAULT — new table OR truncate-then-import): After import, simply verify
exec count(*) from loadTable(dbPath, tableName) == expectedImportCountand checkselect top 10 * from loadTable(dbPath, tableName)for data correctness. Since the table was either newly created or cleared withtruncate(dbPath, tableName)before import, there are NO pre-existing rows to conflict with — the final row count should exactly equal the source data row count for ALL storage engines (OLAP, TSDB, PKEY, IOTDB). No beforeCount/afterCount tracking or key-existence check is needed in this mode.Append import mode (⚠️ engine-aware verification REQUIRED — ONLY when prompt explicitly mentions "append"/"追加"): When data is appended to an existing table that already contains rows, the simple row-count equality check may FAIL for overwrite/deduplication engines. In append mode, MUST use the engine-aware verification mechanism based on the storage engine, because
tableInsert/append!/loadTextExreturn values are unreliable for PKEY/key-value tables (PKEY engine only counts NEW rows, not updates from key conflicts;append!andloadTextExdo not return inserted count at all):Strategy A — Row count difference check (non-overwrite engines): Use when the storage engine is
OLAP, orTSDBwithkeepDuplicates=ALL(default). Every appended row is added without deduplication, so the row count difference accurately reflects imported rows:beforeCount = exec count(*) from loadTable(dbPath, tableName) // ... execute append import ... afterCount = exec count(*) from loadTable(dbPath, tableName) expectedImportCount = /* row count of source data */ // Verify exact equality: if (afterCount - beforeCount == expectedImportCount) { /* success */ }Strategy B — Key existence check (overwrite/deduplication engines): Use when the storage engine is
PKEY, orTSDBwithkeepDuplicates=LASTorkeepDuplicates=FIRST. In append mode, the engine may OVERWRITE existing rows with the same primary key (PKEY) or KEEP only the last/first occurrence (TSDB+LAST/FIRST), soafterCount - beforeCountmay be LESS thanexpectedImportCount— the row count difference equality check (Strategy A) WILL FAIL. Use
…(truncated)