Skip to content

feat(import): import JSON files into a table#1536

Merged
datlechin merged 11 commits into
mainfrom
feat/json-import
Jun 1, 2026
Merged

feat(import): import JSON files into a table#1536
datlechin merged 11 commits into
mainfrom
feat/json-import

Conversation

@datlechin

@datlechin datlechin commented Jun 1, 2026

Copy link
Copy Markdown
Member

Adds JSON file import. Today TablePro can export to JSON and import SQL files, but there's no way to import JSON data back in. This closes that gap with a dedicated, native import sheet.

What it does

File ▸ Import → pick a .json / .jsonl / .ndjson file opens a dedicated JSON import sheet (the SQL dialog is unchanged and stays SQL-only). The sheet accepts:

  • an array of objects [{...}, {...}],
  • newline-delimited JSON (one object per line, streamed in constant memory for large files),
  • and TablePro's own JSON export shape { "table": [ {...} ] }, so an export round-trips back in.

Two destinations (segmented control):

  • Existing table: pick the table, then a field-mapping grid with, per row, an include toggle, the JSON field, a sample value, and a target-column pop-up (auto-matched by name; remap or skip any field).
  • New table: name the table and review columns inferred from the data (include, name, type as a dialect-aware editable pop-up, primary key, nullable, default). Creates the table via the existing per-dialect generateCreateTableSQL, then imports.

Options (on-error, wrap-in-transaction, delete-existing-rows) reuse the plugin's settings. Select-all toggles and validation (unique column names, one field per column) gate the Import button.

Architecture

The import contract was SQL-statement-only (source.statements() then sink.execute(statement:)). JSON is the first row-based importer, so the contract is extended additively (default-implemented, so no PluginKit ABI bump; stays at 17):

  • PluginImportDataSink: insertRow(_:), targetTable, deleteAllRowsFromTargetTable().
  • ImportFormatPlugin: requiresTargetTable, detectSourceFields(at:targetTable:) (name, sample, inferred type).

The plugin parses JSON and yields rows; the app owns the target table and generates parameterized inserts via SQLStatementGenerator + driver.executeParameterized (zero string concatenation, so JSON values can't inject SQL). Bool-vs-int uses CFBooleanGetTypeID; nested objects and arrays serialize to JSON text. New JSONImport.tableplugin bundled target, never published to the registry.

UI

Native SwiftUI: Grid/GridRow for both grids (auto-aligned columns), left-aligned destination controls, and a type pop-up sourced from columnTypesByCategory(for:) (same list the structure editor uses). Grid over Table deliberately, since editable TextField cells in Table are unreliable on macOS.

Tests

  • JSONImportPluginTests: array/NDJSON/wrapper parsing, value conversion (null/bool/number/nested), type inference, export-shape round-trip.
  • SQLStatementGeneratorImportTests: parameterized insert + per-dialect quoting, injection-as-data, null binds.
  • JSONImportTypeMapperTests: inferred-type to SQL type per dialect, with a generic fallback.

Known v1 limits

  • Import into an existing table or a new table; create-new infers columns but auto-increment isn't in the inline editor (PK/null/default/type are).
  • Binary round-trip is lossy (base64 stays text unless the column is binary).
  • Detecting fields for a very large single .json array parses the whole file (NDJSON streams).
  • New-table CREATE TABLE commits before the row insert (not one atomic unit on engines where DDL auto-commits).

Docs and CHANGELOG updated.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 08bd721326

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +54 to +58
if settings.deleteExistingRows {
try await sink.deleteAllRowsFromTargetTable()
}
if useTransaction {
try await sink.beginTransaction()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Start the transaction before clearing rows

When deleteExistingRows and the default transactional rollback mode are enabled, the target table is cleared before beginTransaction() runs, so a later parse/insert failure rolls back only the inserted rows and leaves the pre-existing data permanently deleted. Move the delete inside the same transaction as the import so rollback restores the table contents.

Useful? React with 👍 / 👎.

Comment on lines +121 to +123
switch settings.errorHandling {
case .stopAndRollback, .stopAndCommit:
throw PluginImportError.statementFailed(statement: "row \(line)", line: line, underlyingError: error)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Honor stop-and-commit imports

With Stop and Commit selected and transactions enabled, this branch throws the same error as stopAndRollback, and the outer catch unconditionally calls rollbackTransaction(). That discards all rows that were successfully inserted before the failing JSON row, which is the opposite of the selected stop-and-commit behavior.

Useful? React with 👍 / 👎.

Comment on lines +31 to +34
self.columnMapping = Dictionary(
columnMapping.map { ($0.key.lowercased(), $0.value) },
uniquingKeysWith: { _, last in last }
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve case-sensitive JSON field mappings

JSON object keys are case-sensitive, but lowercasing the source field names collapses mappings such as id -> lower_id and ID -> upper_id into one entry. For files with fields that differ only by case, imports can write values to the wrong destination column or generate duplicate target columns instead of honoring the user's mapping.

Useful? React with 👍 / 👎.

# Conflicts:
#	CHANGELOG.md

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: beb90c22c6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +200 to +203
case let number as NSNumber:
if CFGetTypeID(number) == CFBooleanGetTypeID() {
return .text(number.boolValue ? "true" : "false")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve boolean values for MySQL imports

When importing JSON into a newly created MySQL/MariaDB table, inferType maps all-boolean fields to TINYINT(1), but this conversion stores JSON true as the string parameter "true". The MySQL driver binds .text values as MYSQL_TYPE_STRING, and MySQL coerces the string 'true' to numeric 0, so every true value imported into the inferred boolean column is stored as false. This also affects existing MySQL boolean/tinyint targets; booleans need a dialect-aware representation before binding.

Useful? React with 👍 / 👎.

@datlechin datlechin merged commit d1612f0 into main Jun 1, 2026
4 of 5 checks passed
@datlechin datlechin deleted the feat/json-import branch June 1, 2026 15:18

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 18de96f8ee

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


static func extractRows(from object: Any, targetTable: String?) throws -> [[String: Any]] {
if let array = object as? [Any] {
return array.compactMap { $0 as? [String: Any] }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Surface non-object elements instead of dropping them

When a regular .json file is a top-level array, any element that is not a JSON object is silently removed by compactMap, so inputs like [{"id":1}, null, {"id":2}] complete successfully and report only two imported rows. Since the importer advertises an array of objects, default stop-on-error imports should fail rather than hide malformed rows and make the import look complete.

Useful? React with 👍 / 👎.

Comment on lines +62 to +63
var lineNumber = 0
for try await line in url.lines {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip malformed NDJSON rows in continue mode

With Skip and Continue selected for .jsonl/.ndjson, parse errors from an individual line still escape before the row-level error handler is reached, so one malformed JSON line aborts the rest of the file even though later lines could be imported. This makes the continue mode only handle database insert failures, not bad input rows in the line-delimited format.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant