From e8fe0658227f7232404dc7ea9b4a8f31b05a82b4 Mon Sep 17 00:00:00 2001 From: Asad Date: Wed, 20 Nov 2024 14:41:09 -0500 Subject: [PATCH] initial --- README.md | 69 +++++++ gen_test_files.py | 219 +++++++++++++++++++++ package.json | 17 ++ pnpm-lock.yaml | 335 ++++++++++++++++++++++++++++++++ requirements.txt | 3 + sql-types.js | 113 +++++++++++ test.js | 70 +++++++ test_files/boolean_formats.xlsx | Bin 0 -> 5570 bytes test_files/date_formats.xlsx | Bin 0 -> 5558 bytes test_files/number_formats.xlsx | Bin 0 -> 5635 bytes test_files/precision.xlsx | Bin 0 -> 5621 bytes test_files/special_values.xlsx | Bin 0 -> 5459 bytes test_files/string_formats.xlsx | Bin 0 -> 5646 bytes 13 files changed, 826 insertions(+) create mode 100644 README.md create mode 100644 gen_test_files.py create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 requirements.txt create mode 100644 sql-types.js create mode 100644 test.js create mode 100644 test_files/boolean_formats.xlsx create mode 100644 test_files/date_formats.xlsx create mode 100644 test_files/number_formats.xlsx create mode 100644 test_files/precision.xlsx create mode 100644 test_files/special_values.xlsx create mode 100644 test_files/string_formats.xlsx diff --git a/README.md b/README.md new file mode 100644 index 0000000..f432b7f --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# Sheetjs to PostgreSQL Creating a Table Demo + +A Node.js utility that intelligently converts Sheetjs `worksheet` to PostgreSQL tables while preserving appropriate data types. + +> This demo project serves as a refernce implementation for SheetJS + PostgreSQL integration. For more details, vist the [SheetJS Documentation](https://docs.sheetjs.com/docs/demos/data/postgresql/#creating-a-table). + +### Features +* Automatic data type detection from Excel columns +* Support various data formats: + * Numbers (integer and floating-point) + * Dates + * Booleans + * Text +* Handles special number formats (scientific notations, high precision) +* Clean column names for PostgreSQL compatibility + +### Prerequisites +* Node.js +* PostgreSQL (16) +* Python 3.x + +### Installation +1. Install Python dependencies: + +```bash +pip install -r requirements.txt +``` + +2. Install Node.js dependencies: +``` +npm install i +``` + +### Setup +1. Generate test_files: +```bash +python3 gen_test_files.py +``` +2. Configure PostgreSQL connection in `test.js` +```javascript +const client = new Client({ + host: 'localhost', + database: 'SheetJSPG', + user: 'postgres', + password: '7509' +}); +``` + +### Run +```bash +node test.js +``` + +### Test Files +The test suite includes various Excel files testing different data scenarios: + +* `number_formats.xlsx`: Various numeric formats +* `date_formats.xlsx`: Date handling +* `special_values.xlsx`: Edge cases +* `precision.xlsx`: High-precision numbers +* `string_formats.xlsx`: Text handling +* `boolean_formats.xlsx`: Boolean values + +### Type Mapping +* Excel dates → PostgreSQL `date` +* Booleans → PostgreSQL `boolean` +* High-precision numbers → PostgreSQL `numeric` +* Standard numbers → PostgreSQL `double precision` +* Text/other → PostgreSQL `text` \ No newline at end of file diff --git a/gen_test_files.py b/gen_test_files.py new file mode 100644 index 0000000..06a9b1f --- /dev/null +++ b/gen_test_files.py @@ -0,0 +1,219 @@ +import pandas as pd +from datetime import datetime +import numpy as np +import os + +def create_test_directory(): + """Create a directory for test files if it doesn't exist""" + if not os.path.exists('test_files'): + os.makedirs('test_files') + +def generate_number_formats_test(): + """Test Case 1: Common spreadsheet number formats""" + df = pd.DataFrame({ + 'id': range(1, 7), + 'value': [ + 1234.56, # Plain number + '1,234.56', # Thousands separator + 1234.5600, # Fixed decimal places + 0.1234, # Will be formatted as percentage + -1234.56, # Will be formatted as parentheses + -1230 # Will be formatted as scientific + ] + }) + + # Create Excel writer with xlsxwriter engine + writer = pd.ExcelWriter('test_files/number_formats.xlsx', engine='xlsxwriter') + df.to_excel(writer, index=False, sheet_name='Sheet1') + + # Get workbook and worksheet objects + workbook = writer.book + worksheet = writer.sheets['Sheet1'] + + # Add formats + percent_format = workbook.add_format({'num_format': '0.00%'}) + accounting_format = workbook.add_format({'num_format': '_(* #,##0.00_);_(* (#,##0.00);_(* "-"??_);_(@_)'}) + scientific_format = workbook.add_format({'num_format': '0.00E+00'}) + + # Apply formats to specific cells + worksheet.set_column('B:B', 15) # Set column width + worksheet.write('B5', 0.1234, percent_format) # Percentage + worksheet.write('B6', -1234.56, accounting_format) # Parentheses + worksheet.write('B7', -1230, scientific_format) # Scientific + + writer.close() + +def generate_date_formats_test(): + """Test Case 3: Date and timestamp formats""" + df = pd.DataFrame({ + 'id': range(1, 5), + 'date': [ + datetime(2024, 1, 1), # ISO format + datetime(2024, 1, 1), # US format + datetime(2024, 1, 1), # Excel format + 45292, # Excel serial date + ] + }) + + writer = pd.ExcelWriter('test_files/date_formats.xlsx', engine='xlsxwriter') + df.to_excel(writer, index=False, sheet_name='Sheet1') + + workbook = writer.book + worksheet = writer.sheets['Sheet1'] + + # Add different date and timestamp formats + iso_format = workbook.add_format({'num_format': 'yyyy-mm-dd'}) + us_format = workbook.add_format({'num_format': 'm/d/yyyy'}) + excel_format = workbook.add_format({'num_format': 'dd-mmm-yyyy'}) + + # New timestamp formats + datetime_24h_format = workbook.add_format({'num_format': 'yyyy-mm-dd hh:mm:ss'}) + datetime_12h_format = workbook.add_format({'num_format': 'yyyy-mm-dd hh:mm:ss AM/PM'}) + datetime_ms_format = workbook.add_format({'num_format': 'yyyy-mm-dd hh:mm:ss.000'}) + iso_timestamp_format = workbook.add_format({'num_format': 'yyyy-mm-ddThh:mm:ss'}) + + # Set column width to accommodate timestamps + worksheet.set_column('B:B', 25) + + # Apply formats + worksheet.write('B2', datetime(2024, 1, 1), iso_format) + worksheet.write('B3', datetime(2024, 1, 1), us_format) + worksheet.write('B4', datetime(2024, 1, 1), excel_format) + worksheet.write('B5', 45292, excel_format) + + + writer.close() + + +def generate_special_values_test(): + """Test Case 4: Empty and special values""" + df = pd.DataFrame({ + 'id': range(1, 6), + 'value': [ + np.nan, # NULL + '', # Empty string + '#N/A', # Excel error + '#DIV/0!', # Excel error + '-' # Common placeholder + ] + }) + + writer = pd.ExcelWriter('test_files/special_values.xlsx', engine='xlsxwriter') + df.to_excel(writer, index=False, sheet_name='Sheet1') + writer.close() + +def generate_precision_test(): + """Test Case 5: Number precision""" + df = pd.DataFrame({ + 'id': range(1, 8), + 'value': [ + 1.234567890123456, # High precision decimal + 12345678901234567890, # Large integer (> 15 digits) + -0.00000000123456, # Small decimal + 9.99999e20, # Scientific notation large + -1.23456e-10, # Scientific notation small + 123456789.123456789, # Mixed large number with decimals + 1234567890123456.789 # Edge case for precision + ] + }) + + writer = pd.ExcelWriter('test_files/precision.xlsx', engine='xlsxwriter') + df.to_excel(writer, index=False, sheet_name='Sheet1') + + workbook = writer.book + worksheet = writer.sheets['Sheet1'] + + # Add formats for different number types + precision_format = workbook.add_format({'num_format': '0.000000000000000'}) + scientific_format = workbook.add_format({'num_format': '0.000000E+00'}) + + worksheet.write('B3', 1.234567890123456, precision_format) + worksheet.write('B4', 9.99999e20, scientific_format) + + # Apply formats + worksheet.set_column('B:B', 20) + for row in range(1, 8): + if row in [4, 5]: # Scientific notation + worksheet.write(row, 1, df['value'][row-1], scientific_format) + else: + worksheet.write(row, 1, df['value'][row-1], precision_format) + + writer.close() + +def generate_string_formats_test(): + """Test Case 2: String formats and special characters""" + df = pd.DataFrame({ + 'id': range(1, 9), + 'value': [ + 'Simple text', # Plain text + 'Text with spaces ', # Trailing spaces + ' Text with spaces', # Leading spaces + 'Text with\nnewline', # Newline character + 'Text with "quotes"', # Quoted text + 'Text with special chars: @#$%', # Special characters + 'Very long text ' * 10, # Long text + 'Super long text ' * 100 # Super long text + ] + }) + + writer = pd.ExcelWriter('test_files/string_formats.xlsx', engine='xlsxwriter') + df.to_excel(writer, index=False, sheet_name='Sheet1') + + workbook = writer.book + worksheet = writer.sheets['Sheet1'] + + # Set column width to show long text + worksheet.set_column('B:B', 50) + + writer.close() + +def generate_boolean_formats_test(): + """Test Case: Boolean formats in Excel""" + df = pd.DataFrame({ + 'id': range(1, 5), + 'value': [ + True, # Simple True + False, # Simple False + 'TRUE', # String TRUE + 'FALSE' # String FALSE + ] + }) + + writer = pd.ExcelWriter('test_files/boolean_formats.xlsx', engine='xlsxwriter') + df.to_excel(writer, index=False, sheet_name='Sheet1') + + workbook = writer.book + worksheet = writer.sheets['Sheet1'] + + # Add boolean formats + bool_format = workbook.add_format({'num_format': 'BOOLEAN'}) + custom_true_false = workbook.add_format({'num_format': '"True";;"False"'}) + yes_no_format = workbook.add_format({'num_format': '"YES";;NO'}) + + # Set column width + worksheet.set_column('B:B', 15) + + # Apply different boolean formats + worksheet.write('B2', True, bool_format) # Standard TRUE + worksheet.write('B3', False, bool_format) # Standard FALSE + worksheet.write('B4', True, yes_no_format) # Yes/No format + worksheet.write('B5', False, yes_no_format) # Yes/No format + + writer.close() + + +def main(): + """Geneate all test Excel files""" + create_test_directory() + + print("Generating test Excel files...") + generate_number_formats_test() + generate_date_formats_test() + generate_special_values_test() + generate_precision_test() + generate_string_formats_test() + generate_boolean_formats_test() + print("Test files generated in 'test_files' directory") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..6e0aaf0 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "rework-create-table-deduction-solution", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "assert": "^2.1.0", + "pg": "^8.13.1", + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..4b456fc --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,335 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + assert: + specifier: ^2.1.0 + version: 2.1.0 + pg: + specifier: ^8.13.1 + version: 8.13.1 + xlsx: + specifier: https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz + version: '@cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz' + +packages: + + /assert@2.1.0: + resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==} + dependencies: + call-bind: 1.0.7 + is-nan: 1.3.2 + object-is: 1.1.6 + object.assign: 4.1.5 + util: 0.12.5 + dev: false + + /available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + dependencies: + possible-typed-array-names: 1.0.0 + dev: false + + /call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + set-function-length: 1.2.2 + dev: false + + /define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 + dev: false + + /define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + dev: false + + /es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.4 + dev: false + + /es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + dev: false + + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: false + + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + dev: false + + /get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + dev: false + + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.4 + dev: false + + /has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + dependencies: + es-define-property: 1.0.0 + dev: false + + /has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} + engines: {node: '>= 0.4'} + dev: false + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: false + + /has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: false + + /hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: false + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: false + + /is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + dev: false + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: false + + /is-generator-function@1.0.10: + resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.2 + dev: false + + /is-nan@1.3.2: + resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + dev: false + + /is-typed-array@1.1.13: + resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.15 + dev: false + + /object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + dev: false + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: false + + /object.assign@4.1.5: + resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: false + + /pg-cloudflare@1.1.1: + resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==} + requiresBuild: true + dev: false + optional: true + + /pg-connection-string@2.7.0: + resolution: {integrity: sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==} + dev: false + + /pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + dev: false + + /pg-pool@3.7.0(pg@8.13.1): + resolution: {integrity: sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==} + peerDependencies: + pg: '>=8.0' + dependencies: + pg: 8.13.1 + dev: false + + /pg-protocol@1.7.0: + resolution: {integrity: sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==} + dev: false + + /pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + dev: false + + /pg@8.13.1: + resolution: {integrity: sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==} + engines: {node: '>= 8.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + dependencies: + pg-connection-string: 2.7.0 + pg-pool: 3.7.0(pg@8.13.1) + pg-protocol: 1.7.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.1.1 + dev: false + + /pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + dependencies: + split2: 4.2.0 + dev: false + + /possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + dev: false + + /postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + dev: false + + /postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + dev: false + + /postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + dev: false + + /postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + dependencies: + xtend: 4.0.2 + dev: false + + /set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + dev: false + + /split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + dev: false + + /util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + dependencies: + inherits: 2.0.4 + is-arguments: 1.1.1 + is-generator-function: 1.0.10 + is-typed-array: 1.1.13 + which-typed-array: 1.1.15 + dev: false + + /which-typed-array@1.1.15: + resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.2 + dev: false + + /xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + dev: false + + '@cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz': + resolution: {tarball: https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz} + name: xlsx + version: 0.20.3 + engines: {node: '>=0.8'} + hasBin: true + dev: false diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0111e7a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +pandas==2.1.0 +xlsxwriter==3.1.2 +numpy==1.24.3 \ No newline at end of file diff --git a/sql-types.js b/sql-types.js new file mode 100644 index 0000000..7682d76 --- /dev/null +++ b/sql-types.js @@ -0,0 +1,113 @@ +const format = require("pg-format"); +const XLSX = require('xlsx'); + +// Utility func for number handling +const isNumeric = value => !isNaN(String(value).replace(/[,$\s]/g,'')) && !isNaN(parseFloat(String(value).replace(/[,$\s]/g,''))); +const cleanNumericValue = value => typeof value === 'string' ? value.replace(/[,$\s]/g, '') : value; + +// Determines PostgreSQL data type based on column values +function deduceType(values) { + if (!values || values.length === 0) return 'text'; + + const isDateCell = cell => cell && (cell.t === 'd' | (cell.t === 'n')); + const isBooleanCell = cell => cell?.t === 'b'; + const isValidNumber = cell => cell && (cell.t === 'n' || isNumeric(cell.v)); + const needsPrecision = num => { + const str = num.toString(); + return str.includes('e') || + (str.includes('.') && str.split('.')[1].length > 6) || + Math.abs(num) > 1e15; + }; + + // Type detection priority: dates > booleans > numbers > text + if (values.some(isDateCell)) return 'date'; + if (values.some(isBooleanCell) && values.every(cell => !cell || isBooleanCell(cell))) return 'boolean'; + + const numberValues = values + .filter(isValidNumber) + .map(cell => parseFloat(cleanNumericValue(cell.v))); + + if (numberValues.length && values.every(cell => !cell || isValidNumber(cell))) { + return numberValues.some(needsPrecision) ? 'numeric' : 'double precision'; + } + return 'text'; +} + +// Converts Sheetjs cell value to PostgreSQL compatible format +function parseValue(cell, type) { + if (!cell || cell.v == null || cell.v === '') return null; + + switch (type) { + case 'date': + if (cell.t === 'd') return cell.v.toISOString().split('T')[0]; + if (cell.t === 'n') return new Date((cell.v - 25569) * 86400 * 1000).toISOString().split('T')[0]; + return null; + case 'double precision': + if (cell.t === 'n') return cell.v; + if (isNumeric(cell.v)) return parseFloat(cleanNumericValue(cell.v)); + return null; + case 'boolean': + return cell.t === 'b' ? cell.v : null; + default: + return String(cell.v); + } +} + +async function sheet_to_pg_table(client, worksheet, tableName) { + if (!worksheet['!ref']) return; + + const range = XLSX.utils.decode_range(worksheet['!ref']); + + // Extract headers from first row, clean names for PostgreSQL + const headers = []; + for (let col = range.s.c; col <= range.e.c; col++) { + const cellAddress = XLSX.utils.encode_cell({ r: range.s.r, c: col }); + const cell = worksheet[cellAddress]; + const headerValue = cell ? String(cell.v).replace(/[^a-zA-Z0-9_]/g, '_') : `column_${col + 1}`; + headers.push(headerValue.toLowerCase()); + } + + // Group cell values by column for type deduction + const columnValues = headers.map(() => []); + for (let row = range.s.r + 1; row <= range.e.r; row++) { + for (let col = range.s.c; col <= range.e.c; col++) { + const cellAddress = XLSX.utils.encode_cell({ r: row, c: col }); + const cell = worksheet[cellAddress]; + columnValues[col].push(cell); + } + } + + // Deduc PostgreSQL type for each column + const types = {}; + headers.forEach((header, idx) => { + types[header] = deduceType(columnValues[idx]); + }); + + await client.query(format('DROP TABLE IF EXISTS %I', tableName)); + + const createTableSQL = format( + 'CREATE TABLE %I (%s)', + tableName, + headers.map(header => format('%I %s', header, types[header])).join(', ') + ); + await client.query(createTableSQL); + + // Insert data row by row + for (let row = range.s.r + 1; row <= range.e.r; row++) { + const values = headers.map((header, col) => { + const cellAddress = XLSX.utils.encode_cell({ r: row, c: col }); + const cell = worksheet[cellAddress]; + return parseValue(cell, types[header]); + }); + + const insertSQL = format( + 'INSERT INTO %I (%s) VALUES (%s)', + tableName, + headers.map(h => format('%I', h)).join(', '), + values.map(() => '%L').join(', ') + ); + await client.query(format(insertSQL, ...values)); + } +} + +module.exports = { sheet_to_pg_table }; \ No newline at end of file diff --git a/test.js b/test.js new file mode 100644 index 0000000..f79106e --- /dev/null +++ b/test.js @@ -0,0 +1,70 @@ +const XLSX = require('xlsx'); +const { Client } = require('pg'); +const { sheet_to_pg_table } = require('./sql-types'); +const path = require('path'); + +async function readExcelAndTest(filename, tableName) { + console.log(`\nTesting ${filename}...`); + + // Read Excel file + const workbook = XLSX.readFile(path.join('test_files', filename), { dense: true } ); + const sheetName = workbook.SheetNames[0]; + const worksheet = workbook.Sheets[sheetName]; + + // Convert to array of objects + const data = XLSX.utils.sheet_to_json(worksheet); // keep number formatting + console.log('Parsed Excel data:', data); + + // Connect to PostgreSQL + const client = new Client({ + host: 'localhost', + database: 'SheetJSPG', + user: 'postgres', + password: '7509' + }); + + try { + await client.connect(); + + // Import data + await sheet_to_pg_table(client, workbook, tableName); + + // Verify table structure + const structure = await client.query(` + SELECT column_name, data_type + FROM information_schema.columns + WHERE table_name = $1 + ORDER BY ordinal_position; + `, [tableName]); + console.log('\nTable structure:', structure.rows); + + // Verify data + const results = await client.query(`SELECT * FROM ${tableName}`); + console.log('\nImported data from DB:', results.rows); + + } catch (error) { + console.error(`Error testing ${filename}:`, error); + throw error; + } finally { + await client.end(); + } +} + +async function runAllTests() { + try { + // Test each Excel file + await readExcelAndTest('number_formats.xlsx', 'test_number_formats'); + await readExcelAndTest('date_formats.xlsx', 'test_dates'); + await readExcelAndTest('special_values.xlsx', 'test_special_values'); + await readExcelAndTest('precision.xlsx', 'test_precision'); + await readExcelAndTest('string_formats.xlsx', 'test_string_formats'); + await readExcelAndTest('boolean_formats.xlsx', 'test_boolean_formats'); + + console.log('\nAll tests completed successfully'); + } catch (error) { + console.error('\nTests failed:', error); + process.exit(1); + } +} + +runAllTests().catch(console.error); \ No newline at end of file diff --git a/test_files/boolean_formats.xlsx b/test_files/boolean_formats.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..148aa0915ce08dd712594a5ac8c2c14099a260f1 GIT binary patch literal 5570 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 literal 0 HcmV?d00001 diff --git a/test_files/date_formats.xlsx b/test_files/date_formats.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..a84a23af701334e94c452f76623be89a6ac49050 GIT binary patch literal 5558 zcmZ`-2Q*ym)*dZ-jfpOL9U^Lk=tgJsl86L@QAQ6zbRs$-A$kiUy69chi4sJwLv%(9 zqW+Wb`|iz+|K4-fIqzC$t^J(!KD)fn-Vik`Y)SwCfQNph0Z&a;?rUKH05RAAz)kd* zhw_eaHz?fAOz*i9)YX*V)4{$t0Re9pAX7ZP2=AHZx$CB_4-8Pejkr~Ki1o-@4n#k_ zPeggy{CXGiP&+eFi`}a(aUQG`RgE)5+ zbT^qpgOmzJXS9#1mIpVP@ae<1p7Pln=Rxk?F9JH7@;Cle>=g?Rl#n1E6Ge&;2G6M3 z@cLniz9?-J>ZR@J;`th3Ys9WGv_#3GI!5)<^yo8@K2)TvyK(Ui=9J70b;d~w3gZc= zRL>Z5+tuRq1KS2IHvTjP6Y}6{gP1|Q94KTM4%#CN*u(%%YjzJCCr(Whw#QrR$M`+T%wc} zs|$5j)xzGU7-OJv&ke7*lT7#O2O3Gd(Ug5!2$&rA;OKhTv)1Ut4<#tK(_a6T;KJ$w znW=jAf{VMuBKGiyj_YyTPqUeZQ!p-;`IgyOpu-QL26oJLD5oibjvf~|cObpZzNO?HM3^Ij%s;MVs z1V$>C72Y00P1p?xE!Qivy!6yrNLq?Wu%st>7f$wdHG?Bp*7Z$cqIOCXT${gdU*$dQ z#;9Skxg9XKxL9VZE9|P5L}P@;hj_(KW>Ac0*+bdeB`j6m-8K&R2o#^6&*Q?=Gc6Dy z{`B4;@^+6(dVd2ELYyY7Tk6gsrt{eGhGFHHMB0$AQ{T8~UeZ*H!2SkugY{2-=w-XV z7ojt@8w&uS{+pj)%(&V>p>D2s{#x$-qA2Qh8$4BjEEM&E0CD`WO{T^eLpz&Gl{E2# zsKel6>Q;j4xNXzn8q@?^kxfulQ)^`R)v@;t-MPksIQq8xFgL{!4LSU^t}2~l;Kh$a zryn5{t0C}C8)3b}JNLYF{dlY`GG+k__?T5iYYP`u7GFfLxE4jNFryL&jMydLS6jm? zn5MXNSeI1xlew8iOz|Ynq=`n{D+0Vn=qYVlWI&PaOMA0SoJyM&iAIDMWQ3-Qjw7?C zvc{(1)p-i5V}6duTYl|$Q7>-)LwjgyVa<_txm>D-3*!kR$0;jjFe6c4< z8JWKp-_428%4XAiqnrI%&1bOU2Nt-~DurWwvN|(Y{GQ`zMqVtqTk&?&P6(AdtY6(; zGCM^f$+LcG!r|sE#}3<%S$;`v<(jI`h?2$`JZ^jB<}(mYa$+^xwhpopCMd1xAP9Z? z$Kxh>1S14}L%*;HHUxh$jC-B{weFE*IZ*2{2O5Jage;vgdgor$svPcWOgy$#0&Sc4f4KInW6} z`rE<(3jHW;1iCDcH68%R=3tDrtR^CbpVjo_{1sq>9CN}33=$O#!

