initial commit
This commit is contained in:
28
config.yaml
28
config.yaml
@@ -0,0 +1,28 @@
|
|||||||
|
connection_string: postgresql://postgres:mysecretpassword@localhost:5433/postgres
|
||||||
|
|
||||||
|
# The query to execute. This MUST be a COPY...TO STDOUT (FORMAT binary) command.
|
||||||
|
# We cast all columns to match the binary types our parser expects.
|
||||||
|
query: "COPY (SELECT id::BIGINT, uuid::TEXT, username::TEXT, score::REAL, is_active::BOOLEAN, last_login::TIMESTAMP, notes::TEXT, course_id::INT, start_date::DATE, rating::FLOAT8 FROM benchmark_table) TO STDOUT (FORMAT binary)"
|
||||||
|
|
||||||
|
# 'schema' is the ordered list of columns *exactly* as they appear in the query's SELECT statement.
|
||||||
|
schema:
|
||||||
|
- column_name: id
|
||||||
|
arrow_type: Int64
|
||||||
|
- column_name: uuid
|
||||||
|
arrow_type: Utf8
|
||||||
|
- column_name: username
|
||||||
|
arrow_type: Utf8
|
||||||
|
- column_name: score
|
||||||
|
arrow_type: Float32
|
||||||
|
- column_name: is_active
|
||||||
|
arrow_type: Boolean
|
||||||
|
- column_name: last_login
|
||||||
|
arrow_type: Timestamp(Nanosecond, None)
|
||||||
|
- column_name: notes
|
||||||
|
arrow_type: Utf8
|
||||||
|
- column_name: course_id
|
||||||
|
arrow_type: Int32
|
||||||
|
- column_name: start_date
|
||||||
|
arrow_type: Date32
|
||||||
|
- column_name: rating
|
||||||
|
arrow_type: Float64
|
||||||
127
setup_db.sql
127
setup_db.sql
@@ -0,0 +1,127 @@
|
|||||||
|
-- 1. Drop the old table if it exists
|
||||||
|
DROP TABLE IF EXISTS benchmark_table;
|
||||||
|
|
||||||
|
-- 2. Create the new 10-column table
|
||||||
|
CREATE TABLE benchmark_table (
|
||||||
|
id BIGINT,
|
||||||
|
uuid TEXT, -- Storing as TEXT for simplicity, can be native UUID
|
||||||
|
username TEXT,
|
||||||
|
score REAL,
|
||||||
|
is_active BOOLEAN,
|
||||||
|
last_login TIMESTAMP,
|
||||||
|
notes TEXT,
|
||||||
|
course_id INT,
|
||||||
|
start_date DATE,
|
||||||
|
rating FLOAT8
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 3. Use COPY FROM STDIN to bulk-load 100 rows of data
|
||||||
|
-- This is the fastest way to insert data into Postgres.
|
||||||
|
COPY benchmark_table (id, uuid, username, score, is_active, last_login, notes, course_id, start_date, rating) FROM STDIN WITH (FORMAT csv);
|
||||||
|
1,a1b2c3d4-e5f6-7890-1234-567890abcdef,user_1,95.5,true,2025-01-10 10:30:00,First note,101,2024-01-01,4.5
|
||||||
|
2,b2c3d4e5-f6a7-8901-2345-678901bcdef0,user_2,88.0,true,2025-02-15 11:45:10,Second note,102,2024-01-02,4.2
|
||||||
|
3,c3d4e5f6-a7b8-9012-3456-789012cdef01,user_3,72.1,false,2025-03-20 12:00:05,,101,2024-01-03,3.8
|
||||||
|
4,d4e5f6a7-b8c9-0123-4567-890123def012,user_4,99.9,true,2025-04-25 14:15:30,High score,103,2024-01-04,4.9
|
||||||
|
5,e5f6a7b8-c9d0-1234-5678-901234ef0123,user_5,65.0,false,2025-05-30 09:10:20,Needs review,102,2024-01-05,3.1
|
||||||
|
6,f6a7b8c9-d0e1-2345-6789-012345f01234,user_6,78.5,true,2025-06-01 18:05:00,Passed,101,2024-01-06,4.0
|
||||||
|
7,a7b8c9d0-e1f2-3456-7890-123456012345,user_7,82.3,true,2025-07-07 13:20:45,,104,2024-01-07,4.1
|
||||||
|
8,b8c9d0e1-f2a3-4567-8901-234567123456,user_8,91.0,true,2025-08-12 10:00:00,Excellent,103,2024-01-08,4.7
|
||||||
|
9,c9d0e1f2-a3b4-5678-9012-345678234567,user_9,55.0,false,2025-09-18 20:30:15,Failed,102,2024-01-09,2.5
|
||||||
|
10,d0e1f2a3-b4c5-6789-0123-456789345678,user_10,89.5,true,2025-10-22 11:11:11,Good work,101,2024-01-10,4.6
|
||||||
|
11,e1f2a3b4-c5d6-7890-1234-567890456789,user_11,92.0,true,2025-11-28 14:55:00,Solid,103,2024-01-11,4.8
|
||||||
|
12,f2a3b4c5-d6e7-8901-2345-678901567890,user_12,75.0,true,2025-12-01 08:30:00,,104,2024-01-12,3.9
|
||||||
|
13,a3b4c5d6-e7f8-9012-3456-789012678901,user_13,68.2,false,2025-01-15 17:45:30,Re-test,102,2024-01-13,3.3
|
||||||
|
14,b4c5d6e7-f8a9-0123-4567-890123789012,user_14,81.0,true,2025-02-20 12:01:00,Passed,101,2024-01-14,4.3
|
||||||
|
15,c5d6e7f8-a9b0-1234-5678-901234890123,user_15,77.7,true,2025-03-25 10:10:10,Average,103,2024-01-15,3.7
|
||||||
|
16,d6e7f8a9-b0c1-2345-6789-012345901234,user_16,94.5,true,2025-04-30 16:20:25,,104,2024-01-16,4.8
|
||||||
|
17,e7f8a9b0-c1d2-3456-7890-123456012345,user_17,83.0,true,2025-05-05 11:30:00,Good,101,2024-01-17,4.2
|
||||||
|
18,f8a9b0c1-d2e3-4567-8901-234567123456,user_18,60.5,false,2025-06-10 19:00:50,Needs help,102,2024-01-18,2.9
|
||||||
|
19,a9b0c1d2-e3f4-5678-9012-345678234567,user_19,88.5,true,2025-07-15 14:45:00,Great,103,2024-01-19,4.6
|
||||||
|
20,b0c1d2e3-f4a5-6789-0123-456789345678,user_20,90.0,true,2025-08-20 09:00:00,,104,2024-01-20,4.7
|
||||||
|
21,c1d2e3f4-a5b6-7890-1234-567890456789,user_21,93.2,true,2025-09-25 10:30:00,Top score,101,2024-01-21,4.9
|
||||||
|
22,d2e3f4a5-b6c7-8901-2345-678901567890,user_22,76.8,true,2025-10-30 11:45:10,Passed,102,2024-01-22,3.9
|
||||||
|
23,e3f4a5b6-c7d8-9012-3456-789012678901,user_23,69.9,false,2025-11-04 12:00:05,,101,2024-01-23,3.5
|
||||||
|
24,f4a5b6c7-d8e9-0123-4567-890123789012,user_24,85.0,true,2025-12-10 14:15:30,Good,103,2024-01-24,4.4
|
||||||
|
25,a5b6c7d8-e9f0-1234-5678-901234890123,user_25,70.0,true,2025-01-15 09:10:20,Review,102,2024-01-25,3.6
|
||||||
|
26,b6c7d8e9-f0a1-2345-6789-012345901234,user_26,81.5,true,2025-02-20 18:05:00,Passed,101,2024-01-26,4.1
|
||||||
|
27,c7d8e9f0-a1b2-3456-7890-123456012345,user_27,87.3,true,2025-03-27 13:20:45,,104,2024-01-27,4.5
|
||||||
|
28,d8e9f0a1-b2c3-4567-8901-234567123456,user_28,94.0,true,2025-04-01 10:00:00,Excellent,103,2024-01-28,4.8
|
||||||
|
29,e9f0a1b2-c3d4-5678-9012-345678234567,user_29,58.0,false,2025-05-07 20:30:15,Failed,102,2024-01-29,2.8
|
||||||
|
30,f0a1b2c3-d4e5-6789-0123-456789345678,user_30,90.5,true,2025-06-12 11:11:11,Great work,101,2024-01-30,4.7
|
||||||
|
31,0a1b2c3d-4e5f-6789-0123-456789abcdef,user_31,96.0,true,2025-07-18 14:55:00,Top,103,2024-01-31,4.9
|
||||||
|
32,1b2c3d4e-5f6a-7890-1234-567890bcdef0,user_32,74.0,true,2025-08-23 08:30:00,,104,2024-02-01,3.8
|
||||||
|
33,2c3d4e5f-6a7b-8901-2345-678901cdef01,user_33,67.0,false,2025-09-28 17:45:30,Re-test,102,2024-02-02,3.2
|
||||||
|
34,3d4e5f6a-7b8c-9012-3456-789012def012,user_34,80.0,true,2025-10-03 12:01:00,Passed,101,2024-02-03,4.2
|
||||||
|
35,4e5f6a7b-8c9d-0123-4567-890123ef0123,user_35,79.0,true,2025-11-08 10:10:10,Average,103,2024-02-04,3.8
|
||||||
|
36,5f6a7b8c-9d0e-1234-5678-901234f01234,user_36,95.0,true,2025-12-14 16:20:25,,104,2024-02-05,4.9
|
||||||
|
37,6a7b8c9d-0e1f-2345-6789-012345012345,user_37,84.5,true,2025-01-20 11:30:00,Good,101,2024-02-06,4.3
|
||||||
|
38,7b8c9d0e-1f2a-3456-7890-123456123456,user_38,62.0,false,2025-02-25 19:00:50,Needs help,102,2024-02-07,3.0
|
||||||
|
39,8c9d0e1f-2a3b-4567-8901-234567234567,user_39,87.0,true,2025-03-02 14:45:00,Great,103,2024-02-08,4.5
|
||||||
|
40,9d0e1f2a-3b4c-5678-9012-345678345678,user_40,91.8,true,2025-04-07 09:00:00,,104,2024-02-09,4.7
|
||||||
|
41,0e1f2a3b-4c5d-6789-0123-456789456789,user_41,94.2,true,2025-05-13 10:30:00,Top score,101,2024-02-10,4.8
|
||||||
|
42,1f2a3b4c-5d6e-7890-1234-567890567890,user_42,78.0,true,2025-06-18 11:45:10,Passed,102,2024-02-11,4.0
|
||||||
|
43,2a3b4c5d-6e7f-8901-2345-678901678901,user_43,71.5,false,2025-07-24 12:00:05,,101,2024-02-12,3.6
|
||||||
|
44,3b4c5d6e-7f8a-9012-3456-789012789012,user_44,86.3,true,2025-08-29 14:15:30,Good,103,2024-02-13,4.5
|
||||||
|
45,4c5d6e7f-8a9b-0123-4567-890123890123,user_45,72.0,true,2025-09-03 09:10:20,Review,102,2024-02-14,3.7
|
||||||
|
46,5d6e7f8a-9b0c-1234-5678-901234901234,user_46,83.0,true,2025-10-09 18:05:00,Passed,101,2024-02-15,4.2
|
||||||
|
47,6e7f8a9b-0c1d-2345-6789-012345012345,user_47,89.0,true,2025-11-14 13:20:45,,104,2024-02-16,4.6
|
||||||
|
48,7f8a9b0c-1d2e-3456-7890-123456123456,user_48,96.5,true,2025-12-20 10:00:00,Excellent,103,2024-02-17,4.9
|
||||||
|
49,8a9b0c1d-2e3f-4567-8901-234567234567,user_49,60.0,false,2025-01-25 20:30:15,Failed,102,2024-02-18,2.9
|
||||||
|
50,9b0c1d2e-3f4a-5678-9012-345678345678,user_50,92.5,true,2025-02-01 11:11:11,Great work,101,2024-02-19,4.8
|
||||||
|
51,a1b2c3d4-e5f6-7890-1234-567890abcdef,user_51,95.5,true,2025-01-10 10:30:00,First note,101,2024-01-01,4.5
|
||||||
|
52,b2c3d4e5-f6a7-8901-2345-678901bcdef0,user_52,88.0,true,2025-02-15 11:45:10,Second note,102,2024-01-02,4.2
|
||||||
|
53,c3d4e5f6-a7b8-9012-3456-789012cdef01,user_53,72.1,false,2025-03-20 12:00:05,,101,2024-01-03,3.8
|
||||||
|
54,d4e5f6a7-b8c9-0123-4567-890123def012,user_54,99.9,true,2025-04-25 14:15:30,High score,103,2024-01-04,4.9
|
||||||
|
55,e5f6a7b8-c9d0-1234-5678-901234ef0123,user_55,65.0,false,2025-05-30 09:10:20,Needs review,102,2024-01-05,3.1
|
||||||
|
56,f6a7b8c9-d0e1-2345-6789-012345f01234,user_56,78.5,true,2025-06-01 18:05:00,Passed,101,2024-01-06,4.0
|
||||||
|
57,a7b8c9d0-e1f2-3456-7890-123456012345,user_57,82.3,true,2025-07-07 13:20:45,,104,2024-01-07,4.1
|
||||||
|
58,b8c9d0e1-f2a3-4567-8901-234567123456,user_58,91.0,true,2025-08-12 10:00:00,Excellent,103,2024-01-08,4.7
|
||||||
|
59,c9d0e1f2-a3b4-5678-9012-345678234567,user_59,55.0,false,2025-09-18 20:30:15,Failed,102,2024-01-09,2.5
|
||||||
|
60,d0e1f2a3-b4c5-6789-0123-456789345678,user_60,89.5,true,2025-10-22 11:11:11,Good work,101,2024-01-10,4.6
|
||||||
|
61,e1f2a3b4-c5d6-7890-1234-567890456789,user_61,92.0,true,2025-11-28 14:55:00,Solid,103,2024-01-11,4.8
|
||||||
|
62,f2a3b4c5-d6e7-8901-2345-678901567890,user_62,75.0,true,2025-12-01 08:30:00,,104,2024-01-12,3.9
|
||||||
|
63,a3b4c5d6-e7f8-9012-3456-789012678901,user_63,68.2,false,2025-01-15 17:45:30,Re-test,102,2024-01-13,3.3
|
||||||
|
64,b4c5d6e7-f8a9-0123-4567-890123789012,user_64,81.0,true,2025-02-20 12:01:00,Passed,101,2024-01-14,4.3
|
||||||
|
65,c5d6e7f8-a9b0-1234-5678-901234890123,user_65,77.7,true,2025-03-25 10:10:10,Average,103,2024-01-15,3.7
|
||||||
|
66,d6e7f8a9-b0c1-2345-6789-012345901234,user_66,94.5,true,2025-04-30 16:20:25,,104,2024-01-16,4.8
|
||||||
|
67,e7f8a9b0-c1d2-3456-7890-123456012345,user_67,83.0,true,2025-05-05 11:30:00,Good,101,2024-01-12,4.2
|
||||||
|
68,f8a9b0c1-d2e3-4567-8901-234567123456,user_68,60.5,false,2025-06-10 19:00:50,Needs help,102,2024-01-18,2.9
|
||||||
|
69,a9b0c1d2-e3f4-5678-9012-345678234567,user_69,88.5,true,2025-07-15 14:45:00,Great,103,2024-01-19,4.6
|
||||||
|
70,b0c1d2e3-f4a5-6789-0123-456789345678,user_70,90.0,true,2025-08-20 09:00:00,,104,2024-01-20,4.7
|
||||||
|
71,c1d2e3f4-a5b6-7890-1234-567890456789,user_71,93.2,true,2025-09-25 10:30:00,Top score,101,2024-01-21,4.9
|
||||||
|
72,d2e3f4a5-b6c7-8901-2345-678901567890,user_72,76.8,true,2025-10-30 11:45:10,Passed,102,2024-01-22,3.9
|
||||||
|
73,e3f4a5b6-c7d8-9012-3456-789012678901,user_73,69.9,false,2025-11-04 12:00:05,,101,2024-01-23,3.5
|
||||||
|
74,f4a5b6c7-d8e9-0123-4567-890123789012,user_74,85.0,true,2025-12-10 14:15:30,Good,103,2024-01-24,4.4
|
||||||
|
75,a5b6c7d8-e9f0-1234-5678-901234890123,user_75,70.0,true,2025-01-15 09:10:20,Review,102,2024-01-25,3.6
|
||||||
|
76,b6c7d8e9-f0a1-2345-6789-012345901234,user_76,81.5,true,2025-02-20 18:05:00,Passed,101,2024-01-26,4.1
|
||||||
|
77,c7d8e9f0-a1b2-3456-7890-123456012345,user_77,87.3,true,2025-03-27 13:20:45,,104,2024-01-27,4.5
|
||||||
|
78,d8e9f0a1-b2c3-4567-8901-234567123456,user_78,94.0,true,2025-04-01 10:00:00,Excellent,103,2024-01-28,4.8
|
||||||
|
79,e9f0a1b2-c3d4-5678-9012-345678234567,user_79,58.0,false,2025-05-07 20:30:15,Failed,102,2024-01-29,2.8
|
||||||
|
80,f0a1b2c3-d4e5-6789-0123-456789345678,user_80,90.5,true,2025-06-12 11:11:11,Great work,101,2024-01-30,4.7
|
||||||
|
81,0a1b2c3d-4e5f-6789-0123-456789abcdef,user_81,96.0,true,2025-07-18 14:55:00,Top,103,2024-01-31,4.9
|
||||||
|
82,1b2c3d4e-5f6a-7890-1234-567890bcdef0,user_82,74.0,true,2025-08-23 08:30:00,,104,2024-02-01,3.8
|
||||||
|
83,2c3d4e5f-6a7b-8901-2345-678901cdef01,user_83,67.0,false,2025-09-28 17:45:30,Re-test,102,2024-02-02,3.2
|
||||||
|
84,3d4e5f6a-7b8c-9012-3456-789012def012,user_84,80.0,true,2025-10-03 12:01:00,Passed,101,2024-02-03,4.2
|
||||||
|
85,4e5f6a7b-8c9d-0123-4567-890123ef0123,user_85,79.0,true,2025-11-08 10:10:10,Average,103,2024-02-04,3.8
|
||||||
|
86,5f6a7b8c-9d0e-1234-5678-901234f01234,user_86,95.0,true,2025-12-14 16:20:25,,104,2024-02-05,4.9
|
||||||
|
87,6a7b8c9d-0e1f-2345-6789-012345012345,user_87,84.5,true,2025-01-20 11:30:00,Good,101,2024-02-06,4.3
|
||||||
|
88,7b8c9d0e-1f2a-3456-7890-123456123456,user_88,62.0,false,2025-02-25 19:00:50,Needs help,102,2024-02-07,3.0
|
||||||
|
89,8c9d0e1f-2a3b-4567-8901-234567234567,user_89,87.0,true,2025-03-02 14:45:00,Great,103,2024-02-08,4.5
|
||||||
|
90,9d0e1f2a-3b4c-5678-9012-345678345678,user_90,91.8,true,2025-04-07 09:00:00,,104,2024-02-09,4.7
|
||||||
|
91,0e1f2a3b-4c5d-6789-0123-456789456789,user_91,94.2,true,2025-05-13 10:30:00,Top score,101,2024-02-10,4.8
|
||||||
|
92,1f2a3b4c-5d6e-7890-1234-567890567890,user_92,78.0,true,2025-06-18 11:45:10,Passed,102,2024-02-11,4.0
|
||||||
|
93,2a3b4c5d-6e7f-8901-2345-678901678901,user_93,71.5,false,2025-07-24 12:00:05,,101,2024-02-12,3.6
|
||||||
|
94,3b4c5d6e-7f8a-9012-3456-789012789012,user_94,86.3,true,2025-08-29 14:15:30,Good,103,2024-02-13,4.5
|
||||||
|
95,4c5d6e7f-8a9b-0123-4567-890123890123,user_95,72.0,true,2025-09-03 09:10:20,Review,102,2024-02-14,3.7
|
||||||
|
96,5d6e7f8a-9b0c-1234-5678-901234901234,user_96,83.0,true,2025-10-09 18:05:00,Passed,101,2024-02-15,4.2
|
||||||
|
97,6e7f8a9b-0c1d-2345-6789-012345012345,user_97,89.0,true,2025-11-14 13:20:45,,104,2024-02-16,4.6
|
||||||
|
98,7f8a9b0c-1d2e-3456-7890-123456123456,user_98,96.5,true,2025-12-20 10:00:00,Excellent,103,2024-02-17,4.9
|
||||||
|
99,8a9b0c1d-2e3f-4567-8901-234567234567,user_99,60.0,false,2025-01-25 20:30:15,Failed,102,2024-02-18,2.9
|
||||||
|
100,9b0c1d2e-3f4a-5678-9012-345678345678,user_100,92.5,true,2025-02-01 11:11:11,Great work,101,2024-02-19,4.8
|
||||||
|
\.
|
||||||
|
-- ```
|
||||||
|
-- **To run this script:**
|
||||||
|
-- 1. Save it as `setup_db.sql`.
|
||||||
|
-- 2. Use the `psql` command-line tool (which comes with Postgres) to execute it against your database (the one running in Docker):
|
||||||
|
-- ```bash
|
||||||
|
-- psql "postgresql://postgres:mysecretpassword@localhost:5433/postgres" -f setup_db.sql
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// --- External Crates ---
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use anyhow::{Context, Result, anyhow};
|
||||||
|
use std::fmt::{self, Display};
|
||||||
|
|
||||||
|
// --- 1. CUSTOM ERROR DEFINITION ---
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ConfigError(String);
|
||||||
|
|
||||||
|
impl Display for ConfigError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "Configuration Error: {}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::error::Error for ConfigError {}
|
||||||
|
|
||||||
|
|
||||||
|
// --- 2. CONFIGURATION STRUCTS (The "Configured Opinion") ---
|
||||||
|
// We make these 'pub' (public) so src/lib.rs can use them.
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct ColumnConfig {
|
||||||
|
pub column_name: String,
|
||||||
|
pub arrow_type: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct ConnectorConfig {
|
||||||
|
pub connection_string: String,
|
||||||
|
pub query: String, // <--- ADD THIS FIELD
|
||||||
|
pub schema: Vec<ColumnConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 3. CONFIG LOADING FUNCTION ---
|
||||||
|
// This is also 'pub' so src/lib.rs can call it.
|
||||||
|
pub fn load_and_validate_config(path: &str) -> Result<ConnectorConfig> {
|
||||||
|
let file_content = std::fs::read_to_string(path)
|
||||||
|
.context(format!("Failed to read config file at path: {}", path))?;
|
||||||
|
|
||||||
|
let config: ConnectorConfig = serde_yaml::from_str(&file_content)
|
||||||
|
.context("Failed to deserialize YAML configuration")?;
|
||||||
|
|
||||||
|
if config.connection_string.is_empty() {
|
||||||
|
return Err(ConfigError("Connection string cannot be empty.".to_string())).map_err(anyhow::Error::from)?;
|
||||||
|
}
|
||||||
|
if config.query.is_empty() {
|
||||||
|
return Err(ConfigError("Query cannot be empty.".to_string())).map_err(anyhow::Error::from)?;
|
||||||
|
}
|
||||||
|
// FIX: Make the COPY check more flexible (allows newlines)
|
||||||
|
let uppercase_query = config.query.trim().to_uppercase();
|
||||||
|
if !uppercase_query.starts_with("COPY") || !uppercase_query.contains("TO STDOUT") || !uppercase_query.contains("FORMAT BINARY") {
|
||||||
|
return Err(ConfigError("Query must be a 'COPY ... TO STDOUT (FORMAT binary)' command.".to_string())).map_err(anyhow::Error::from)?;
|
||||||
|
}
|
||||||
|
if config.schema.is_empty() {
|
||||||
|
return Err(ConfigError("Schema cannot be empty.".to_string())).map_err(anyhow::Error::from)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
271
src/parser.rs
271
src/parser.rs
@@ -0,0 +1,271 @@
|
|||||||
|
// --- External Crates ---
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio_postgres::{NoTls, CopyOutStream};
|
||||||
|
use anyhow::{Context, Result, anyhow};
|
||||||
|
// FIX: Import new builders
|
||||||
|
use arrow::array::{
|
||||||
|
ArrayBuilder, ArrayRef,
|
||||||
|
Int64Builder, Float64Builder, Float32Builder, StringBuilder, BooleanBuilder,
|
||||||
|
TimestampNanosecondBuilder, Date32Builder, Int32Builder
|
||||||
|
};
|
||||||
|
// FIX: Import new types
|
||||||
|
use arrow::datatypes::{
|
||||||
|
DataType, Field, Schema,
|
||||||
|
Float64Type, Float32Type, Int64Type, Int32Type, Utf8Type, BooleanType, TimestampNanosecondType,
|
||||||
|
Date32Type
|
||||||
|
};
|
||||||
|
use arrow::record_batch::RecordBatch;
|
||||||
|
use futures_util::stream::StreamExt;
|
||||||
|
use bytes::Bytes;
|
||||||
|
use byteorder::{BigEndian, ReadBytesExt};
|
||||||
|
use std::io::{Cursor, Read};
|
||||||
|
use std::str;
|
||||||
|
use chrono::{NaiveDateTime, NaiveDate};
|
||||||
|
|
||||||
|
// --- Internal Crates ---
|
||||||
|
use crate::config::ConnectorConfig;
|
||||||
|
|
||||||
|
|
||||||
|
// --- 1. CORE DATABASE LOGIC ---
|
||||||
|
pub async fn run_db_logic(config: ConnectorConfig) -> Result<()> {
|
||||||
|
// 1. Establish the connection
|
||||||
|
println!("UncheckedIO: Attempting connection...");
|
||||||
|
|
||||||
|
let (client, connection) = tokio_postgres::connect(&config.connection_string, NoTls).await
|
||||||
|
.context("Failed to connect to PostgreSQL database")?;
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(e) = connection.await {
|
||||||
|
eprintln!("Postgres connection error: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Execute the COPY TO STDOUT command
|
||||||
|
// FIX: We are now using the query from the config file!
|
||||||
|
let copy_query = &config.query;
|
||||||
|
println!("UncheckedIO: Executing user-defined query...");
|
||||||
|
|
||||||
|
let copy_stream = client.copy_out(copy_query.as_str()).await
|
||||||
|
.context("Failed to execute COPY TO STDOUT protocol. Check your query syntax and permissions.")?;
|
||||||
|
|
||||||
|
// 3. Handle the Binary Stream
|
||||||
|
let pinned_stream: Pin<Box<CopyOutStream>> = Box::pin(copy_stream);
|
||||||
|
|
||||||
|
// Build the Arrow Schema from the config
|
||||||
|
let schema_fields: Vec<Field> = config.schema.iter().map(|col_cfg| {
|
||||||
|
// We now allow "nullable" to be controlled by the column name, a temporary "hack"
|
||||||
|
let nullable = col_cfg.column_name == "notes";
|
||||||
|
|
||||||
|
let arrow_type = match col_cfg.arrow_type.as_str() {
|
||||||
|
"Int64" => DataType::Int64,
|
||||||
|
"Int32" => DataType::Int32, // NEW
|
||||||
|
"Float64" => DataType::Float64, // NEW
|
||||||
|
"Float32" => DataType::Float32,
|
||||||
|
"Utf8" | "String" => DataType::Utf8,
|
||||||
|
"Boolean" => DataType::Boolean,
|
||||||
|
"Timestamp(Nanosecond, None)" => DataType::Timestamp(arrow::datatypes::TimeUnit::Nanosecond, None),
|
||||||
|
"Date32" => DataType::Date32, // NEW
|
||||||
|
_ => panic!("Unsupported type in config: {}", col_cfg.arrow_type),
|
||||||
|
};
|
||||||
|
Field::new(&col_cfg.column_name, arrow_type, nullable)
|
||||||
|
}).collect();
|
||||||
|
let arrow_schema = Arc::new(Schema::new(schema_fields));
|
||||||
|
|
||||||
|
// Call the parser
|
||||||
|
let (rows_processed, record_batch) = handle_binary_copy(pinned_stream, arrow_schema.clone()).await?;
|
||||||
|
|
||||||
|
println!("UncheckedIO: Successfully parsed {} rows via binary stream.", rows_processed);
|
||||||
|
|
||||||
|
// 4. Final Output Confirmation
|
||||||
|
println!("UncheckedIO: Built RecordBatch with {} rows and {} columns.",
|
||||||
|
record_batch.num_rows(), record_batch.num_columns());
|
||||||
|
|
||||||
|
println!("UncheckedIO: Data transfer complete. We lived.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- 2. BINARY STREAM HANDLER (THE DYNAMIC PARSER) ---
|
||||||
|
|
||||||
|
// This enum will hold our different builder types
|
||||||
|
enum DynamicBuilder {
|
||||||
|
Int64(Box<Int64Builder>),
|
||||||
|
Int32(Box<Int32Builder>), // NEW
|
||||||
|
Float64(Box<Float64Builder>), // NEW
|
||||||
|
Float32(Box<Float32Builder>),
|
||||||
|
String(Box<StringBuilder>),
|
||||||
|
Boolean(Box<BooleanBuilder>),
|
||||||
|
Timestamp(Box<TimestampNanosecondBuilder>),
|
||||||
|
Date32(Box<Date32Builder>), // NEW
|
||||||
|
}
|
||||||
|
|
||||||
|
// Postgres Epoch for timestamps
|
||||||
|
const POSTGRES_EPOCH_NAIVE: NaiveDateTime = NaiveDate::from_ymd_opt(2000, 1, 1).unwrap().and_hms_opt(0, 0, 0).unwrap();
|
||||||
|
// Unix Epoch for dates
|
||||||
|
const UNIX_EPOCH_NAIVE_DATE: NaiveDate = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap();
|
||||||
|
|
||||||
|
// This function is now private to this module (it's not 'pub')
|
||||||
|
async fn handle_binary_copy(
|
||||||
|
mut stream: Pin<Box<CopyOutStream>>,
|
||||||
|
arrow_schema: Arc<Schema>
|
||||||
|
) -> Result<(usize, RecordBatch)> {
|
||||||
|
|
||||||
|
let mut binary_buffer: Vec<u8> = Vec::new();
|
||||||
|
|
||||||
|
// 1. Aggregate all bytes from the stream into our buffer
|
||||||
|
while let Some(segment_result) = stream.next().await {
|
||||||
|
let segment: Bytes = segment_result.context("Error reading segment from CopyOutStream")?;
|
||||||
|
binary_buffer.extend_from_slice(&segment);
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes_received = binary_buffer.len();
|
||||||
|
if bytes_received == 0 {
|
||||||
|
return Err(anyhow!("Received zero bytes from COPY stream. Check query and permissions."));
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("UncheckedIO: Received {} total bytes from stream.", bytes_received);
|
||||||
|
|
||||||
|
// --- PARSE POSTGRES BINARY HEADER ---
|
||||||
|
let mut cursor = Cursor::new(&binary_buffer[..]);
|
||||||
|
let mut magic_signature = [0u8; 11];
|
||||||
|
cursor.read_exact(&mut magic_signature).context("Failed to read magic signature")?;
|
||||||
|
if &magic_signature != b"PGCOPY\n\xff\r\n\0" {
|
||||||
|
return Err(anyhow!("Invalid Postgres COPY binary signature."));
|
||||||
|
}
|
||||||
|
let _flags = cursor.read_u32::<BigEndian>().context("Failed to read flags")?;
|
||||||
|
let _header_ext_len = cursor.read_u32::<BigEndian>().context("Failed to read header extension length")?;
|
||||||
|
println!("UncheckedIO: Postgres binary header validated.");
|
||||||
|
|
||||||
|
// --- DYNAMIC FIELD DESERIALIZATION ---
|
||||||
|
|
||||||
|
// 1. Create a dynamic list of builders based on the schema
|
||||||
|
let mut builders: Vec<DynamicBuilder> = arrow_schema.fields().iter().map(|field| {
|
||||||
|
match field.data_type() {
|
||||||
|
DataType::Int64 => DynamicBuilder::Int64(Box::new(Int64Builder::new())),
|
||||||
|
DataType::Int32 => DynamicBuilder::Int32(Box::new(Int32Builder::new())),
|
||||||
|
DataType::Float64 => DynamicBuilder::Float64(Box::new(Float64Builder::new())),
|
||||||
|
DataType::Float32 => DynamicBuilder::Float32(Box::new(Float32Builder::new())),
|
||||||
|
DataType::Utf8 => DynamicBuilder::String(Box::new(StringBuilder::new())),
|
||||||
|
DataType::Boolean => DynamicBuilder::Boolean(Box::new(BooleanBuilder::new())),
|
||||||
|
DataType::Timestamp(arrow::datatypes::TimeUnit::Nanosecond, None) => {
|
||||||
|
DynamicBuilder::Timestamp(Box::new(TimestampNanosecondBuilder::new()))
|
||||||
|
},
|
||||||
|
DataType::Date32 => DynamicBuilder::Date32(Box::new(Date32Builder::new())),
|
||||||
|
_ => panic!("Unsupported type in builder creation!"),
|
||||||
|
}
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
let mut rows_processed = 0;
|
||||||
|
|
||||||
|
// 2. Loop through the rest of the buffer until we hit the 2-byte trailer
|
||||||
|
loop {
|
||||||
|
let col_count = match cursor.read_i16::<BigEndian>() {
|
||||||
|
Ok(count) => count,
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
|
||||||
|
break; // Reached end of buffer *before* trailer, assume it's the end
|
||||||
|
}
|
||||||
|
Err(e) => return Err(e.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check for the 2-byte trailer (-1) which signals the end of the data
|
||||||
|
if col_count == -1 {
|
||||||
|
println!("UncheckedIO: Reached end-of-stream trailer.");
|
||||||
|
break; // End of stream
|
||||||
|
}
|
||||||
|
|
||||||
|
rows_processed += 1;
|
||||||
|
|
||||||
|
// 3. Dynamic Row-Parsing Loop
|
||||||
|
for (i, builder) in builders.iter_mut().enumerate() {
|
||||||
|
let field_len_i32 = cursor.read_i32::<BigEndian>()
|
||||||
|
.context(format!("Failed to read field length for col {}", i))?;
|
||||||
|
|
||||||
|
if field_len_i32 == -1 {
|
||||||
|
// Handle NULLs
|
||||||
|
match builder {
|
||||||
|
DynamicBuilder::Int64(b) => b.append_null(),
|
||||||
|
DynamicBuilder::Float64(b) => b.append_null(),
|
||||||
|
DynamicBuilder::Int32(b) => b.append_null(),
|
||||||
|
DynamicBuilder::Float32(b) => b.append_null(),
|
||||||
|
DynamicBuilder::String(b) => b.append_null(),
|
||||||
|
DynamicBuilder::Boolean(b) => b.append_null(),
|
||||||
|
DynamicBuilder::Timestamp(b) => b.append_null(),
|
||||||
|
DynamicBuilder::Date32(b) => b.append_null(),
|
||||||
|
}
|
||||||
|
continue; // Go to the next field
|
||||||
|
}
|
||||||
|
|
||||||
|
let field_len_usize = field_len_i32 as usize;
|
||||||
|
|
||||||
|
match builder {
|
||||||
|
DynamicBuilder::Int64(b) => {
|
||||||
|
let val = cursor.read_i64::<BigEndian>()?;
|
||||||
|
b.append_value(val);
|
||||||
|
}
|
||||||
|
DynamicBuilder::Int32(b) => {
|
||||||
|
let val = cursor.read_i32::<BigEndian>()?;
|
||||||
|
b.append_value(val);
|
||||||
|
}
|
||||||
|
DynamicBuilder::Float64(b) => {
|
||||||
|
let val = cursor.read_f64::<BigEndian>()?;
|
||||||
|
b.append_value(val);
|
||||||
|
}
|
||||||
|
DynamicBuilder::Float32(b) => {
|
||||||
|
let val = cursor.read_f32::<BigEndian>()?;
|
||||||
|
b.append_value(val);
|
||||||
|
}
|
||||||
|
DynamicBuilder::String(b) => {
|
||||||
|
let mut str_buf = vec![0; field_len_usize];
|
||||||
|
cursor.read_exact(&mut str_buf)?;
|
||||||
|
let val_str = str::from_utf8(&str_buf)
|
||||||
|
.context(format!("Failed to parse UTF-8 string for col {}", i))?;
|
||||||
|
b.append_value(val_str);
|
||||||
|
}
|
||||||
|
DynamicBuilder::Boolean(b) => {
|
||||||
|
let val_bool = cursor.read_u8()?;
|
||||||
|
b.append_value(val_bool != 0);
|
||||||
|
}
|
||||||
|
DynamicBuilder::Timestamp(b) => {
|
||||||
|
let pg_micros = cursor.read_i64::<BigEndian>()?;
|
||||||
|
let unix_epoch = NaiveDateTime::from_timestamp_opt(0, 0).unwrap();
|
||||||
|
let pg_epoch = POSTGRES_EPOCH_NAIVE;
|
||||||
|
let epoch_delta_micros = (pg_epoch - unix_epoch).num_microseconds().unwrap();
|
||||||
|
let unix_micros = epoch_delta_micros + pg_micros;
|
||||||
|
let unix_nanos = unix_micros * 1000;
|
||||||
|
b.append_value(unix_nanos);
|
||||||
|
}
|
||||||
|
DynamicBuilder::Date32(b) => {
|
||||||
|
// Postgres binary DATE is i32 days since 2000-01-01
|
||||||
|
let pg_days = cursor.read_i32::<BigEndian>()?;
|
||||||
|
// Arrow Date32 is i32 days since 1970-01-01 (Unix Epoch)
|
||||||
|
let epoch_delta_days = (POSTGRES_EPOCH_NAIVE.date() - UNIX_EPOCH_NAIVE_DATE).num_days() as i32;
|
||||||
|
let unix_days = epoch_delta_days + pg_days;
|
||||||
|
b.append_value(unix_days);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Finalize the Arrays
|
||||||
|
let final_columns: Vec<ArrayRef> = builders.into_iter().map(|builder| {
|
||||||
|
match builder {
|
||||||
|
DynamicBuilder::Int64(mut b) => Arc::new(b.finish()) as ArrayRef,
|
||||||
|
DynamicBuilder::Int32(mut b) => Arc::new(b.finish()) as ArrayRef,
|
||||||
|
DynamicBuilder::Float64(mut b) => Arc::new(b.finish()) as ArrayRef,
|
||||||
|
DynamicBuilder::Float32(mut b) => Arc::new(b.finish()) as ArrayRef,
|
||||||
|
DynamicBuilder::String(mut b) => Arc::new(b.finish()) as ArrayRef,
|
||||||
|
DynamicBuilder::Boolean(mut b) => Arc::new(b.finish()) as ArrayRef,
|
||||||
|
DynamicBuilder::Timestamp(mut b) => Arc::new(b.finish()) as ArrayRef,
|
||||||
|
DynamicBuilder::Date32(mut b) => Arc::new(b.finish()) as ArrayRef,
|
||||||
|
}
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
// 5. Build the Final RecordBatch
|
||||||
|
let record_batch = RecordBatch::try_new(
|
||||||
|
arrow_schema.clone(),
|
||||||
|
final_columns,
|
||||||
|
).context("Failed to create final Arrow RecordBatch")?;
|
||||||
|
|
||||||
|
Ok((rows_processed, record_batch))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user