diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index d2d8ccb..29402d3 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -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 diff --git a/README.md b/README.md index 144dce2..21dc65a 100644 --- a/README.md +++ b/README.md @@ -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 | @@ -117,7 +117,7 @@ json2sql is one of eight tools in the DevForge suite. One license covers all CLI ---
- Part of DevForge — CLI tools built by autonomous AI. + Part of Revenue Holdings — CLI tools built by autonomous AI.
## License diff --git a/src/json2sql/__pycache__/__init__.cpython-312.pyc b/src/json2sql/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index d74d106..0000000 Binary files a/src/json2sql/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/src/json2sql/__pycache__/__init__.cpython-314.pyc b/src/json2sql/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index 53328d5..0000000 Binary files a/src/json2sql/__pycache__/__init__.cpython-314.pyc and /dev/null differ diff --git a/src/json2sql/__pycache__/cli.cpython-312.pyc b/src/json2sql/__pycache__/cli.cpython-312.pyc deleted file mode 100644 index 3499064..0000000 Binary files a/src/json2sql/__pycache__/cli.cpython-312.pyc and /dev/null differ diff --git a/src/json2sql/__pycache__/cli.cpython-314.pyc b/src/json2sql/__pycache__/cli.cpython-314.pyc deleted file mode 100644 index 700dfef..0000000 Binary files a/src/json2sql/__pycache__/cli.cpython-314.pyc and /dev/null differ diff --git a/src/json2sql/__pycache__/converter.cpython-312.pyc b/src/json2sql/__pycache__/converter.cpython-312.pyc deleted file mode 100644 index a5b1d3b..0000000 Binary files a/src/json2sql/__pycache__/converter.cpython-312.pyc and /dev/null differ diff --git a/src/json2sql/__pycache__/converter.cpython-314.pyc b/src/json2sql/__pycache__/converter.cpython-314.pyc deleted file mode 100644 index 16dd35d..0000000 Binary files a/src/json2sql/__pycache__/converter.cpython-314.pyc and /dev/null differ diff --git a/src/json2sql/__pycache__/dialects.cpython-312.pyc b/src/json2sql/__pycache__/dialects.cpython-312.pyc deleted file mode 100644 index 52001fb..0000000 Binary files a/src/json2sql/__pycache__/dialects.cpython-312.pyc and /dev/null differ diff --git a/src/json2sql/__pycache__/dialects.cpython-314.pyc b/src/json2sql/__pycache__/dialects.cpython-314.pyc deleted file mode 100644 index 3cf8804..0000000 Binary files a/src/json2sql/__pycache__/dialects.cpython-314.pyc and /dev/null differ diff --git a/tests/__pycache__/test_converter.cpython-312-pytest-9.0.3.pyc b/tests/__pycache__/test_converter.cpython-312-pytest-9.0.3.pyc deleted file mode 100644 index 1b1664f..0000000 Binary files a/tests/__pycache__/test_converter.cpython-312-pytest-9.0.3.pyc and /dev/null differ diff --git a/tests/__pycache__/test_converter.cpython-314-pytest-9.0.3.pyc b/tests/__pycache__/test_converter.cpython-314-pytest-9.0.3.pyc deleted file mode 100644 index b4c0580..0000000 Binary files a/tests/__pycache__/test_converter.cpython-314-pytest-9.0.3.pyc and /dev/null differ diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..88f5c29 --- /dev/null +++ b/tests/test_cli.py @@ -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