>^|+mus)Uwi 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 ZC+#6>IOxs|03bl${%Ail{Tj;x{tr{lg(Ls~ literal 0 HcmV?d00001 diff --git a/test_files/number_formats.xlsx b/test_files/number_formats.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..7b9ece4cb009aaf44098d24fe96d476fdead58b4 GIT binary patch literal 5635 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 UPa^gu zGALDXaYZ+!KQomTftcw=3@JZ^#EVUFvdFgBUIGx~lZD^F@{WFDYFBi%1pDH6^i8 zGbvfw=Qw|A`mmvJ)C;32IS%IePfg98o&T1!{r<+KiGwi{q&&25?(2#x7X>`nRxF@- zz-Tk_K&<9FeQt7jx9aD3Y~G3nK{t;l0yTA0)gl|LxjSW6T_78Ju^+sPH5|TEGZ`0_ z^a-v>6?1aE2LYREnUQ+?SZwrdAPzB*p#l|6dLA-cPfXo1g|j-YG*=hJcj)0LG~<0X z7sM_-w5Lugfo&k?!Geo+pQZEs~*V9{-@OcJ0dShf8W$azCh=mx^xExDN| zgr(3bBT8A^0)IM(u8IT#YdJa!BLU>hTe`*TV4{S`kT~qt{TM%7)nh6b%N}g$abD^* z+64~B8Tw;G)v>UtaQ?sDhB>LfJ^_K3 z%l|Ont`c1Ji~fcK00DsknA86c_vk9{s;%-Hn1D&c|8KHf<+*A<{N|~^gdT?Hf0_|j zS*}j7zgem=o!>uM{x#2D1z(-SeuH^1Qw#?DUvt@2hO1N0Z-z@-^pfGB)vCUO6(7G8y4P4wUJ9!xF$H~gPz4^_vzcKHuN%r^jY&W@KJ GFW?_KrKxuS literal 0 HcmV?d00001 diff --git a/test_files/precision.xlsx b/test_files/precision.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..1d900b404d647c9bf2e05bb64516cc77ccdb162b GIT binary patch literal 5621 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 literal 0 HcmV?d00001 diff --git a/test_files/special_values.xlsx b/test_files/special_values.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..7076c8adccfc51a3541d74f3d4c0593984e1f05e GIT binary patch literal 5459 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 literal 0 HcmV?d00001 diff --git a/test_files/string_formats.xlsx b/test_files/string_formats.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..f3e51d8856fb446d73fb0bf79ce3b48af7b79319 GIT binary patch literal 5646 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 LzxMf;-3$0XUoWl} literal 0 HcmV?d00001