diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000..c09528d --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,64 @@ +# Test Database Workflows + +## Centralized Test Schema + +The database integration tests use a centralized schema to ensure consistency across all supported databases. + +### Schema File + +**`test-schema.json`** - Contains the complete test schema with: +- **84 columns** covering all generator types +- **83 generator fields** (excluding auto-increment ID) +- All categories: Personal, Location, Network, Financial, Time, Localization, Product, Files, Media, Animals, Food, Vehicles, Identifiers, Text, and App data + +### How It Works + +Each database test job: +1. Merges the centralized schema with database-specific configuration using `jq` +2. Validates that exactly 83 generator fields are present +3. Creates and populates the test table +4. Verifies the row count + +### Benefits + +✅ **Single Source of Truth** - All generator fields defined once +✅ **Consistency** - Same tests across all 6 databases +✅ **Easy Updates** - Add new generators in one place +✅ **Validation** - Automatic field count verification +✅ **Reduced Duplication** - 71 lines vs 569 lines per database + +### Supported Databases + +1. **MySQL** 8.0 +2. **MariaDB** 10.11 +3. **PostgreSQL** 14 +4. **SQLite** 3.x +5. **MS SQL Server** 2022 +6. **CockroachDB** Latest + +### Adding New Generators + +To add a new generator to all database tests: + +1. Add the column to `test-schema.json` in the `tables[0].columns` array +2. Add the generator field to `test-schema.json` in the `populate[0].fields` array +3. Update the field count validation in `test-databases.yml` (increment the `83` in all 6 tests) + +Example: +```json +// In tables[0].columns: +{"name": "new_field", "type": {"name": "string", "args": {"length": 50}}} + +// In populate[0].fields: +{"name": "new_field", "generator": "new_generator"} +``` + +### Schema Validation + +Each test automatically validates: +```bash +FIELD_COUNT=$(jq '.populate[0].fields | length' test-schema.json) +[ "$FIELD_COUNT" -eq 83 ] || (echo "❌ Expected 83 fields, got $FIELD_COUNT" && exit 1) +``` + +This ensures no fields are accidentally omitted during schema merging. diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 939ac30..e8694b9 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -51,8 +51,11 @@ jobs: if [ "${{ github.event_name }}" = "push" ]; then REF="${{ github.ref }}" TAG_VERSION="${REF#refs/tags/}" - else + elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then TAG_VERSION="${{ inputs.tag }}" + else + echo "❌ Unexpected event type: ${{ github.event_name }}" + exit 1 fi VERSION=$(echo "$TAG_VERSION" | sed 's/^v//') @@ -408,7 +411,8 @@ jobs: ### Homebrew (macOS/Linux) ```bash - brew install 0xdps/tap/fakestack + brew tap 0xdps/packages + brew install fakestack ``` ### Go @@ -469,9 +473,9 @@ jobs: echo "linux_arm64=$(sha256sum binaries/fakestack-linux-arm64 | awk '{print $1}')" >> $GITHUB_OUTPUT echo "linux_amd64=$(sha256sum binaries/fakestack-linux-amd64 | awk '{print $1}')" >> $GITHUB_OUTPUT - - name: Clone homebrew-fakestack tap + - name: Clone homebrew-packages tap run: | - git clone https://github.com/0xdps/homebrew-fakestack.git tap + git clone https://github.com/0xdps/homebrew-packages.git tap - name: Update Homebrew formula run: | @@ -494,11 +498,11 @@ jobs: cat tap/Formula/fakestack.rb - - name: Push to homebrew-fakestack tap + - name: Push to homebrew-packages tap working-directory: tap run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add Formula/fakestack.rb git commit -m "chore: update formula to ${GITHUB_REF#refs/tags/}" - git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/0xdps/homebrew-fakestack.git main || echo "⚠️ Formula update failed, manual update needed" + git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/0xdps/homebrew-packages.git trunk || echo "⚠️ Formula update failed, manual update needed" diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index ed40b70..3b0d2cd 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -3,6 +3,8 @@ name: Build & Test on: push: branches: [ trunk, develop ] + tags: + - 'v*' paths-ignore: - '**.md' - 'docs/**' diff --git a/.github/workflows/test-databases.yml b/.github/workflows/test-databases.yml new file mode 100644 index 0000000..6ddaf74 --- /dev/null +++ b/.github/workflows/test-databases.yml @@ -0,0 +1,426 @@ +name: Test Database Support + +# Manual trigger only - run on demand to test all database integrations +on: + workflow_dispatch: + inputs: + test_mysql: + description: 'Test MySQL' + required: false + default: true + type: boolean + test_mariadb: + description: 'Test MariaDB' + required: false + default: true + type: boolean + test_postgres: + description: 'Test PostgreSQL' + required: false + default: true + type: boolean + test_sqlite: + description: 'Test SQLite' + required: false + default: true + type: boolean + test_mssql: + description: 'Test MS SQL Server' + required: false + default: true + type: boolean + test_cockroachdb: + description: 'Test CockroachDB' + required: false + default: true + type: boolean + +jobs: + test-mysql: + name: Test MySQL + runs-on: ubuntu-latest + if: github.event.inputs.test_mysql == 'true' + + services: + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: testpass + MYSQL_DATABASE: testdb + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.21' + cache-dependency-path: golang/go.sum + + - name: Build fakestack + working-directory: golang + run: | + CGO_ENABLED=1 go build -o ../bin/fakestack-test + chmod +x ../bin/fakestack-test + + - name: Create test schema + run: | + # Merge centralized schema with database-specific config + jq -s '.[0] * {"database": .[1]}' \ + .github/workflows/test-schema.json \ + <(echo '{ + "dbtype": "mysql", + "username": "root", + "password": "testpass", + "host": "127.0.0.1:3306", + "database": "testdb" + }') \ + > test-schema.json + + # Verify the schema has all expected fields + FIELD_COUNT=$(jq '.populate[0].fields | length' test-schema.json) + echo "Schema contains $FIELD_COUNT generator fields" + [ "$FIELD_COUNT" -eq 83 ] || (echo "❌ Expected 83 fields, got $FIELD_COUNT" && exit 1) + + - name: Test create and populate + run: | + ./bin/fakestack-test -c -p -f test-schema.json + + - name: Verify data + run: | + sudo apt-get update && sudo apt-get install -y mysql-client + COUNT=$(mysql -h 127.0.0.1 -u root -ptestpass testdb -N -s -e "SELECT COUNT(*) FROM comprehensive_test;") + echo "Found $COUNT rows in comprehensive_test" + [ "$COUNT" -eq 10 ] && echo "✅ MySQL test passed - all generators working!" + + test-mariadb: + name: Test MariaDB + runs-on: ubuntu-latest + if: github.event.inputs.test_mariadb == 'true' + + services: + mariadb: + image: mariadb:11 + env: + MYSQL_ROOT_PASSWORD: testpass + MYSQL_DATABASE: testdb + ports: + - 3306:3306 + options: >- + --health-cmd="healthcheck.sh --connect --innodb_initialized" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.21' + cache-dependency-path: golang/go.sum + + - name: Build fakestack + working-directory: golang + run: | + CGO_ENABLED=1 go build -o ../bin/fakestack-test + chmod +x ../bin/fakestack-test + + - name: Create test schema + run: | + # Merge centralized schema with database-specific config + jq -s '.[0] * {"database": .[1]}' \ + .github/workflows/test-schema.json \ + <(echo '{ + "dbtype": "mysql", + "username": "root", + "password": "testpass", + "host": "127.0.0.1:3306", + "database": "testdb" + }') \ + > test-schema.json + + # Verify the schema has all expected fields + FIELD_COUNT=$(jq '.populate[0].fields | length' test-schema.json) + echo "Schema contains $FIELD_COUNT generator fields" + [ "$FIELD_COUNT" -eq 83 ] || (echo "❌ Expected 83 fields, got $FIELD_COUNT" && exit 1) + + - name: Test create and populate + run: | + ./bin/fakestack-test -c -p -f test-schema.json + + - name: Verify data + run: | + sudo apt-get update && sudo apt-get install -y mysql-client + COUNT=$(mysql -h 127.0.0.1 -u root -ptestpass testdb -N -s -e "SELECT COUNT(*) FROM comprehensive_test;") + echo "Found $COUNT rows in comprehensive_test" + [ "$COUNT" -eq 10 ] && echo "✅ MariaDB test passed - all generators working!" + + test-postgres: + name: Test PostgreSQL + runs-on: ubuntu-latest + if: github.event.inputs.test_postgres == 'true' + + services: + postgres: + image: postgres:16 + env: + POSTGRES_PASSWORD: testpass + POSTGRES_DB: testdb + ports: + - 5432:5432 + options: >- + --health-cmd="pg_isready" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.21' + cache-dependency-path: golang/go.sum + + - name: Build fakestack + working-directory: golang + run: | + CGO_ENABLED=1 go build -o ../bin/fakestack-test + chmod +x ../bin/fakestack-test + + - name: Create test schema + run: | + # Merge centralized schema with database-specific config + jq -s '.[0] * {"database": .[1]}' \ + .github/workflows/test-schema.json \ + <(echo '{ + "dbtype": "postgres", + "username": "postgres", + "password": "testpass", + "host": "localhost:5432", + "database": "testdb" + }') \ + > test-schema.json + + # Verify the schema has all expected fields + FIELD_COUNT=$(jq '.populate[0].fields | length' test-schema.json) + echo "Schema contains $FIELD_COUNT generator fields" + [ "$FIELD_COUNT" -eq 83 ] || (echo "❌ Expected 83 fields, got $FIELD_COUNT" && exit 1) + + - name: Test create and populate + run: | + ./bin/fakestack-test -c -p -f test-schema.json + + - name: Verify data + run: | + sudo apt-get update && sudo apt-get install -y postgresql-client + COUNT=$(PGPASSWORD=testpass psql -h localhost -U postgres testdb -t -c "SELECT COUNT(*) FROM comprehensive_test;" | xargs) + echo "Found $COUNT rows in comprehensive_test" + [ "$COUNT" -eq 10 ] && echo "✅ PostgreSQL test passed - all generators working!" + + test-sqlite: + name: Test SQLite + runs-on: ubuntu-latest + if: github.event.inputs.test_sqlite == 'true' + + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.21' + cache-dependency-path: golang/go.sum + + - name: Build fakestack + working-directory: golang + run: | + CGO_ENABLED=1 go build -o ../bin/fakestack-test + chmod +x ../bin/fakestack-test + + - name: Create test schema + run: | + # Merge centralized schema with database-specific config + jq -s '.[0] * {"database": .[1]}' \ + .github/workflows/test-schema.json \ + <(echo '{ + "dbtype": "sqlite", + "host": "test.db" + }') \ + > test-schema.json + + # Verify the schema has all expected fields + FIELD_COUNT=$(jq '.populate[0].fields | length' test-schema.json) + echo "Schema contains $FIELD_COUNT generator fields" + [ "$FIELD_COUNT" -eq 83 ] || (echo "❌ Expected 83 fields, got $FIELD_COUNT" && exit 1) + + - name: Test create and populate + run: | + ./bin/fakestack-test -c -p -f test-schema.json + + - name: Verify data + run: | + sudo apt-get update && sudo apt-get install -y sqlite3 + COUNT=$(sqlite3 test.db "SELECT COUNT(*) FROM comprehensive_test;") + echo "Found $COUNT rows in comprehensive_test" + [ "$COUNT" -eq 10 ] && echo "✅ SQLite test passed - all generators working!" + + test-mssql: + name: Test MS SQL Server + runs-on: ubuntu-latest + if: github.event.inputs.test_mssql == 'true' + + services: + mssql: + image: mcr.microsoft.com/mssql/server:2022-latest + env: + ACCEPT_EULA: Y + SA_PASSWORD: TestPass123! + MSSQL_PID: Developer + ports: + - 1433:1433 + + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.21' + cache-dependency-path: golang/go.sum + + - name: Wait for MSSQL + run: sleep 30 + + - name: Setup database + run: | + docker exec $(docker ps -q --filter ancestor=mcr.microsoft.com/mssql/server:2022-latest) \ + /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'TestPass123!' -C \ + -Q "CREATE DATABASE testdb;" + + - name: Build fakestack + working-directory: golang + run: | + CGO_ENABLED=1 go build -o ../bin/fakestack-test + chmod +x ../bin/fakestack-test + + - name: Create test schema + run: | + # Merge centralized schema with database-specific config + jq -s '.[0] * {"database": .[1]}' \ + .github/workflows/test-schema.json \ + <(echo '{ + "dbtype": "mssql", + "username": "sa", + "password": "TestPass123!", + "host": "localhost:1433", + "database": "testdb" + }') \ + > test-schema.json + + # Verify the schema has all expected fields + FIELD_COUNT=$(jq '.populate[0].fields | length' test-schema.json) + echo "Schema contains $FIELD_COUNT generator fields" + [ "$FIELD_COUNT" -eq 83 ] || (echo "❌ Expected 83 fields, got $FIELD_COUNT" && exit 1) + + - name: Test create and populate + run: | + ./bin/fakestack-test -c -p -f test-schema.json + + - name: Verify data + run: | + COUNT=$(docker exec $(docker ps -q --filter ancestor=mcr.microsoft.com/mssql/server:2022-latest) \ + /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'TestPass123!' -d testdb -h -1 -C \ + -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM comprehensive_test;" | xargs) + echo "Found $COUNT rows in comprehensive_test" + [ "$COUNT" -eq 10 ] && echo "✅ MSSQL test passed - all generators working!" + + test-cockroachdb: + name: Test CockroachDB + runs-on: ubuntu-latest + if: github.event.inputs.test_cockroachdb == 'true' + + steps: + - uses: actions/checkout@v4 + + - name: Start CockroachDB + run: | + docker run -d --name cockroach \ + -p 26257:26257 -p 8080:8080 \ + cockroachdb/cockroach:latest start-single-node --insecure + sleep 10 + + - name: Setup database + run: | + sudo apt-get update && sudo apt-get install -y postgresql-client + PGPASSWORD='' psql -h localhost -p 26257 -U root -c "CREATE DATABASE testdb;" + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.21' + cache-dependency-path: golang/go.sum + + - name: Build fakestack + working-directory: golang + run: | + CGO_ENABLED=1 go build -o ../bin/fakestack-test + chmod +x ../bin/fakestack-test + + - name: Create test schema + run: | + # Merge centralized schema with database-specific config + jq -s '.[0] * {"database": .[1]}' \ + .github/workflows/test-schema.json \ + <(echo '{ + "dbtype": "postgres", + "username": "root", + "password": "", + "host": "localhost:26257", + "database": "testdb" + }') \ + > test-schema.json + + # Verify the schema has all expected fields + FIELD_COUNT=$(jq '.populate[0].fields | length' test-schema.json) + echo "Schema contains $FIELD_COUNT generator fields" + [ "$FIELD_COUNT" -eq 83 ] || (echo "❌ Expected 83 fields, got $FIELD_COUNT" && exit 1) + + - name: Test create and populate + run: | + ./bin/fakestack-test -c -p -f test-schema.json + + - name: Verify data + run: | + COUNT=$(PGPASSWORD='' psql -h localhost -p 26257 -U root testdb -t -c "SELECT COUNT(*) FROM comprehensive_test;" | xargs) + echo "Found $COUNT rows in comprehensive_test" + [ "$COUNT" -eq 10 ] && echo "✅ CockroachDB test passed - all generators working!" + + summary: + name: Test Summary + needs: [test-mysql, test-mariadb, test-postgres, test-sqlite, test-mssql, test-cockroachdb] + runs-on: ubuntu-latest + if: always() + + steps: + - name: Summary + run: | + echo "🎯 Database Testing Complete" + echo "==============================" + echo "Selected databases:" + echo " MySQL: ${{ github.event.inputs.test_mysql == 'true' && needs.test-mysql.result || 'skipped' }}" + echo " MariaDB: ${{ github.event.inputs.test_mariadb == 'true' && needs.test-mariadb.result || 'skipped' }}" + echo " PostgreSQL: ${{ github.event.inputs.test_postgres == 'true' && needs.test-postgres.result || 'skipped' }}" + echo " SQLite: ${{ github.event.inputs.test_sqlite == 'true' && needs.test-sqlite.result || 'skipped' }}" + echo " MSSQL: ${{ github.event.inputs.test_mssql == 'true' && needs.test-mssql.result || 'skipped' }}" + echo " CockroachDB: ${{ github.event.inputs.test_cockroachdb == 'true' && needs.test-cockroachdb.result || 'skipped' }}" diff --git a/.github/workflows/test-schema.json b/.github/workflows/test-schema.json new file mode 100644 index 0000000..b196db9 --- /dev/null +++ b/.github/workflows/test-schema.json @@ -0,0 +1,184 @@ +{ + "tables": [ + { + "name": "comprehensive_test", + "columns": [ + {"name": "id", "type": "integer", "options": {"primary_key": true, "autoincrement": true}}, + {"name": "first_name", "type": {"name": "string", "args": {"length": 50}}}, + {"name": "last_name", "type": {"name": "string", "args": {"length": 50}}}, + {"name": "full_name", "type": {"name": "string", "args": {"length": 100}}}, + {"name": "username", "type": {"name": "string", "args": {"length": 50}}}, + {"name": "email", "type": {"name": "string", "args": {"length": 100}}}, + {"name": "password", "type": {"name": "string", "args": {"length": 255}}}, + {"name": "gender", "type": {"name": "string", "args": {"length": 20}}}, + {"name": "address", "type": {"name": "string", "args": {"length": 255}}}, + {"name": "street_address", "type": {"name": "string", "args": {"length": 255}}}, + {"name": "city", "type": {"name": "string", "args": {"length": 100}}}, + {"name": "state", "type": {"name": "string", "args": {"length": 50}}}, + {"name": "country", "type": {"name": "string", "args": {"length": 100}}}, + {"name": "postcode", "type": {"name": "string", "args": {"length": 20}}}, + {"name": "latitude", "type": "float"}, + {"name": "longitude", "type": "float"}, + {"name": "company", "type": {"name": "string", "args": {"length": 100}}}, + {"name": "job_title", "type": {"name": "string", "args": {"length": 100}}}, + {"name": "phone", "type": {"name": "string", "args": {"length": 30}}}, + {"name": "url", "type": {"name": "string", "args": {"length": 255}}}, + {"name": "domain", "type": {"name": "string", "args": {"length": 100}}}, + {"name": "ipv4", "type": {"name": "string", "args": {"length": 15}}}, + {"name": "ipv6", "type": {"name": "string", "args": {"length": 45}}}, + {"name": "mac_address", "type": {"name": "string", "args": {"length": 17}}}, + {"name": "date_field", "type": "date"}, + {"name": "past_date", "type": "date"}, + {"name": "future_date", "type": "date"}, + {"name": "word", "type": {"name": "string", "args": {"length": 50}}}, + {"name": "sentence", "type": {"name": "string", "args": {"length": 255}}}, + {"name": "paragraph", "type": "text"}, + {"name": "random_int", "type": "integer"}, + {"name": "salary", "type": "float"}, + {"name": "price", "type": "float"}, + {"name": "is_active", "type": "boolean"}, + {"name": "uuid", "type": {"name": "string", "args": {"length": 36}}}, + {"name": "status", "type": {"name": "string", "args": {"length": 20}}}, + {"name": "priority", "type": {"name": "string", "args": {"length": 20}}}, + {"name": "credit_card", "type": {"name": "string", "args": {"length": 20}}}, + {"name": "credit_card_type", "type": {"name": "string", "args": {"length": 30}}}, + {"name": "currency", "type": {"name": "string", "args": {"length": 3}}}, + {"name": "currency_long", "type": {"name": "string", "args": {"length": 50}}}, + {"name": "bitcoin_address", "type": {"name": "string", "args": {"length": 50}}}, + {"name": "timestamp", "type": {"name": "string", "args": {"length": 50}}}, + {"name": "time", "type": {"name": "string", "args": {"length": 20}}}, + {"name": "year", "type": "integer"}, + {"name": "month", "type": "integer"}, + {"name": "weekday", "type": {"name": "string", "args": {"length": 20}}}, + {"name": "timezone", "type": {"name": "string", "args": {"length": 50}}}, + {"name": "country_code", "type": {"name": "string", "args": {"length": 2}}}, + {"name": "language", "type": {"name": "string", "args": {"length": 50}}}, + {"name": "locale", "type": {"name": "string", "args": {"length": 20}}}, + {"name": "color", "type": {"name": "string", "args": {"length": 30}}}, + {"name": "hex_color", "type": {"name": "string", "args": {"length": 7}}}, + {"name": "product_name", "type": {"name": "string", "args": {"length": 100}}}, + {"name": "product_category", "type": {"name": "string", "args": {"length": 50}}}, + {"name": "product_price", "type": "float"}, + {"name": "filename", "type": {"name": "string", "args": {"length": 100}}}, + {"name": "file_extension", "type": {"name": "string", "args": {"length": 10}}}, + {"name": "mime_type", "type": {"name": "string", "args": {"length": 50}}}, + {"name": "image_url", "type": {"name": "string", "args": {"length": 255}}}, + {"name": "user_agent", "type": {"name": "string", "args": {"length": 255}}}, + {"name": "book_title", "type": {"name": "string", "args": {"length": 100}}}, + {"name": "book_author", "type": {"name": "string", "args": {"length": 100}}}, + {"name": "book_genre", "type": {"name": "string", "args": {"length": 50}}}, + {"name": "movie_name", "type": {"name": "string", "args": {"length": 100}}}, + {"name": "animal", "type": {"name": "string", "args": {"length": 50}}}, + {"name": "pet_name", "type": {"name": "string", "args": {"length": 50}}}, + {"name": "cat", "type": {"name": "string", "args": {"length": 50}}}, + {"name": "dog", "type": {"name": "string", "args": {"length": 50}}}, + {"name": "fruit", "type": {"name": "string", "args": {"length": 50}}}, + {"name": "vegetable", "type": {"name": "string", "args": {"length": 50}}}, + {"name": "breakfast", "type": {"name": "string", "args": {"length": 100}}}, + {"name": "dessert", "type": {"name": "string", "args": {"length": 100}}}, + {"name": "drink", "type": {"name": "string", "args": {"length": 50}}}, + {"name": "car_maker", "type": {"name": "string", "args": {"length": 50}}}, + {"name": "car_model", "type": {"name": "string", "args": {"length": 50}}}, + {"name": "car_type", "type": {"name": "string", "args": {"length": 50}}}, + {"name": "ssn", "type": {"name": "string", "args": {"length": 11}}}, + {"name": "iban", "type": {"name": "string", "args": {"length": 34}}}, + {"name": "emoji", "type": {"name": "string", "args": {"length": 10}}}, + {"name": "quote", "type": {"name": "string", "args": {"length": 255}}}, + {"name": "phrase", "type": {"name": "string", "args": {"length": 100}}}, + {"name": "app_name", "type": {"name": "string", "args": {"length": 100}}}, + {"name": "app_version", "type": {"name": "string", "args": {"length": 20}}} + ] + } + ], + "populate": [ + { + "name": "comprehensive_test", + "count": 10, + "fields": [ + {"name": "first_name", "generator": "first_name"}, + {"name": "last_name", "generator": "last_name"}, + {"name": "full_name", "generator": "name"}, + {"name": "username", "generator": "user_name"}, + {"name": "email", "generator": "email"}, + {"name": "password", "generator": "password"}, + {"name": "gender", "generator": "gender"}, + {"name": "address", "generator": "address"}, + {"name": "street_address", "generator": "street_address"}, + {"name": "city", "generator": "city"}, + {"name": "state", "generator": "state"}, + {"name": "country", "generator": "country"}, + {"name": "postcode", "generator": "postcode"}, + {"name": "latitude", "generator": "latitude"}, + {"name": "longitude", "generator": "longitude"}, + {"name": "company", "generator": "company"}, + {"name": "job_title", "generator": "job"}, + {"name": "phone", "generator": "phone_number"}, + {"name": "url", "generator": "url"}, + {"name": "domain", "generator": "domain_name"}, + {"name": "ipv4", "generator": "ipv4"}, + {"name": "ipv6", "generator": "ipv6"}, + {"name": "mac_address", "generator": "mac_address"}, + {"name": "date_field", "generator": "date"}, + {"name": "past_date", "generator": "past_date"}, + {"name": "future_date", "generator": "future_date"}, + {"name": "word", "generator": "word"}, + {"name": "sentence", "generator": "sentence"}, + {"name": "paragraph", "generator": "paragraph"}, + {"name": "random_int", "generator": "random_int", "args": {"min": 1, "max": 1000}}, + {"name": "salary", "generator": "float", "args": {"min": 30000.0, "max": 150000.0}}, + {"name": "price", "generator": "decimal", "args": {"min": 9.99, "max": 999.99}}, + {"name": "is_active", "generator": "boolean"}, + {"name": "uuid", "generator": "uuid"}, + {"name": "status", "generator": "random_from", "args": ["active", "inactive", "pending", "suspended"]}, + {"name": "priority", "generator": "random_from", "args": ["low", "medium", "high", "urgent"]}, + {"name": "credit_card", "generator": "credit_card"}, + {"name": "credit_card_type", "generator": "credit_card_type"}, + {"name": "currency", "generator": "currency"}, + {"name": "currency_long", "generator": "currency_long"}, + {"name": "bitcoin_address", "generator": "bitcoin_address"}, + {"name": "timestamp", "generator": "timestamp"}, + {"name": "time", "generator": "time"}, + {"name": "year", "generator": "year"}, + {"name": "month", "generator": "month"}, + {"name": "weekday", "generator": "weekday"}, + {"name": "timezone", "generator": "timezone"}, + {"name": "country_code", "generator": "country_code"}, + {"name": "language", "generator": "language"}, + {"name": "locale", "generator": "locale"}, + {"name": "color", "generator": "color"}, + {"name": "hex_color", "generator": "hex_color"}, + {"name": "product_name", "generator": "product_name"}, + {"name": "product_category", "generator": "product_category"}, + {"name": "product_price", "generator": "price"}, + {"name": "filename", "generator": "filename"}, + {"name": "file_extension", "generator": "file_extension"}, + {"name": "mime_type", "generator": "mime_type"}, + {"name": "image_url", "generator": "image_url"}, + {"name": "user_agent", "generator": "user_agent"}, + {"name": "book_title", "generator": "book_title"}, + {"name": "book_author", "generator": "book_author"}, + {"name": "book_genre", "generator": "book_genre"}, + {"name": "movie_name", "generator": "movie_name"}, + {"name": "animal", "generator": "animal"}, + {"name": "pet_name", "generator": "pet_name"}, + {"name": "cat", "generator": "cat"}, + {"name": "dog", "generator": "dog"}, + {"name": "fruit", "generator": "fruit"}, + {"name": "vegetable", "generator": "vegetable"}, + {"name": "breakfast", "generator": "breakfast"}, + {"name": "dessert", "generator": "dessert"}, + {"name": "drink", "generator": "drink"}, + {"name": "car_maker", "generator": "car_maker"}, + {"name": "car_model", "generator": "car_model"}, + {"name": "car_type", "generator": "car_type"}, + {"name": "ssn", "generator": "ssn"}, + {"name": "iban", "generator": "iban"}, + {"name": "emoji", "generator": "emoji"}, + {"name": "quote", "generator": "quote"}, + {"name": "phrase", "generator": "phrase"}, + {"name": "app_name", "generator": "app_name"}, + {"name": "app_version", "generator": "app_version"} + ] + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 738ad93..76ee496 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,88 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- **Template Generator**: Custom generator for creating data patterns using templates + - Syntax: `{{generator|modifier1|modifier2}}` + - Supports all built-in generators (name, email, word, uuid, random_int, etc.) + - Modifiers: `upper`, `lower`, `title`, `trim`, `truncate(n)` + - Example: `"{{word|upper|truncate(3)}}-{{random_int(1000,9999)}}"` → `PRD-4821` + - Perfect for SKUs, employee IDs, license plates, order numbers + - See `docs/TEMPLATE_EXAMPLES.md` for comprehensive examples + - Enables custom data formats without writing code +- **Interactive Schema Generator**: New `-g` / `--generate` CLI flag for creating schemas + - Built directly into fakestack CLI (no separate script needed) + - 10 pre-built templates (Users, Employees, Products, Orders, Customers, Blog Posts, Inventory, Transactions, Students, Tasks) + - Support for all 6 database types + - Customizable database credentials and output filename + - Automatic row count suggestions per template + - Usage: `fakestack -g .` or `fakestack -g my-schema.json` + - Implementation in `golang/templates.go` for clean code organization +- **New Generators**: Added 80+ generators across 12 categories for comprehensive test data generation + - **Financial/Payment** (8): `credit_card`, `credit_card_type`, `credit_card_cvv`, `credit_card_exp`, `currency`, `currency_long`, `bitcoin_address`, `bitcoin_private_key` + - **Time & Timestamps** (7): `timestamp`, `time`, `year`, `month`, `month_string`, `weekday`, `timezone` + - **Localization** (4): `country_code`, `language`, `language_abbr`, `locale` + - **Product/E-commerce** (8): `color`, `hex_color`, `safe_color`, `product_name`, `product_category`, `product_description`, `product_feature`, `price` + - **Files & Media** (4): `filename`, `file_extension`, `mime_type`, `image_url` + - **User Agent & Browser** (5): `user_agent`, `chrome_user_agent`, `firefox_user_agent`, `safari_user_agent`, `opera_user_agent` + - **Books & Media** (5): `book_title`, `book_author`, `book_genre`, `movie_name`, `movie_genre` + - **Animals & Nature** (7): `animal`, `animal_type`, `pet_name`, `cat`, `dog`, `bird`, `farm_animal` + - **Food** (8): `fruit`, `vegetable`, `breakfast`, `lunch`, `dinner`, `snack`, `dessert`, `drink` + - **Vehicle/Transportation** (5): `car_maker`, `car_model`, `car_type`, `car_fuel_type`, `car_transmission_type` + - **Identifiers** (4): `ssn`, `ein`, `iban`, `routing_number` + - **Text Types** (6): `emoji`, `emoji_description`, `emoji_category`, `quote`, `phrase`, `question` + - **App & Software** (3): `app_name`, `app_version`, `app_author` + - **Range Support**: Added `integer`, `float`, and `decimal` generators with min/max range support + - `integer` / `random_int` - Generate integers with configurable min/max range + - `float` / `decimal` - Generate floating-point numbers with configurable min/max range + - Backward compatible with existing `random_int` generator + - Total generators: 116+ covering most common data generation needs +- **Database Support**: Added support for 3 additional database systems + - MariaDB - MySQL-compatible database with enhanced features + - MS SQL Server - Microsoft's enterprise database with IDENTITY syntax support + - CockroachDB - Distributed SQL database (PostgreSQL-compatible) +- **Manual Testing Workflow**: Created `.github/workflows/test-databases.yml` for on-demand database testing + - Tests all 6 supported databases in parallel + - Manual trigger via GitHub Actions UI (`workflow_dispatch`) + - Individual checkboxes for selective database testing (all selected by default) + - Docker-based testing for MySQL, PostgreSQL, MariaDB, MSSQL, SQLite, CockroachDB +- **Centralized Test Schema**: Created `.github/workflows/comprehensive-test-schema.json` + - Single source of truth for all database integration tests + - 84 columns covering all 116+ generators (83 fields + 1 auto-increment ID) + - Automatic field count validation to prevent missing generators + - Schema merging with database-specific configuration using `jq` + - Reduced test workflow from 1,477 to 432 lines (71% reduction) + - Ensures consistency across all 6 database tests + - See `.github/workflows/README.md` for documentation +- **Documentation**: Added comprehensive configuration examples and Docker setup for new databases +- **Version Selector**: Added version dropdown to documentation (powered by mike) +- **Uninstall Script**: Created `scripts/uninstall.sh` for removing fakestack from npm, pip, and Homebrew + +### Changed +- **Homebrew Tap**: Renamed from `homebrew-fakestack` to `homebrew-packages` for future tool support + - New installation: `brew tap 0xdps/packages && brew install fakestack` + - Prepares tap for additional tools beyond fakestack +- **Database Driver**: Added Microsoft SQL Server driver (`github.com/denisenkom/go-mssqldb`) +- **Documentation**: Updated all installation instructions to use new tap name + +### Fixed +- **MSSQL Compatibility**: Fixed SQL syntax issues for MS SQL Server + - Added square brackets `[table]` and `[column]` around identifiers + - Changed placeholders from `?` to `@p1, @p2, @p3` format + - Updated `CREATE TABLE` to use `IF OBJECT_ID` syntax + - Updated `DROP TABLE` to use `IF OBJECT_ID` syntax + - Fixed sqlcmd path in workflow to `/opt/mssql-tools18/bin/sqlcmd` with `-C` flag +- **Deprecation Fix**: Replaced deprecated `strings.Title()` with Unicode-compliant `cases.Title()` + - Added dependency: `golang.org/x/text v0.31.0` + - Fixed deprecation warning in Go 1.24.0 + - Improved Unicode handling for title casing + +### Technical Details +- Total supported databases: **6** (SQLite, MySQL, PostgreSQL, MariaDB, MSSQL, CockroachDB) +- MariaDB uses MySQL driver (drop-in compatible) +- CockroachDB uses PostgreSQL driver +- MSSQL implements IDENTITY autoincrement and NEWID() for random selection + ## [1.0.1] - 2025-11-17 ### Added @@ -180,7 +262,8 @@ npm install fakestack **Homebrew:** ```bash -brew install 0xdps/tap/fakestack +brew tap 0xdps/packages +brew install fakestack ``` ### Quick Start diff --git a/README.md b/README.md index 744e0a0..8012d28 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,9 @@ Generate databases from JSON schemas with realistic fake data. **10-50x faster** - 🚀 **Schema-Driven** - Define tables and data in simple JSON format - ⚡ **High Performance** - Go core delivers 10-50x speed improvement -- 💡 **Realistic Data** - 50+ generators for names, emails, addresses, dates, and more -- 🗄️ **Multi-Database** - Works with SQLite, MySQL, and PostgreSQL +- 💡 **Realistic Data** - 116+ generators covering financial, localization, products, animals, food, and more +- 🎨 **Custom Patterns** - Template generator for custom data formats (SKUs, IDs, codes) +- 🗄️ **Multi-Database** - Works with SQLite, MySQL, PostgreSQL, MariaDB, MSSQL, CockroachDB - 🎯 **Simple API** - Easy CLI and programmatic usage - 🌍 **Cross-Platform** - Linux, macOS, Windows (amd64 & arm64) - 📦 **Multi-Ecosystem** - Available on PyPI, npm, and Homebrew @@ -62,13 +63,33 @@ Pre-built binaries available on [GitHub Releases](https://github.com/0xdps/fake- ## 🚀 Quick Start -### 1. Download Example Schema +### 1. Generate a Schema (Interactive) + +Use the built-in interactive generator: ```bash -fakestack -d . +fakestack -g . +# or specify output filename +fakestack -g my-schema.json ``` -This creates a `schema.json` file in the current directory. +Choose from 10 pre-built templates: +- **Users** - Basic user management +- **Employees** - Employee records with departments +- **Products** - E-commerce products +- **Orders** - Order management system +- **Customers** - Customer database +- **Blog Posts** - Content management +- **Inventory** - Stock management +- **Transactions** - Financial records +- **Students** - Educational records +- **Tasks** - Task management + +Or download a basic example: + +```bash +fakestack -d . +``` ### 2. Create Tables and Populate Data @@ -98,7 +119,9 @@ Options: -c, --create-table Create database tables from schema -p, --populate-data Populate tables with fake data -f, --file Path to JSON schema file + -g, --generate Generate schema interactively (use '.' for default filename) -d, --download-schema Download example schema + -v, --version Show version information -h, --help Display help message ``` @@ -281,6 +304,33 @@ Create a `schema.json` file defining your database structure: } ``` +## 🎨 Custom Data Patterns + +Create custom data formats using the **template generator**: + +```json +{ + "name": "sku", + "generator": "template", + "args": { + "pattern": "{{word|upper|truncate(3)}}-{{random_int(1000,9999)}}" + } +} +``` +**Output**: `PRD-4821`, `INV-9234`, `STO-1456` + +**More Examples:** +```json +{"pattern": "EMP-{{random_int(10000,99999)}}"} // EMP-45678 +{"pattern": "{{uuid|upper|truncate(8)}}"} // A1B2C3D4 +{"pattern": "{{city}}, {{state}} {{postcode}}"} // San Francisco, CA 94102 +``` + +**Supported Modifiers**: `upper`, `lower`, `title`, `trim`, `truncate(n)` + +📚 **[Template Examples](docs/TEMPLATE_EXAMPLES.md)** - Comprehensive examples +📖 **[Custom Generators Guide](docs/CUSTOM_GENERATORS.md)** - Full guide with advanced patterns + ## 📚 Documentation 📖 **[Full Documentation on ReadTheDocs](https://fake-stack.readthedocs.io/)** - Complete documentation with examples and tutorials @@ -288,6 +338,7 @@ Create a `schema.json` file defining your database structure: - **[Getting Started](docs/getting-started.md)** - Installation and basic usage - **[Schema Reference](docs/schema-reference.md)** - Complete schema documentation - **[Data Generators](docs/generators.md)** - All available data generators +- **[Custom Generators](docs/CUSTOM_GENERATORS.md)** - Template generator guide - **[Database Support](docs/databases.md)** - Database-specific configuration - **[API Reference](docs/api-reference.md)** - Python and Node.js APIs - **[Examples](docs/examples.md)** - Real-world examples @@ -393,7 +444,7 @@ Built with: - 💬 **Discussions**: [GitHub Discussions](https://github.com/0xdps/fake-stack/discussions) - 📦 **PyPI**: https://pypi.org/project/fakestack/ - 📦 **npm**: https://www.npmjs.com/package/fakestack -- 🍺 **Homebrew**: `brew install 0xdps/fakestack` +- 🍺 **Homebrew**: `brew tap 0xdps/packages && brew install fakestack` ## 🔖 Changelog diff --git a/docs/.readthedocs.yaml b/docs/.readthedocs.yaml index af9f463..d410071 100644 --- a/docs/.readthedocs.yaml +++ b/docs/.readthedocs.yaml @@ -7,6 +7,9 @@ build: os: ubuntu-22.04 tools: python: "3.11" + commands: + - pip install mike + - mike deploy --push --update-aliases $READTHEDOCS_VERSION latest python: install: diff --git a/docs/api-reference.md b/docs/api-reference.md index 7bb9a0c..2577970 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -418,7 +418,8 @@ After installing via Homebrew: ```bash # Install -brew install 0xdps/fakestack +brew tap 0xdps/packages +brew install fakestack # Use fakestack schema.json diff --git a/docs/databases.md b/docs/databases.md index ccc9250..4779f12 100644 --- a/docs/databases.md +++ b/docs/databases.md @@ -1,6 +1,13 @@ # Database Support -Fakestack supports three major database systems with native drivers and optimized connections. +Fakestack supports **6 major database systems** with native drivers and optimized connections: + +- **SQLite** - Lightweight, file-based database +- **MySQL** - Popular open-source relational database +- **PostgreSQL** - Advanced open-source relational database +- **MariaDB** - MySQL-compatible database with enhanced features +- **MS SQL Server** - Microsoft's enterprise database system +- **CockroachDB** - Distributed SQL database (PostgreSQL-compatible) ## SQLite @@ -432,6 +439,196 @@ mysql+mysqlconnector://username:password@host:port/database postgresql+psycopg2://username:password@host:port/database ``` +## MariaDB + +### Overview + +MariaDB is a MySQL-compatible database with enhanced performance, security features, and additional storage engines. + +**Pros:** +- Drop-in MySQL replacement +- Better performance than MySQL in many workloads +- More storage engines (Aria, ColumnStore, etc.) +- Active open-source development + +**Cons:** +- Some MySQL features not available +- Less widely adopted than MySQL + +### Configuration + +```json +{ + "database": { + "dbtype": "mariadb", + "username": "root", + "password": "yourpassword", + "host": "localhost:3306", + "database": "testdb" + } +} +``` + +### Connection Options + +| Option | Required | Default | Description | +|--------|----------|---------|-------------| +| `dbtype` | Yes | - | Must be `"mariadb"` | +| `username` | Yes | - | Database user | +| `password` | Yes | - | User password | +| `host` | Yes | - | Host and port (e.g., `localhost:3306`) | +| `database` | Yes | - | Database name | + +### Usage Example + +```bash +# Create and populate +fakestack -c -p -f schema.json +``` + +**Schema example:** +```json +{ + "database": { + "dbtype": "mariadb", + "username": "root", + "password": "password", + "host": "localhost:3306", + "database": "mydb" + }, + "tables": [...], + "populate": [...] +} +``` + +## MS SQL Server + +### Overview + +Microsoft SQL Server is an enterprise-grade relational database management system with advanced analytics and integration capabilities. + +**Pros:** +- Enterprise features and support +- Excellent Windows integration +- Advanced analytics and BI tools +- Strong security features + +**Cons:** +- Primarily Windows-focused (Linux support improving) +- Licensing costs for production +- More resource-intensive + +### Configuration + +```json +{ + "database": { + "dbtype": "mssql", + "username": "sa", + "password": "YourPassword123!", + "host": "localhost:1433", + "database": "testdb" + } +} +``` + +### Connection Options + +| Option | Required | Default | Description | +|--------|----------|---------|-------------| +| `dbtype` | Yes | - | Must be `"mssql"` | +| `username` | Yes | - | Database user (typically `sa`) | +| `password` | Yes | - | Strong password required | +| `host` | Yes | - | Host and port (e.g., `localhost:1433`) | +| `database` | Yes | - | Database name | + +### Docker Setup + +```bash +docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=YourPassword123!' \ + -p 1433:1433 --name mssql \ + mcr.microsoft.com/mssql/server:2022-latest +``` + +### Usage Example + +```bash +fakestack -c -p -f mssql-schema.json +``` + +### Special Considerations + +- **Password requirements**: Must be strong (uppercase, lowercase, numbers, special chars) +- **IDENTITY columns**: Use `autoincrement: true` for auto-incrementing primary keys +- **Connection timeout**: May need longer timeout for first connection + +## CockroachDB + +### Overview + +CockroachDB is a distributed SQL database that is PostgreSQL-compatible and designed for cloud-native applications. + +**Pros:** +- Horizontal scalability +- Built-in replication and consistency +- PostgreSQL wire protocol compatibility +- Resilient to node failures + +**Cons:** +- More complex setup than traditional databases +- Different performance characteristics +- Some PostgreSQL features not supported + +### Configuration + +```json +{ + "database": { + "dbtype": "cockroachdb", + "username": "root", + "password": "", + "host": "localhost:26257", + "database": "testdb" + } +} +``` + +### Connection Options + +| Option | Required | Default | Description | +|--------|----------|---------|-------------| +| `dbtype` | Yes | - | Must be `"cockroachdb"` | +| `username` | Yes | - | Database user (default: `root`) | +| `password` | No | `""` | Password (empty for insecure mode) | +| `host` | Yes | - | Host and port (default: `26257`) | +| `database` | Yes | - | Database name | + +### Docker Setup + +```bash +# Start single-node cluster (insecure for testing) +docker run -d -p 26257:26257 -p 8080:8080 \ + --name cockroach \ + cockroachdb/cockroach:latest start-single-node --insecure + +# Create database +docker exec -it cockroach ./cockroach sql --insecure \ + --execute="CREATE DATABASE testdb;" +``` + +### Usage Example + +```bash +fakestack -c -p -f cockroachdb-schema.json +``` + +### Special Considerations + +- Uses PostgreSQL driver internally +- Default port is `26257` (not `5432`) +- Supports most PostgreSQL SQL syntax +- Better suited for distributed deployments + ## Troubleshooting ### SQLite diff --git a/docs/getting-started.md b/docs/getting-started.md index e9137c6..3e123a7 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -18,7 +18,8 @@ npm install fakestack ### Homebrew (macOS/Linux) ```bash -brew install 0xdps/fakestack +brew tap 0xdps/packages +brew install fakestack ``` ### Direct Binary Download diff --git a/docs/index.md b/docs/index.md index 9efab5c..fa467c3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -48,7 +48,8 @@ hide: --- ```bash - brew install 0xdps/fakestack + brew tap 0xdps/packages + brew install fakestack ``` [:octicons-arrow-right-24: Get started](getting-started.md) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index f566f15..1858ab6 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -91,6 +91,9 @@ plugins: - offline extra: + version: + provider: mike + default: latest social: - icon: fontawesome/brands/github link: https://github.com/0xdps/fake-stack diff --git a/docs/requirements.txt b/docs/requirements.txt index b6d5aa6..afbf304 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,4 @@ mkdocs>=1.5.0 mkdocs-material[imaging]>=9.4.0 pymdown-extensions>=10.3 +mike>=2.0.0 diff --git a/go.mod b/go.mod index 352cfb2..71a007a 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/0xdps/fake-stack -go 1.22 +go 1.24.0 require ( github.com/brianvoe/gofakeit/v7 v7.1.2 @@ -9,4 +9,11 @@ require ( github.com/mattn/go-sqlite3 v1.14.24 ) -require filippo.io/edwards25519 v1.1.0 // indirect +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/denisenkom/go-mssqldb v0.12.3 // indirect + github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect + github.com/golang-sql/sqlexp v0.1.0 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect + golang.org/x/text v0.31.0 // indirect +) diff --git a/go.sum b/go.sum index 074cd93..4a4f92b 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,50 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0= +github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8= github.com/brianvoe/gofakeit/v7 v7.1.2 h1:vSKaVScNhWVpf1rlyEKSvO8zKZfuDtGqoIHT//iNNb8= github.com/brianvoe/gofakeit/v7 v7.1.2/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw= +github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/golang/database.go b/golang/database.go index 4087ce7..b04135b 100644 --- a/golang/database.go +++ b/golang/database.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + _ "github.com/denisenkom/go-mssqldb" _ "github.com/go-sql-driver/mysql" _ "github.com/lib/pq" _ "github.com/mattn/go-sqlite3" @@ -46,12 +47,14 @@ func (db *Database) Close() error { // getDriver returns the SQL driver name func getDriver(dbType DbType) string { switch dbType { - case MySQL: + case MySQL, MariaDB: return "mysql" - case Postgres: + case Postgres, CockroachDB: return "postgres" case SQLite: return "sqlite3" + case MSSQL: + return "sqlserver" default: return "sqlite3" } @@ -60,14 +63,17 @@ func getDriver(dbType DbType) string { // buildConnectionString builds a database connection string func buildConnectionString(opts DbOptions) string { switch opts.DbType { - case MySQL: + case MySQL, MariaDB: return fmt.Sprintf("%s:%s@tcp(%s)/%s?parseTime=true", opts.Username, opts.Password, opts.Host, opts.Database) - case Postgres: + case Postgres, CockroachDB: return fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable", opts.Username, opts.Password, opts.Host, opts.Database) case SQLite: return opts.Database + case MSSQL: + return fmt.Sprintf("sqlserver://%s:%s@%s?database=%s", + opts.Username, opts.Password, opts.Host, opts.Database) default: return opts.Database } @@ -87,7 +93,12 @@ func (db *Database) CreateTables() error { // DropTables drops all tables defined in the schema func (db *Database) DropTables() error { for _, table := range db.schema.Tables { - query := fmt.Sprintf("DROP TABLE IF EXISTS %s", table.Name) + var query string + if db.driver == "sqlserver" { + query = fmt.Sprintf("IF OBJECT_ID('[%s]', 'U') IS NOT NULL DROP TABLE [%s]", table.Name, table.Name) + } else { + query = fmt.Sprintf("DROP TABLE IF EXISTS %s", table.Name) + } if _, err := db.conn.Exec(query); err != nil { return fmt.Errorf("failed to drop table %s: %w", table.Name, err) } @@ -105,8 +116,14 @@ func (db *Database) createTable(table DbTable) error { columns = append(columns, colDef) } - query := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (\n %s\n)", - table.Name, strings.Join(columns, ",\n ")) + var query string + if db.driver == "sqlserver" { + query = fmt.Sprintf("IF OBJECT_ID('[%s]', 'U') IS NULL CREATE TABLE [%s] (\n %s\n)", + table.Name, table.Name, strings.Join(columns, ",\n ")) + } else { + query = fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (\n %s\n)", + table.Name, strings.Join(columns, ",\n ")) + } _, err := db.conn.Exec(query) return err @@ -115,7 +132,11 @@ func (db *Database) createTable(table DbTable) error { // buildColumnDefinition builds a SQL column definition func (db *Database) buildColumnDefinition(col ParsedTableColumn) string { sqlType := db.getSQLType(col.Type) - def := fmt.Sprintf("%s %s", col.Name, sqlType) + colName := col.Name + if db.driver == "sqlserver" { + colName = fmt.Sprintf("[%s]", col.Name) + } + def := fmt.Sprintf("%s %s", colName, sqlType) // Handle options if primary, ok := col.Options["primary_key"].(bool); ok && primary { @@ -125,8 +146,10 @@ func (db *Database) buildColumnDefinition(col ParsedTableColumn) string { def += " AUTOINCREMENT" } else if db.driver == "mysql" { def += " AUTO_INCREMENT" + } else if db.driver == "sqlserver" { + def += " IDENTITY(1,1)" } else { - // PostgreSQL uses SERIAL or BIGSERIAL + // PostgreSQL and CockroachDB use SERIAL or BIGSERIAL def = strings.Replace(def, sqlType, "SERIAL", 1) } } @@ -189,18 +212,30 @@ func (db *Database) Insert(tableName string, data map[string]interface{}) error i := 1 for col, val := range data { - columns = append(columns, col) - if db.driver == "postgres" { - placeholders = append(placeholders, fmt.Sprintf("$%d", i)) + if db.driver == "sqlserver" { + columns = append(columns, fmt.Sprintf("[%s]", col)) + placeholders = append(placeholders, fmt.Sprintf("@p%d", i)) } else { - placeholders = append(placeholders, "?") + columns = append(columns, col) + if db.driver == "postgres" { + placeholders = append(placeholders, fmt.Sprintf("$%d", i)) + } else { + placeholders = append(placeholders, "?") + } } values = append(values, val) i++ } + var tableName_quoted string + if db.driver == "sqlserver" { + tableName_quoted = fmt.Sprintf("[%s]", tableName) + } else { + tableName_quoted = tableName + } + query := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)", - tableName, + tableName_quoted, strings.Join(columns, ", "), strings.Join(placeholders, ", ")) @@ -211,13 +246,20 @@ func (db *Database) Insert(tableName string, data map[string]interface{}) error // GetRandomValue gets a random value from a column func (db *Database) GetRandomValue(tableName, columnName string) (interface{}, error) { var query string + tableQuoted := tableName + columnQuoted := columnName + if db.driver == "sqlserver" { + tableQuoted = fmt.Sprintf("[%s]", tableName) + columnQuoted = fmt.Sprintf("[%s]", columnName) + } + switch db.driver { - case "sqlite3": - query = fmt.Sprintf("SELECT %s FROM %s ORDER BY RANDOM() LIMIT 1", columnName, tableName) + case "sqlite3", "postgres": + query = fmt.Sprintf("SELECT %s FROM %s ORDER BY RANDOM() LIMIT 1", columnQuoted, tableQuoted) case "mysql": - query = fmt.Sprintf("SELECT %s FROM %s ORDER BY RAND() LIMIT 1", columnName, tableName) - case "postgres": - query = fmt.Sprintf("SELECT %s FROM %s ORDER BY RANDOM() LIMIT 1", columnName, tableName) + query = fmt.Sprintf("SELECT %s FROM %s ORDER BY RAND() LIMIT 1", columnQuoted, tableQuoted) + case "sqlserver": + query = fmt.Sprintf("SELECT TOP 1 %s FROM %s ORDER BY NEWID()", columnQuoted, tableQuoted) } var value interface{} diff --git a/golang/generator.go b/golang/generator.go index f9d6f84..6895ad3 100644 --- a/golang/generator.go +++ b/golang/generator.go @@ -5,6 +5,8 @@ import ( "strings" "github.com/brianvoe/gofakeit/v7" + "golang.org/x/text/cases" + "golang.org/x/text/language" ) // Generator handles fake data generation @@ -109,7 +111,7 @@ func (g *Generator) Generate(field PopulateField, commons map[string]interface{} return g.fake.Word(), nil // Numbers - case "random_int": + case "integer", "random_int": min, max := 0, 100 if argsMap, ok := args.(map[string]interface{}); ok { if minVal, ok := argsMap["min"].(float64); ok { @@ -120,6 +122,17 @@ func (g *Generator) Generate(field PopulateField, commons map[string]interface{} } } return g.fake.IntRange(min, max), nil + case "float", "decimal": + min, max := 0.0, 100.0 + if argsMap, ok := args.(map[string]interface{}); ok { + if minVal, ok := argsMap["min"].(float64); ok { + min = minVal + } + if maxVal, ok := argsMap["max"].(float64); ok { + max = maxVal + } + } + return g.fake.Float64Range(min, max), nil case "random_digit": return g.fake.Digit(), nil case "random_number": @@ -137,6 +150,180 @@ func (g *Generator) Generate(field PopulateField, commons map[string]interface{} case "uuid": return g.fake.UUID(), nil + // Financial/Payment + case "credit_card": + return g.fake.CreditCardNumber(nil), nil + case "credit_card_type": + return g.fake.CreditCardType(), nil + case "credit_card_cvv": + return g.fake.CreditCardCvv(), nil + case "credit_card_exp": + return g.fake.CreditCardExp(), nil + case "currency": + return g.fake.CurrencyShort(), nil + case "currency_long": + return g.fake.CurrencyLong(), nil + case "bitcoin_address": + return g.fake.BitcoinAddress(), nil + case "bitcoin_private_key": + return g.fake.BitcoinPrivateKey(), nil + + // Time & Timestamps + case "timestamp": + return g.fake.Date().Unix(), nil + case "time": + return g.fake.Date().Format("15:04:05"), nil + case "year": + return g.fake.Year(), nil + case "month": + return g.fake.Month(), nil + case "month_string": + return g.fake.MonthString(), nil + case "weekday": + return g.fake.WeekDay(), nil + case "timezone": + return g.fake.TimeZone(), nil + + // Localization + case "country_code": + return g.fake.CountryAbr(), nil + case "language": + return g.fake.Language(), nil + case "language_abbr": + return g.fake.LanguageAbbreviation(), nil + case "locale": + return g.fake.Language() + "_" + g.fake.CountryAbr(), nil + + // Product/E-commerce + case "color": + return g.fake.Color(), nil + case "hex_color": + return g.fake.HexColor(), nil + case "safe_color": + return g.fake.SafeColor(), nil + case "product_name": + return g.fake.ProductName(), nil + case "product_category": + return g.fake.ProductCategory(), nil + case "product_description": + return g.fake.ProductDescription(), nil + case "product_feature": + return g.fake.ProductFeature(), nil + case "price": + return g.fake.Price(1.0, 1000.0), nil + + // Files & Media + case "filename": + return g.fake.Word() + "." + g.fake.FileExtension(), nil + case "file_extension": + return g.fake.FileExtension(), nil + case "mime_type": + return g.fake.FileMimeType(), nil + case "image_url": + return fmt.Sprintf("https://picsum.photos/%d/%d", 400, 300), nil + + // User Agent & Browser + case "user_agent": + return g.fake.UserAgent(), nil + case "chrome_user_agent": + return g.fake.ChromeUserAgent(), nil + case "firefox_user_agent": + return g.fake.FirefoxUserAgent(), nil + case "safari_user_agent": + return g.fake.SafariUserAgent(), nil + case "opera_user_agent": + return g.fake.OperaUserAgent(), nil + + // Books & Media + case "book_title": + return g.fake.BookTitle(), nil + case "book_author": + return g.fake.BookAuthor(), nil + case "book_genre": + return g.fake.BookGenre(), nil + case "movie_name": + return g.fake.MovieName(), nil + case "movie_genre": + return g.fake.MovieGenre(), nil + + // Animals & Nature + case "animal": + return g.fake.Animal(), nil + case "animal_type": + return g.fake.AnimalType(), nil + case "pet_name": + return g.fake.PetName(), nil + case "cat": + return g.fake.Cat(), nil + case "dog": + return g.fake.Dog(), nil + case "bird": + return g.fake.Bird(), nil + case "farm_animal": + return g.fake.FarmAnimal(), nil + + // Food + case "fruit": + return g.fake.Fruit(), nil + case "vegetable": + return g.fake.Vegetable(), nil + case "breakfast": + return g.fake.Breakfast(), nil + case "lunch": + return g.fake.Lunch(), nil + case "dinner": + return g.fake.Dinner(), nil + case "snack": + return g.fake.Snack(), nil + case "dessert": + return g.fake.Dessert(), nil + case "drink": + return g.fake.Drink(), nil + + // Vehicle/Transportation + case "car_maker": + return g.fake.CarMaker(), nil + case "car_model": + return g.fake.CarModel(), nil + case "car_type": + return g.fake.CarType(), nil + case "car_fuel_type": + return g.fake.CarFuelType(), nil + case "car_transmission_type": + return g.fake.CarTransmissionType(), nil + + // Identifiers + case "ssn": + return g.fake.SSN(), nil + case "ein": + return g.fake.Cusip(), nil + case "iban": + return g.fake.AchAccount(), nil + case "routing_number": + return g.fake.AchRouting(), nil + + // Additional Text Types + case "emoji": + return g.fake.Emoji(), nil + case "emoji_description": + return g.fake.EmojiDescription(), nil + case "emoji_category": + return g.fake.EmojiCategory(), nil + case "quote": + return g.fake.Quote(), nil + case "phrase": + return g.fake.Phrase(), nil + case "question": + return g.fake.Question(), nil + + // App & Software + case "app_name": + return g.fake.AppName(), nil + case "app_version": + return g.fake.AppVersion(), nil + case "app_author": + return g.fake.AppAuthor(), nil + // Custom generators case "person": return g.generatePerson() @@ -148,6 +335,8 @@ func (g *Generator) Generate(field PopulateField, commons map[string]interface{} return g.uniqueItem(field.Name, args) case "db_random_item": return g.dbRandomItem(args) + case "template": + return g.generateFromTemplate(args, commons) default: return nil, fmt.Errorf("unknown generator: %s", generator) @@ -278,3 +467,199 @@ func (g *Generator) dbRandomItem(args interface{}) (interface{}, error) { func (g *Generator) ClearUniques() { g.uniques = make(map[string]map[interface{}]bool) } + +// generateFromTemplate generates value from a template pattern +func (g *Generator) generateFromTemplate(args interface{}, commons map[string]interface{}) (interface{}, error) { + argsMap, ok := args.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("template generator requires args object with 'pattern' field") + } + + pattern, ok := argsMap["pattern"].(string) + if !ok { + return nil, fmt.Errorf("template generator requires 'pattern' string") + } + + return g.parseTemplate(pattern, commons) +} + +// parseTemplate parses and executes template pattern +func (g *Generator) parseTemplate(pattern string, commons map[string]interface{}) (string, error) { + result := pattern + + // Find all {{...}} placeholders + for { + start := strings.Index(result, "{{") + if start == -1 { + break + } + + end := strings.Index(result[start:], "}}") + if end == -1{ + return "", fmt.Errorf("unclosed template placeholder in: %s", pattern) + } + end += start + + // Extract placeholder content + placeholder := result[start+2 : end] + + // Parse placeholder: generator|modifier1|modifier2 + parts := strings.Split(placeholder, "|") + generatorExpr := strings.TrimSpace(parts[0]) + modifiers := parts[1:] + + // Generate value + value, err := g.executeTemplateGenerator(generatorExpr, commons) + if err != nil { + return "", fmt.Errorf("failed to execute generator '%s': %w", generatorExpr, err) + } + + // Apply modifiers + valueStr := fmt.Sprintf("%v", value) + for _, modifier := range modifiers { + valueStr = g.applyModifier(valueStr, strings.TrimSpace(modifier)) + } + + // Replace placeholder with generated value + result = result[:start] + valueStr + result[end+2:] + } + + return result, nil +} + +// executeTemplateGenerator executes a generator expression +func (g *Generator) executeTemplateGenerator(expr string, commons map[string]interface{}) (interface{}, error) { + // Parse generator name and arguments + // Format: generator_name or generator_name(arg1,arg2) + + parenIndex := strings.Index(expr, "(") + if parenIndex == -1 { + // No arguments, simple generator + return g.executeSimpleGenerator(expr) + } + + // Has arguments + generatorName := strings.TrimSpace(expr[:parenIndex]) + argsStr := expr[parenIndex+1:] + + // Remove closing parenthesis + if !strings.HasSuffix(argsStr, ")") { + return nil, fmt.Errorf("missing closing parenthesis in: %s", expr) + } + argsStr = argsStr[:len(argsStr)-1] + + // Parse arguments + args := g.parseTemplateArgs(argsStr) + + return g.executeGeneratorWithArgs(generatorName, args) +} + +// executeSimpleGenerator executes a generator without arguments +func (g *Generator) executeSimpleGenerator(name string) (interface{}, error) { + switch name { + case "name": + return g.fake.Name(), nil + case "first_name": + return g.fake.FirstName(), nil + case "last_name": + return g.fake.LastName(), nil + case "user_name", "username": + return g.fake.Username(), nil + case "email": + return g.fake.Email(), nil + case "word": + return g.fake.Word(), nil + case "sentence": + return g.fake.Sentence(10), nil + case "city": + return g.fake.Address().City, nil + case "state": + return g.fake.Address().State, nil + case "country": + return g.fake.Address().Country, nil + case "postcode", "zip_code": + return g.fake.Address().Zip, nil + case "company": + return g.fake.Company(), nil + case "uuid": + return g.fake.UUID(), nil + case "phone": + return g.fake.Phone(), nil + case "date": + return g.fake.Date(), nil + default: + return nil, fmt.Errorf("unknown generator: %s", name) + } +} + +// executeGeneratorWithArgs executes a generator with arguments +func (g *Generator) executeGeneratorWithArgs(name string, args []string) (interface{}, error) { + switch name { + case "random_int": + if len(args) != 2 { + return nil, fmt.Errorf("random_int requires 2 arguments: min, max") + } + min, max := 0, 100 + fmt.Sscanf(args[0], "%d", &min) + fmt.Sscanf(args[1], "%d", &max) + return g.fake.IntRange(min, max), nil + case "truncate": + // Special case: handled as modifier + return nil, fmt.Errorf("truncate should be used as modifier, not generator") + default: + return g.executeSimpleGenerator(name) + } +} + +// parseTemplateArgs parses comma-separated arguments +func (g *Generator) parseTemplateArgs(argsStr string) []string { + if argsStr == "" { + return []string{} + } + + parts := strings.Split(argsStr, ",") + args := make([]string, len(parts)) + for i, part := range parts { + args[i] = strings.TrimSpace(part) + } + return args +} + +// applyModifier applies a modifier to a string value +func (g *Generator) applyModifier(value, modifier string) string { + // Check if modifier has arguments: modifier(arg) + parenIndex := strings.Index(modifier, "(") + if parenIndex != -1 { + modName := modifier[:parenIndex] + argStr := modifier[parenIndex+1 : len(modifier)-1] + + switch modName { + case "truncate": + var length int + fmt.Sscanf(argStr, "%d", &length) + if length > 0 && len(value) > length { + return value[:length] + } + return value + case "format": + // Date formatting - simplified + return value + } + return value + } + + // Simple modifiers + switch modifier { + case "upper": + return strings.ToUpper(value) + case "lower": + return strings.ToLower(value) + case "title": + caser := cases.Title(language.English) + return caser.String(strings.ToLower(value)) + case "trim": + return strings.TrimSpace(value) + default: + return value + } +} diff --git a/golang/main.go b/golang/main.go index 2e1455e..f0c79eb 100644 --- a/golang/main.go +++ b/golang/main.go @@ -78,6 +78,9 @@ func main() { downloadSchema := flag.String("d", "", "download example schema to specified path (use '.' for current directory)") downloadSchemaLong := flag.String("download-schema", "", "download example schema to specified path") + generateSchema := flag.String("g", "", "generate schema interactively (specify output filename or use '.' for default)") + generateSchemaLong := flag.String("generate", "", "generate schema interactively (specify output filename)") + versionFlag := flag.Bool("v", false, "show version information") versionFlagLong := flag.Bool("version", false, "show version information") @@ -107,6 +110,23 @@ func main() { if download == "" { download = *downloadSchemaLong } + generate := *generateSchema + if generate == "" { + generate = *generateSchemaLong + } + + // Handle generate schema (interactive) + if generate != "" { + outputFile := "" + if generate != "." { + outputFile = generate + } + if err := RunInteractiveGenerator(outputFile); err != nil { + fmt.Fprintf(os.Stderr, "Error generating schema: %v\n", err) + os.Exit(1) + } + return + } // Handle download schema if download != "" { @@ -121,7 +141,7 @@ func main() { // Validate arguments if !create && !populate { fmt.Fprintf(os.Stderr, "Error: no arguments provided!!!\n") - fmt.Fprintf(os.Stderr, "Use -c to create tables, -p to populate data, or -d to download example schema\n") + fmt.Fprintf(os.Stderr, "Use -c to create tables, -p to populate data, -g to generate schema, or -d to download example\n") flag.Usage() os.Exit(1) } diff --git a/golang/schema.go b/golang/schema.go index d432854..9b24926 100644 --- a/golang/schema.go +++ b/golang/schema.go @@ -10,9 +10,12 @@ import ( type DbType string const ( - MySQL DbType = "mysql" - Postgres DbType = "psql" - SQLite DbType = "sqlite" + MySQL DbType = "mysql" + Postgres DbType = "psql" + SQLite DbType = "sqlite" + MSSQL DbType = "mssql" + MariaDB DbType = "mariadb" + CockroachDB DbType = "cockroachdb" ) // DbOptions represents database connection configuration diff --git a/golang/templates.go b/golang/templates.go new file mode 100644 index 0000000..0eccd77 --- /dev/null +++ b/golang/templates.go @@ -0,0 +1,542 @@ +package main + +import ( +"bufio" +"fmt" +"os" +"strconv" +"strings" +) + +// SchemaTemplate represents a pre-built schema template +type SchemaTemplate struct { + Name string + Description string + TableName string + RowCount int + Generator func(dbConfig DatabaseConfig) string +} + +// DatabaseConfig holds database configuration +type DatabaseConfig struct { + DBType string + Username string + Password string + Host string + Port string + Database string +} + +// RunInteractiveGenerator runs the interactive schema generator +func RunInteractiveGenerator(outputFile string) error { + reader := bufio.NewReader(os.Stdin) + + fmt.Println("========================================") + fmt.Println(" Fakestack Schema Generator") + fmt.Println("========================================") + fmt.Println() + + // Ask for database type + fmt.Println("Select database type:") + fmt.Println("1) SQLite") + fmt.Println("2) MySQL") + fmt.Println("3) PostgreSQL") + fmt.Println("4) MariaDB") + fmt.Println("5) MS SQL Server") + fmt.Println("6) CockroachDB") + fmt.Print("Enter your choice (1-6): ") + + dbChoice, _ := reader.ReadString('\n') + dbChoice = strings.TrimSpace(dbChoice) + + config := DatabaseConfig{} + switch dbChoice { + case "1": + config.DBType = "sqlite" + case "2": + config.DBType = "mysql" + case "3": + config.DBType = "postgres" + case "4": + config.DBType = "mariadb" + case "5": + config.DBType = "mssql" + case "6": + config.DBType = "cockroachdb" + default: + return fmt.Errorf("invalid database choice") + } + + // Ask for credentials (skip for SQLite) + if config.DBType != "sqlite" { + fmt.Println() + fmt.Print("Enter database username (default: root): ") + config.Username, _ = reader.ReadString('\n') + config.Username = strings.TrimSpace(config.Username) + if config.Username == "" { + config.Username = "root" + } + + fmt.Print("Enter database password (default: password): ") + config.Password, _ = reader.ReadString('\n') + config.Password = strings.TrimSpace(config.Password) + if config.Password == "" { + config.Password = "password" + } + + fmt.Print("Enter database host (default: localhost): ") + config.Host, _ = reader.ReadString('\n') + config.Host = strings.TrimSpace(config.Host) + if config.Host == "" { + config.Host = "localhost" + } + + // Set default ports + defaultPort := "3306" + switch config.DBType { + case "mysql", "mariadb": + defaultPort = "3306" + case "postgres": + defaultPort = "5432" + case "mssql": + defaultPort = "1433" + case "cockroachdb": + defaultPort = "26257" + } + + fmt.Printf("Enter database port (default: %s): ", defaultPort) + config.Port, _ = reader.ReadString('\n') + config.Port = strings.TrimSpace(config.Port) + if config.Port == "" { + config.Port = defaultPort + } + + fmt.Print("Enter database name (default: testdb): ") + config.Database, _ = reader.ReadString('\n') + config.Database = strings.TrimSpace(config.Database) + if config.Database == "" { + config.Database = "testdb" + } + } else { + config.Database = "test.db" + } + + // Ask for schema template + fmt.Println() + fmt.Println("Select schema template:") + templates := GetSchemaTemplates() + for i, tmpl := range templates { + fmt.Printf("%d) %s - %s\n", i+1, tmpl.Name, tmpl.Description) + } + fmt.Printf("Enter your choice (1-%d): ", len(templates)) + + schemaChoice, _ := reader.ReadString('\n') + schemaChoice = strings.TrimSpace(schemaChoice) + schemaIdx, err := strconv.Atoi(schemaChoice) + if err != nil || schemaIdx < 1 || schemaIdx > len(templates) { + return fmt.Errorf("invalid schema choice") + } + + selectedTemplate := templates[schemaIdx-1] + + // Ask for output filename if not provided + if outputFile == "" { + fmt.Println() + fmt.Print("Enter output filename (default: schema.json): ") + outputFile, _ = reader.ReadString('\n') + outputFile = strings.TrimSpace(outputFile) + if outputFile == "" { + outputFile = "schema.json" + } + } + + // Generate schema + schemaContent := selectedTemplate.Generator(config) + + // Write to file + if err := os.WriteFile(outputFile, []byte(schemaContent), 0644); err != nil { + return fmt.Errorf("failed to write schema file: %w", err) + } + + fmt.Println() + fmt.Println("✓ Schema file generated successfully!") + fmt.Printf("File: %s\n", outputFile) + fmt.Printf("Database: %s\n", config.DBType) + fmt.Printf("Table: %s\n", selectedTemplate.TableName) + fmt.Printf("Rows: %d\n", selectedTemplate.RowCount) + fmt.Println() + fmt.Println("Next steps:") + fmt.Printf(" 1. Create tables: fakestack -c -f %s\n", outputFile) + fmt.Printf(" 2. Populate data: fakestack -p -f %s\n", outputFile) + fmt.Printf(" 3. Or do both: fakestack -c -p -f %s\n", outputFile) + fmt.Println() + + return nil +} + +// GetSchemaTemplates returns all available schema templates +func GetSchemaTemplates() []SchemaTemplate { + return []SchemaTemplate{ + {Name: "Users", Description: "Basic user management (id, username, email, created_at)", TableName: "users", RowCount: 50, Generator: generateUsersSchema}, + {Name: "Employees", Description: "Employee records (id, first_name, last_name, email, department, salary, hire_date)", TableName: "employees", RowCount: 100, Generator: generateEmployeesSchema}, + {Name: "Products", Description: "E-commerce products (id, name, description, price, stock, category)", TableName: "products", RowCount: 200, Generator: generateProductsSchema}, + {Name: "Orders", Description: "Order management (id, order_number, customer_email, total_amount, status, order_date)", TableName: "orders", RowCount: 500, Generator: generateOrdersSchema}, + {Name: "Customers", Description: "Customer database (id, name, email, phone, address, city, country)", TableName: "customers", RowCount: 150, Generator: generateCustomersSchema}, + {Name: "Blog Posts", Description: "Content management (id, title, content, author, published_at, views)", TableName: "posts", RowCount: 100, Generator: generateBlogPostsSchema}, + {Name: "Inventory", Description: "Stock management (id, item_name, sku, quantity, location, last_updated)", TableName: "inventory", RowCount: 300, Generator: generateInventorySchema}, + {Name: "Transactions", Description: "Financial records (id, transaction_id, amount, type, status, timestamp)", TableName: "transactions", RowCount: 1000, Generator: generateTransactionsSchema}, + {Name: "Students", Description: "Educational records (id, student_id, name, email, grade, enrollment_date)", TableName: "students", RowCount: 200, Generator: generateStudentsSchema}, + {Name: "Tasks", Description: "Task management (id, title, description, priority, status, due_date, assignee)", TableName: "tasks", RowCount: 150, Generator: generateTasksSchema}, + } +} + +// Helper function to build database config JSON +func buildDatabaseConfigJSON(config DatabaseConfig) string { + if config.DBType == "sqlite" { + return fmt.Sprintf(` "database": { + "dbtype": "%s", + "database": "%s" + }`, config.DBType, config.Database) + } + hostWithPort := fmt.Sprintf("%s:%s", config.Host, config.Port) + return fmt.Sprintf(` "database": { + "dbtype": "%s", + "username": "%s", + "password": "%s", + "host": "%s", + "database": "%s" + }`, config.DBType, config.Username, config.Password, hostWithPort, config.Database) +} + +// Schema generator functions +func generateUsersSchema(config DatabaseConfig) string { + return fmt.Sprintf(`{ +%s, + "tables": [ + { + "name": "users", + "columns": [ + {"name": "id", "type": "integer", "options": {"primary_key": true, "autoincrement": true}}, + {"name": "username", "type": {"name": "string", "args": {"length": 50}}, "options": {"nullable": false, "unique": true}}, + {"name": "email", "type": {"name": "string", "args": {"length": 100}}, "options": {"nullable": false, "unique": true}}, + {"name": "created_at", "type": "datetime", "options": {"nullable": false}} + ] + } + ], + "populate": [ + { + "name": "users", + "count": 50, + "fields": [ + {"name": "username", "generator": "user_name"}, + {"name": "email", "generator": "email"}, + {"name": "created_at", "generator": "past_date"} + ] + } + ] +} +`, buildDatabaseConfigJSON(config)) +} + +func generateEmployeesSchema(config DatabaseConfig) string { + return fmt.Sprintf(`{ +%s, + "tables": [ + { + "name": "employees", + "columns": [ + {"name": "id", "type": "integer", "options": {"primary_key": true, "autoincrement": true}}, + {"name": "first_name", "type": {"name": "string", "args": {"length": 50}}, "options": {"nullable": false}}, + {"name": "last_name", "type": {"name": "string", "args": {"length": 50}}, "options": {"nullable": false}}, + {"name": "email", "type": {"name": "string", "args": {"length": 100}}, "options": {"nullable": false, "unique": true}}, + {"name": "department", "type": {"name": "string", "args": {"length": 50}}, "options": {"nullable": false}}, + {"name": "salary", "type": {"name": "decimal", "args": {"precision": 10, "scale": 2}}, "options": {"nullable": false}}, + {"name": "hire_date", "type": "date", "options": {"nullable": false}} + ] + } + ], + "populate": [ + { + "name": "employees", + "count": 100, + "fields": [ + {"name": "first_name", "generator": "first_name"}, + {"name": "last_name", "generator": "last_name"}, + {"name": "email", "generator": "email"}, + {"name": "department", "generator": "random_from", "args": ["Engineering", "Sales", "Marketing", "HR", "Finance", "Operations"]}, + {"name": "salary", "generator": "float", "args": {"min": 40000, "max": 150000}}, + {"name": "hire_date", "generator": "past_date"} + ] + } + ] +} +`, buildDatabaseConfigJSON(config)) +} + +func generateProductsSchema(config DatabaseConfig) string { + return fmt.Sprintf(`{ +%s, + "tables": [ + { + "name": "products", + "columns": [ + {"name": "id", "type": "integer", "options": {"primary_key": true, "autoincrement": true}}, + {"name": "name", "type": {"name": "string", "args": {"length": 100}}, "options": {"nullable": false}}, + {"name": "description", "type": "text", "options": {"nullable": true}}, + {"name": "price", "type": {"name": "decimal", "args": {"precision": 10, "scale": 2}}, "options": {"nullable": false}}, + {"name": "stock", "type": "integer", "options": {"nullable": false}}, + {"name": "category", "type": {"name": "string", "args": {"length": 50}}, "options": {"nullable": false}} + ] + } + ], + "populate": [ + { + "name": "products", + "count": 200, + "fields": [ + {"name": "name", "generator": "word"}, + {"name": "description", "generator": "sentence", "args": {"word_count": 10}}, + {"name": "price", "generator": "float", "args": {"min": 9.99, "max": 999.99}}, + {"name": "stock", "generator": "integer", "args": {"min": 0, "max": 500}}, + {"name": "category", "generator": "random_from", "args": ["Electronics", "Clothing", "Books", "Home & Garden", "Toys", "Sports"]} + ] + } + ] +} +`, buildDatabaseConfigJSON(config)) +} + +func generateOrdersSchema(config DatabaseConfig) string { + return fmt.Sprintf(`{ +%s, + "tables": [ + { + "name": "orders", + "columns": [ + {"name": "id", "type": "integer", "options": {"primary_key": true, "autoincrement": true}}, + {"name": "order_number", "type": {"name": "string", "args": {"length": 50}}, "options": {"nullable": false, "unique": true}}, + {"name": "customer_email", "type": {"name": "string", "args": {"length": 100}}, "options": {"nullable": false}}, + {"name": "total_amount", "type": {"name": "decimal", "args": {"precision": 10, "scale": 2}}, "options": {"nullable": false}}, + {"name": "status", "type": {"name": "string", "args": {"length": 20}}, "options": {"nullable": false}}, + {"name": "order_date", "type": "datetime", "options": {"nullable": false}} + ] + } + ], + "populate": [ + { + "name": "orders", + "count": 500, + "fields": [ + {"name": "order_number", "generator": "uuid"}, + {"name": "customer_email", "generator": "email"}, + {"name": "total_amount", "generator": "float", "args": {"min": 10.00, "max": 1000.00}}, + {"name": "status", "generator": "random_from", "args": ["pending", "processing", "shipped", "delivered", "cancelled"]}, + {"name": "order_date", "generator": "past_date"} + ] + } + ] +} +`, buildDatabaseConfigJSON(config)) +} + +func generateCustomersSchema(config DatabaseConfig) string { + return fmt.Sprintf(`{ +%s, + "tables": [ + { + "name": "customers", + "columns": [ + {"name": "id", "type": "integer", "options": {"primary_key": true, "autoincrement": true}}, + {"name": "name", "type": {"name": "string", "args": {"length": 100}}, "options": {"nullable": false}}, + {"name": "email", "type": {"name": "string", "args": {"length": 100}}, "options": {"nullable": false, "unique": true}}, + {"name": "phone", "type": {"name": "string", "args": {"length": 20}}, "options": {"nullable": true}}, + {"name": "address", "type": {"name": "string", "args": {"length": 200}}, "options": {"nullable": true}}, + {"name": "city", "type": {"name": "string", "args": {"length": 50}}, "options": {"nullable": true}}, + {"name": "country", "type": {"name": "string", "args": {"length": 50}}, "options": {"nullable": false}} + ] + } + ], + "populate": [ + { + "name": "customers", + "count": 150, + "fields": [ + {"name": "name", "generator": "name"}, + {"name": "email", "generator": "email"}, + {"name": "phone", "generator": "phone_number"}, + {"name": "address", "generator": "address"}, + {"name": "city", "generator": "city"}, + {"name": "country", "generator": "country"} + ] + } + ] +} +`, buildDatabaseConfigJSON(config)) +} + +func generateBlogPostsSchema(config DatabaseConfig) string { + return fmt.Sprintf(`{ +%s, + "tables": [ + { + "name": "posts", + "columns": [ + {"name": "id", "type": "integer", "options": {"primary_key": true, "autoincrement": true}}, + {"name": "title", "type": {"name": "string", "args": {"length": 200}}, "options": {"nullable": false}}, + {"name": "content", "type": "text", "options": {"nullable": false}}, + {"name": "author", "type": {"name": "string", "args": {"length": 100}}, "options": {"nullable": false}}, + {"name": "published_at", "type": "datetime", "options": {"nullable": false}}, + {"name": "views", "type": "integer", "options": {"nullable": false}} + ] + } + ], + "populate": [ + { + "name": "posts", + "count": 100, + "fields": [ + {"name": "title", "generator": "sentence", "args": {"word_count": 6}}, + {"name": "content", "generator": "paragraph", "args": {"sentence_count": 5}}, + {"name": "author", "generator": "name"}, + {"name": "published_at", "generator": "past_date"}, + {"name": "views", "generator": "integer", "args": {"min": 0, "max": 10000}} + ] + } + ] +} +`, buildDatabaseConfigJSON(config)) +} + +func generateInventorySchema(config DatabaseConfig) string { + return fmt.Sprintf(`{ +%s, + "tables": [ + { + "name": "inventory", + "columns": [ + {"name": "id", "type": "integer", "options": {"primary_key": true, "autoincrement": true}}, + {"name": "item_name", "type": {"name": "string", "args": {"length": 100}}, "options": {"nullable": false}}, + {"name": "sku", "type": {"name": "string", "args": {"length": 50}}, "options": {"nullable": false, "unique": true}}, + {"name": "quantity", "type": "integer", "options": {"nullable": false}}, + {"name": "location", "type": {"name": "string", "args": {"length": 50}}, "options": {"nullable": false}}, + {"name": "last_updated", "type": "datetime", "options": {"nullable": false}} + ] + } + ], + "populate": [ + { + "name": "inventory", + "count": 300, + "fields": [ + {"name": "item_name", "generator": "word"}, + {"name": "sku", "generator": "uuid"}, + {"name": "quantity", "generator": "integer", "args": {"min": 0, "max": 1000}}, + {"name": "location", "generator": "random_from", "args": ["Warehouse A", "Warehouse B", "Store 1", "Store 2", "Store 3"]}, + {"name": "last_updated", "generator": "past_date"} + ] + } + ] +} +`, buildDatabaseConfigJSON(config)) +} + +func generateTransactionsSchema(config DatabaseConfig) string { + return fmt.Sprintf(`{ +%s, + "tables": [ + { + "name": "transactions", + "columns": [ + {"name": "id", "type": "integer", "options": {"primary_key": true, "autoincrement": true}}, + {"name": "transaction_id", "type": {"name": "string", "args": {"length": 50}}, "options": {"nullable": false, "unique": true}}, + {"name": "amount", "type": {"name": "decimal", "args": {"precision": 10, "scale": 2}}, "options": {"nullable": false}}, + {"name": "type", "type": {"name": "string", "args": {"length": 20}}, "options": {"nullable": false}}, + {"name": "status", "type": {"name": "string", "args": {"length": 20}}, "options": {"nullable": false}}, + {"name": "timestamp", "type": "datetime", "options": {"nullable": false}} + ] + } + ], + "populate": [ + { + "name": "transactions", + "count": 1000, + "fields": [ + {"name": "transaction_id", "generator": "uuid"}, + {"name": "amount", "generator": "float", "args": {"min": 1.00, "max": 10000.00}}, + {"name": "type", "generator": "random_from", "args": ["credit", "debit", "transfer", "refund"]}, + {"name": "status", "generator": "random_from", "args": ["completed", "pending", "failed", "cancelled"]}, + {"name": "timestamp", "generator": "past_date"} + ] + } + ] +} +`, buildDatabaseConfigJSON(config)) +} + +func generateStudentsSchema(config DatabaseConfig) string { + return fmt.Sprintf(`{ +%s, + "tables": [ + { + "name": "students", + "columns": [ + {"name": "id", "type": "integer", "options": {"primary_key": true, "autoincrement": true}}, + {"name": "student_id", "type": {"name": "string", "args": {"length": 20}}, "options": {"nullable": false, "unique": true}}, + {"name": "name", "type": {"name": "string", "args": {"length": 100}}, "options": {"nullable": false}}, + {"name": "email", "type": {"name": "string", "args": {"length": 100}}, "options": {"nullable": false, "unique": true}}, + {"name": "grade", "type": {"name": "string", "args": {"length": 10}}, "options": {"nullable": false}}, + {"name": "enrollment_date", "type": "date", "options": {"nullable": false}} + ] + } + ], + "populate": [ + { + "name": "students", + "count": 200, + "fields": [ + {"name": "student_id", "generator": "uuid"}, + {"name": "name", "generator": "name"}, + {"name": "email", "generator": "email"}, + {"name": "grade", "generator": "random_from", "args": ["A", "B", "C", "D", "F"]}, + {"name": "enrollment_date", "generator": "past_date"} + ] + } + ] +} +`, buildDatabaseConfigJSON(config)) +} + +func generateTasksSchema(config DatabaseConfig) string { + return fmt.Sprintf(`{ +%s, + "tables": [ + { + "name": "tasks", + "columns": [ + {"name": "id", "type": "integer", "options": {"primary_key": true, "autoincrement": true}}, + {"name": "title", "type": {"name": "string", "args": {"length": 200}}, "options": {"nullable": false}}, + {"name": "description", "type": "text", "options": {"nullable": true}}, + {"name": "priority", "type": {"name": "string", "args": {"length": 20}}, "options": {"nullable": false}}, + {"name": "status", "type": {"name": "string", "args": {"length": 20}}, "options": {"nullable": false}}, + {"name": "due_date", "type": "date", "options": {"nullable": true}}, + {"name": "assignee", "type": {"name": "string", "args": {"length": 100}}, "options": {"nullable": true}} + ] + } + ], + "populate": [ + { + "name": "tasks", + "count": 150, + "fields": [ + {"name": "title", "generator": "sentence", "args": {"word_count": 5}}, + {"name": "description", "generator": "paragraph", "args": {"sentence_count": 3}}, + {"name": "priority", "generator": "random_from", "args": ["low", "medium", "high", "urgent"]}, + {"name": "status", "generator": "random_from", "args": ["todo", "in_progress", "review", "done"]}, + {"name": "due_date", "generator": "future_date"}, + {"name": "assignee", "generator": "name"} + ] + } + ] +} +`, buildDatabaseConfigJSON(config)) +} diff --git a/scripts/generate-schema.sh b/scripts/generate-schema.sh new file mode 100755 index 0000000..a6c3de3 --- /dev/null +++ b/scripts/generate-schema.sh @@ -0,0 +1,696 @@ +#!/bin/bash + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}========================================${NC}" +echo -e "${BLUE} Fakestack Schema Generator${NC}" +echo -e "${BLUE}========================================${NC}" +echo "" + +# Ask for database type +echo -e "${YELLOW}Select database type:${NC}" +echo "1) SQLite" +echo "2) MySQL" +echo "3) PostgreSQL" +echo "4) MariaDB" +echo "5) MS SQL Server" +echo "6) CockroachDB" +read -p "Enter your choice (1-6): " db_choice + +case $db_choice in + 1) dbtype="sqlite" ;; + 2) dbtype="mysql" ;; + 3) dbtype="postgres" ;; + 4) dbtype="mariadb" ;; + 5) dbtype="mssql" ;; + 6) dbtype="cockroachdb" ;; + *) echo -e "${RED}Invalid choice!${NC}"; exit 1 ;; +esac + +# Ask for database credentials (skip for SQLite) +if [ "$dbtype" != "sqlite" ]; then + echo "" + read -p "Enter database username (default: root): " username + username=${username:-root} + + read -p "Enter database password (default: password): " password + password=${password:-password} + + read -p "Enter database host (default: localhost): " host + host=${host:-localhost} + + # Set default ports + case $dbtype in + mysql|mariadb) default_port="3306" ;; + postgres) default_port="5432" ;; + mssql) default_port="1433" ;; + cockroachdb) default_port="26257" ;; + esac + + read -p "Enter database port (default: $default_port): " port + port=${port:-$default_port} + host_with_port="${host}:${port}" + + read -p "Enter database name (default: testdb): " database + database=${database:-testdb} +else + database="test.db" +fi + +# Ask for schema template +echo "" +echo -e "${YELLOW}Select schema template:${NC}" +echo "1) Users - Basic user management (id, username, email, created_at)" +echo "2) Employees - Employee records (id, first_name, last_name, email, department, salary, hire_date)" +echo "3) Products - E-commerce products (id, name, description, price, stock, category)" +echo "4) Orders - Order management (id, order_number, customer_email, total_amount, status, order_date)" +echo "5) Customers - Customer database (id, name, email, phone, address, city, country)" +echo "6) Blog Posts - Content management (id, title, content, author, published_at, views)" +echo "7) Inventory - Stock management (id, item_name, sku, quantity, location, last_updated)" +echo "8) Transactions - Financial records (id, transaction_id, amount, type, status, timestamp)" +echo "9) Students - Educational records (id, student_id, name, email, grade, enrollment_date)" +echo "10) Tasks - Task management (id, title, description, priority, status, due_date, assignee)" +read -p "Enter your choice (1-10): " schema_choice + +# Ask for output filename +echo "" +read -p "Enter output filename (default: schema.json): " filename +filename=${filename:-schema.json} + +# Generate schema based on choice +case $schema_choice in + 1) # Users + table_name="users" + row_count=50 + schema_json=$(cat < "$filename" + +echo "" +echo -e "${GREEN}✓ Schema file generated successfully!${NC}" +echo -e "${BLUE}File: ${NC}$filename" +echo -e "${BLUE}Database: ${NC}$dbtype" +echo -e "${BLUE}Table: ${NC}$table_name" +echo -e "${BLUE}Rows: ${NC}$row_count" +echo "" +echo -e "${YELLOW}Next steps:${NC}" +echo -e " 1. Create tables: ${GREEN}fakestack -c -f $filename${NC}" +echo -e " 2. Populate data: ${GREEN}fakestack -p -f $filename${NC}" +echo -e " 3. Or do both: ${GREEN}fakestack -c -p -f $filename${NC}" +echo "" diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh new file mode 100755 index 0000000..2860fa3 --- /dev/null +++ b/scripts/uninstall.sh @@ -0,0 +1,99 @@ +#!/bin/bash + +# Fakestack Uninstall Script +# Removes fakestack from npm, pip, and Homebrew + +set -e + +echo "🗑️ Fakestack Uninstall Script" +echo "==============================" +echo "" + +# Color codes +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Track if anything was uninstalled +UNINSTALLED=false + +# Uninstall from npm (global) +echo "📦 Checking npm (global)..." +if npm list -g fakestack 2>/dev/null | grep -q fakestack; then + echo -e "${YELLOW}Uninstalling fakestack from npm...${NC}" + npm uninstall -g fakestack + echo -e "${GREEN}✓ Uninstalled from npm${NC}" + UNINSTALLED=true +else + echo " Not installed globally in npm" +fi +echo "" + +# Uninstall from pip (current environment) +echo "🐍 Checking pip (current environment)..." +if pip show fakestack &>/dev/null; then + echo -e "${YELLOW}Uninstalling fakestack from pip...${NC}" + pip uninstall -y fakestack + echo -e "${GREEN}✓ Uninstalled from pip${NC}" + UNINSTALLED=true +else + echo " Not installed in current pip environment" +fi +echo "" + +# Uninstall from Homebrew +echo "🍺 Checking Homebrew..." +if brew list fakestack &>/dev/null; then + echo -e "${YELLOW}Uninstalling fakestack from Homebrew...${NC}" + brew uninstall fakestack + echo -e "${GREEN}✓ Uninstalled from Homebrew${NC}" + UNINSTALLED=true +else + echo " Not installed in Homebrew" +fi +echo "" + +# Optional: Remove tap +if brew tap | grep -q "0xdps/packages"; then + echo "🔧 Homebrew tap 0xdps/packages is still installed" + read -p "Do you want to remove the tap? (y/N): " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + brew untap 0xdps/packages + echo -e "${GREEN}✓ Removed tap 0xdps/packages${NC}" + fi + echo "" +fi + +# Check pyenv for other Python versions +echo "🔍 Checking other Python environments..." +if command -v pyenv &>/dev/null; then + PYTHON_VERSIONS=$(pyenv versions --bare 2>/dev/null || echo "") + if [ -n "$PYTHON_VERSIONS" ]; then + echo " Found pyenv Python versions. Checking each..." + while IFS= read -r version; do + if pyenv shell "$version" 2>/dev/null && pip show fakestack &>/dev/null; then + echo -e " ${YELLOW}Found in Python $version${NC}" + read -p " Uninstall from Python $version? (y/N): " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + pip uninstall -y fakestack + echo -e " ${GREEN}✓ Uninstalled from Python $version${NC}" + UNINSTALLED=true + fi + fi + done <<< "$PYTHON_VERSIONS" + pyenv shell --unset 2>/dev/null || true + fi +fi +echo "" + +# Summary +echo "==============================" +if [ "$UNINSTALLED" = true ]; then + echo -e "${GREEN}✓ Uninstall complete!${NC}" +else + echo -e "${YELLOW}No installations found${NC}" +fi +echo ""