Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Build with Jekyll
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,13 @@ sqlite3 test.db < seed.sql

## Pricing

json2sql is one of eight tools in the DevForge suite. One license covers all CLI tools.
json2sql is one of eleven CLI tools in the Revenue Holdings suite. One license covers all CLI tools.

| Plan | Price | Best For |
|------|-------|----------|
| **Free** | $0 | Individual devs, OSS — CLI only, limited rows |
| **json2sql Individual** | **$9/mo** ($7 billed annually) | Professional devs — unlimited rows, batch processing |
| **Suite (all 8 tools)** | **$49/mo** ($39 billed annually) | Full DevForge toolkit — 40% savings |
| **Suite (all 11 CLI tools)** | **$49/mo** ($39 billed annually) | Full Revenue Holdings toolkit — 40% savings |
| **Team** | **$79/mo** ($63 billed annually) | Up to 5 devs — API access, CI/CD integration, priority support |
| **Enterprise** | Custom | SSO, RBAC, compliance reports, dedicated support |

Expand All @@ -117,7 +117,7 @@ json2sql is one of eight tools in the DevForge suite. One license covers all CLI
---

<p align="center">
<sub>Part of <a href="https://coding-dev-tools.github.io/devforge.dev/">DevForge</a> — CLI tools built by autonomous AI.</sub>
<sub>Part of <a href="https://coding-dev-tools.github.io/revenueholdings.dev/">Revenue Holdings</a> — CLI tools built by autonomous AI.</sub>
</p>

## License
Expand Down
Binary file removed src/json2sql/__pycache__/__init__.cpython-312.pyc
Binary file not shown.
Binary file removed src/json2sql/__pycache__/__init__.cpython-314.pyc
Binary file not shown.
Binary file removed src/json2sql/__pycache__/cli.cpython-312.pyc
Binary file not shown.
Binary file removed src/json2sql/__pycache__/cli.cpython-314.pyc
Binary file not shown.
Binary file removed src/json2sql/__pycache__/converter.cpython-312.pyc
Binary file not shown.
Binary file removed src/json2sql/__pycache__/converter.cpython-314.pyc
Binary file not shown.
Binary file removed src/json2sql/__pycache__/dialects.cpython-312.pyc
Binary file not shown.
Binary file removed src/json2sql/__pycache__/dialects.cpython-314.pyc
Binary file not shown.
Binary file not shown.
Binary file not shown.
183 changes: 183 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
"""Tests for the json2sql CLI interface."""

import json
from json2sql.cli import app
from typer.testing import CliRunner

runner = CliRunner()


class TestCLIBasic:
"""Basic CLI command tests."""

def test_convert_json_file(self, tmp_path):
"""Convert a simple JSON file to SQL via CLI."""
data = {"name": "Alice", "age": 30}
json_file = tmp_path / "data.json"
json_file.write_text(json.dumps(data))

result = runner.invoke(app, ["convert", str(json_file)])
assert result.exit_code == 0
assert "CREATE TABLE" in result.stdout
assert "INSERT INTO" in result.stdout
assert "'Alice'" in result.stdout
assert "30" in result.stdout

def test_convert_with_table_name(self, tmp_path):
"""Specify custom table name."""
json_file = tmp_path / "data.json"
json_file.write_text(json.dumps({"x": 1}))

result = runner.invoke(app, ["convert", str(json_file), "--table", "my_table"])
assert result.exit_code == 0
assert "CREATE TABLE" in result.stdout
assert "my_table" in result.stdout

def test_convert_with_dialect_mysql(self, tmp_path):
"""Use MySQL dialect via CLI."""
json_file = tmp_path / "data.json"
json_file.write_text(json.dumps({"active": True}))

result = runner.invoke(app, ["convert", str(json_file), "--dialect", "mysql"])
assert result.exit_code == 0
assert "`active` TINYINT(1)" in result.stdout or "`active`" in result.stdout

def test_convert_with_dialect_sqlite(self, tmp_path):
"""Use SQLite dialect via CLI."""
json_file = tmp_path / "data.json"
json_file.write_text(json.dumps({"price": 9.99}))

result = runner.invoke(app, ["convert", str(json_file), "--dialect", "sqlite"])
assert result.exit_code == 0
assert "REAL" in result.stdout

def test_convert_output_file(self, tmp_path):
"""Write SQL to an output file."""
json_file = tmp_path / "data.json"
json_file.write_text(json.dumps({"name": "test"}))
out_file = tmp_path / "out.sql"

result = runner.invoke(app, ["convert", str(json_file), "--output", str(out_file)])
assert result.exit_code == 0
assert out_file.exists()
content = out_file.read_text()
assert "CREATE TABLE" in content
assert "INSERT INTO" in content

def test_convert_with_flatten(self, tmp_path):
"""Flatten nested JSON via CLI."""
data = {"id": 1, "address": {"city": "NYC"}}
json_file = tmp_path / "nested.json"
json_file.write_text(json.dumps(data))

result = runner.invoke(app, ["convert", str(json_file), "--flatten"])
assert result.exit_code == 0
assert "CREATE TABLE" in result.stdout

def test_convert_schema_only(self, tmp_path):
"""Generate schema-only output (no INSERT)."""
json_file = tmp_path / "data.json"
json_file.write_text(json.dumps([{"name": "Alice", "age": 30}]))

result = runner.invoke(app, ["convert", str(json_file), "--schema-only"])
assert result.exit_code == 0
assert "CREATE TABLE" in result.stdout
assert "INSERT INTO" not in result.stdout

def test_convert_stdin(self):
"""Read JSON from stdin."""
result = runner.invoke(app, ["convert"], input=json.dumps({"name": "stdin_test"}))
assert result.exit_code == 0
assert "'stdin_test'" in result.stdout

def test_convert_empty_stdin_no_input(self):
"""Error when no file and stdin is empty."""
# Simulate no input (isatty = True in CliRunner)
result = runner.invoke(app, ["convert"])
assert result.exit_code == 1
assert "Error" in result.stderr or "Error" in result.stdout

def test_convert_bad_json(self, tmp_path):
"""Error on invalid JSON input."""
json_file = tmp_path / "bad.json"
json_file.write_text("{invalid}")

result = runner.invoke(app, ["convert", str(json_file)])
assert result.exit_code == 1
assert "Error" in result.stderr or "Error" in result.stdout

def test_convert_bad_dialect(self, tmp_path):
"""Error on invalid dialect."""
json_file = tmp_path / "data.json"
json_file.write_text(json.dumps({"x": 1}))

result = runner.invoke(app, ["convert", str(json_file), "--dialect", "oracle"])
assert result.exit_code != 0

def test_convert_file_not_found(self):
"""Error when file does not exist."""
result = runner.invoke(app, ["convert", "nonexistent.json"])
# Typer validates exists=True so it should fail
assert result.exit_code != 0


class TestCLIVersion:
"""Version command tests."""

def test_version(self):
"""Show version."""
result = runner.invoke(app, ["version"])
assert result.exit_code == 0
assert "0.1.0" in result.stdout


class TestCLIErrorHandling:
"""Error handling tests."""

def test_no_args_shows_help(self):
"""Running without args shows help."""
result = runner.invoke(app)
# Typer with no_args_is_help may exit 0 or 2 depending on version
assert "Usage:" in result.stdout or "Usage:" in result.stderr or "Convert" in result.stdout or "Convert" in result.stderr

def test_convert_array_of_objects(self, tmp_path):
"""Convert array of objects via CLI."""
data = [{"name": "Alice"}, {"name": "Bob"}]
json_file = tmp_path / "data.json"
json_file.write_text(json.dumps(data))

result = runner.invoke(app, ["convert", str(json_file)])
assert result.exit_code == 0
assert "'Alice'" in result.stdout
assert "'Bob'" in result.stdout

def test_convert_empty_array(self, tmp_path):
"""Empty array produces appropriate message."""
json_file = tmp_path / "empty.json"
json_file.write_text("[]")

result = runner.invoke(app, ["convert", str(json_file)])
assert result.exit_code == 0
assert "Empty" in result.stdout

def test_convert_boolean_values(self, tmp_path):
"""Boolean rendering depends on dialect."""
data = {"flag": True, "active": False}
json_file = tmp_path / "data.json"
json_file.write_text(json.dumps(data))

# Postgres
result = runner.invoke(app, ["convert", str(json_file), "--dialect", "postgres"])
assert result.exit_code == 0
assert "TRUE" in result.stdout
assert "FALSE" in result.stdout

def test_convert_null_values(self, tmp_path):
"""NULL values handled."""
data = {"name": None}
json_file = tmp_path / "data.json"
json_file.write_text(json.dumps(data))

result = runner.invoke(app, ["convert", str(json_file)])
assert result.exit_code == 0
assert "NULL" in result.stdout