├── .gitignore ├── Makefile ├── README.md ├── inventory ├── group_vars │ └── all.yml ├── host_vars │ ├── nginx.yml │ └── pgserver.yml ├── local.yml ├── prod.yml └── sit.yml ├── nginx ├── ansible │ └── tasks.yml └── sites.j2 ├── pgserver ├── ansible │ └── tasks.yml └── booktown.sql ├── playbook.yml ├── playground-executor ├── .dockerignore ├── Dockerfile ├── ansible │ └── tasks.yml ├── canary │ ├── go.mod │ ├── go.sum │ └── main.go └── entrypoint.sh ├── playground-webapp └── ansible │ └── tasks.yml ├── site ├── .dockerignore ├── .gitignore ├── Dockerfile ├── ansible │ └── tasks.yml ├── docs │ ├── authors.md │ ├── license.md │ └── v4 │ │ ├── adapter │ │ ├── cockroachdb │ │ │ └── index.md │ │ ├── index.md │ │ ├── mongo │ │ │ └── index.md │ │ ├── mssql │ │ │ └── index.md │ │ ├── mysql │ │ │ └── index.md │ │ ├── postgresql │ │ │ └── index.md │ │ ├── ql │ │ │ └── index.md │ │ └── sqlite │ │ │ └── index.md │ │ ├── getting-started │ │ ├── agnostic-db-api.md │ │ ├── connect-to-a-database.md │ │ ├── index.md │ │ ├── key-concepts.md │ │ ├── logger.md │ │ ├── sql-builder-api.md │ │ ├── struct-mapping.md │ │ └── transactions.md │ │ ├── index.md │ │ └── tutorial │ │ ├── index.md │ │ └── record-store-and-hooks.md ├── docusaurus.config.ts ├── package.json ├── plugins │ └── codeblock │ │ └── index.ts ├── sidebars.ts ├── src │ ├── components │ │ └── HomepageFeatures │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ ├── css │ │ └── custom.css │ ├── pages │ │ ├── index.module.css │ │ └── index.tsx │ └── theme │ │ └── Root.js ├── static │ ├── .nojekyll │ └── img │ │ ├── favicon.svg │ │ ├── gopher.svg │ │ ├── landscape.png │ │ ├── session-collection-result.png │ │ └── square.png ├── tsconfig.json └── yarn.lock ├── tour ├── .dockerignore ├── .gitignore ├── Dockerfile ├── ansible │ └── tasks.yml ├── go.mod ├── go.sum ├── static │ ├── css │ │ ├── codemirror-material.css │ │ ├── main.css │ │ ├── style.css │ │ ├── syntax.css │ │ └── upper.css │ ├── favicon.svg │ ├── img │ │ └── gopher-tip.png │ ├── index.html │ └── js │ │ └── main.js └── tutorials │ ├── basics │ ├── 01 │ │ ├── README.md │ │ └── main.go │ ├── 02 │ │ ├── README.md │ │ └── main.go │ └── 03 │ │ ├── README.md │ │ └── main.go │ ├── final │ └── 01 │ │ ├── README.md │ │ └── main.go │ ├── queries │ ├── 01 │ │ ├── README.md │ │ └── main.go │ ├── 02 │ │ ├── README.md │ │ └── main.go │ ├── 03 │ │ ├── README.md │ │ └── main.go │ ├── 04 │ │ ├── README.md │ │ └── main.go │ ├── 05 │ │ ├── README.md │ │ └── main.go │ └── 06 │ │ ├── README.md │ │ └── main.go │ ├── records │ ├── 01 │ │ ├── README.md │ │ └── main.go │ └── 02 │ │ ├── README.md │ │ └── main.go │ ├── sql-builder │ ├── 01 │ │ ├── README.md │ │ └── main.go │ ├── 02 │ │ ├── README.md │ │ └── main.go │ ├── 03 │ │ ├── README.md │ │ └── main.go │ └── 04 │ │ ├── README.md │ │ └── main.go │ ├── transactions │ └── 01 │ │ ├── README.md │ │ └── main.go │ └── welcome │ └── 01 │ ├── README.md │ └── main.go └── vanity ├── Dockerfile └── ansible └── tasks.yml /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw? 2 | *.txt 3 | *.log 4 | *.retry 5 | *.DS_Store 6 | node_modules/ 7 | vendor 8 | .venv 9 | bin/* 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GIT_SHORTHASH := $(shell git rev-parse --short HEAD) 2 | DOCKER_IMAGE_TAG := $(GIT_SHORTHASH) 3 | 4 | VIRTUAL_ENV ?= ./.venv 5 | 6 | export VIRTUAL_ENV 7 | 8 | export DOCKER_IMAGE_TAG 9 | 10 | docker-build: \ 11 | docker-build-site \ 12 | docker-build-vanity \ 13 | docker-build-playground-executor \ 14 | docker-build-tour 15 | 16 | docker-push: \ 17 | docker-push-site \ 18 | docker-push-vanity \ 19 | docker-push-playground-executor \ 20 | docker-push-tour 21 | 22 | docker-build-%: 23 | docker build -t "upper/${*}:${DOCKER_IMAGE_TAG}" -f "${*}/Dockerfile" "${*}/" 24 | 25 | docker-push-%: docker-build-% 26 | docker tag "upper/${*}:${DOCKER_IMAGE_TAG}" "upper/${*}:latest" 27 | docker push "upper/${*}:${DOCKER_IMAGE_TAG}" && \ 28 | docker push "upper/${*}:latest" 29 | 30 | deploy-%: 31 | ansible-playbook \ 32 | -i "./inventory/${*}.yml" \ 33 | -v playbook.yml 34 | 35 | deploy: \ 36 | deploy-local 37 | 38 | yamlfmt: 39 | find . -name \*.yml | grep -v node_modules | xargs yamlfmt 40 | 41 | run: 42 | (cd site && \ 43 | yarn start --host 0.0.0.0) 44 | 45 | run-tour: 46 | cd tour && \ 47 | go run cmd/tour/main.go 48 | 49 | build: 50 | (cd site && \ 51 | yarn build) 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # upper.io 2 | 3 | This is the source code for the upper.io website. 4 | -------------------------------------------------------------------------------- /inventory/group_vars/all.yml: -------------------------------------------------------------------------------- 1 | ansible_user: "root" 2 | services_base_dir: "/var/data/services" 3 | virtualenv_dir: "{{ services_base_dir }}/venv" 4 | network_name: "upper" 5 | container_prefix: "upper-" 6 | healthcheck_retries: 5 7 | healthcheck_interval: 2 8 | docker_image_tag: "{{ lookup('env', 'DOCKER_IMAGE_TAG') | default('latest') }}" 9 | pgserver_demo_user: "demo" 10 | pgserver_demo_password: "b4dp4ss" 11 | public_docker_registry: "mirror.gcr.io" 12 | -------------------------------------------------------------------------------- /inventory/host_vars/nginx.yml: -------------------------------------------------------------------------------- 1 | public_docker_registry: "public.ecr.aws/docker/library" 2 | 3 | -------------------------------------------------------------------------------- /inventory/host_vars/pgserver.yml: -------------------------------------------------------------------------------- 1 | public_docker_registry: "public.ecr.aws/docker/library" 2 | 3 | -------------------------------------------------------------------------------- /inventory/local.yml: -------------------------------------------------------------------------------- 1 | local: 2 | vars: 3 | env: "local" 4 | ansible_host: "127.0.0.1" 5 | container_bind_ip: "127.0.0.1" 6 | tour_url: "https://upper-tour.wip.xiam.dev" 7 | playground_url: "https://upper-demo.wip.xiam.dev" 8 | site_url: "https://upper-site.wip.xiam.dev" 9 | hosts: 10 | pgserver: 11 | playground_executor: 12 | playground_webapp: 13 | tour: 14 | site: 15 | vanity: 16 | nginx: 17 | container_bind_ip: "0.0.0.0" 18 | container_bind_port: "1754" 19 | -------------------------------------------------------------------------------- /inventory/prod.yml: -------------------------------------------------------------------------------- 1 | prod: 2 | vars: 3 | env: "prod" 4 | ansible_host: "prod.internal.upper.io" 5 | container_bind_ip: "0.0.0.0" 6 | tour_url: "https://tour.upper.io" 7 | playground_url: "https://demo.upper.io" 8 | site_url: "https://upper.io" 9 | hosts: 10 | pgserver: 11 | playground_executor: 12 | playground_webapp: 13 | tour: 14 | site: 15 | vanity: 16 | nginx: 17 | -------------------------------------------------------------------------------- /inventory/sit.yml: -------------------------------------------------------------------------------- 1 | sit: 2 | vars: 3 | env: "dev" 4 | ansible_host: "sit.oslab.internal" 5 | container_bind_ip: "127.0.0.1" 6 | tour_url: "https://upper-tour.sit.xiam.dev" 7 | playground_url: "https://upper-demo.sit.xiam.dev" 8 | site_url: "https://upper-site.sit.xiam.dev" 9 | hosts: 10 | pgserver: 11 | playground_executor: 12 | playground_webapp: 13 | tour: 14 | site: 15 | vanity: 16 | nginx: 17 | container_bind_ip: "10.0.2.102" 18 | container_bind_port: "2880" 19 | -------------------------------------------------------------------------------- /nginx/ansible/tasks.yml: -------------------------------------------------------------------------------- 1 | - name: "set container facts" 2 | set_fact: 3 | image_name: "nginx" 4 | image_tag: "alpine" 5 | container_name: "{{ container_prefix}}nginx" 6 | container_base_dir: "{{ services_base_dir }}/nginx" 7 | app_bind_port: "80" 8 | - name: "set docker facts" 9 | set_fact: 10 | image_full_name: "{{ public_docker_registry}}/{{ image_name }}:{{ image_tag }}" 11 | container_bind_port: "{{ container_bind_port | default(app_bind_port) }}" 12 | - name: "create config directory" 13 | file: 14 | path: "{{ container_base_dir }}/config" 15 | state: "directory" 16 | - name: "upload config file" 17 | copy: 18 | content: "{{ lookup('template', '../sites.j2') }}" 19 | dest: "{{ container_base_dir }}/config/sites" 20 | - name: "test nginx config" 21 | command: 22 | docker run --rm -v {{ container_base_dir }}/config/sites:/etc/nginx/conf.d/default.conf {{ image_full_name }} nginx -t 23 | register: nginx_test 24 | - name: "stop if nginx config is invalid" 25 | fail: 26 | msg: "nginx config is invalid" 27 | when: "nginx_test.rc != 0" 28 | - name: "pull docker image: {{ image_full_name }}" 29 | docker_image: 30 | name: "{{ image_full_name }}" 31 | source: "pull" 32 | state: "present" 33 | - name: "stop container: {{ container_name }}" 34 | docker_container: 35 | name: "{{ container_name }}" 36 | state: "stopped" 37 | ignore_errors: "yes" 38 | - name: "(re)start container: {{ container_name }}" 39 | docker_container: 40 | name: "{{ container_name }}" 41 | image: "{{ image_full_name }}" 42 | state: "started" 43 | restart_policy: "always" 44 | volumes: 45 | - "{{ container_base_dir }}/config/sites:/etc/nginx/conf.d/default.conf" 46 | keep_volumes: "no" 47 | networks: 48 | - name: "{{ network_name }}" 49 | ports: 50 | - "{{ container_bind_ip }}:{{ container_bind_port }}:{{ app_bind_port }}" 51 | env: 52 | ENV: "{{ env }}" 53 | -------------------------------------------------------------------------------- /nginx/sites.j2: -------------------------------------------------------------------------------- 1 | server { 2 | server_name 3 | upper.io 4 | upper-site.wip.xiam.dev 5 | upper-site.sit.xiam.dev 6 | ; 7 | 8 | set $site http://upper-site:80; 9 | set $vanity http://upper-vanity:9001; 10 | 11 | resolver 127.0.0.11 valid=10s; 12 | 13 | root /var/www; 14 | 15 | set $upstream $site; 16 | 17 | if ($arg_go-get = 1) { 18 | set $upstream $vanity; 19 | } 20 | 21 | if ($request_uri ~ git-upload-pack) { 22 | set $upstream $vanity; 23 | } 24 | 25 | location /tour { 26 | rewrite ^/tour(/.*)?$ {{ tour_url }}$1 permanent; 27 | } 28 | 29 | location / { 30 | proxy_set_header X-Real-IP $remote_addr; 31 | proxy_set_header Host $host; 32 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 33 | 34 | proxy_pass $upstream; 35 | } 36 | } 37 | 38 | server { 39 | server_name 40 | tour.upper.io 41 | upper-tour.wip.xiam.dev 42 | upper-tour.sit.xiam.dev 43 | ; 44 | 45 | set $tour http://upper-tour:4000; 46 | 47 | resolver 127.0.0.11 valid=10s; 48 | 49 | root /var/www; 50 | 51 | location / { 52 | proxy_set_header X-Real-IP $remote_addr; 53 | proxy_set_header Host $host; 54 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 55 | 56 | proxy_pass $tour; 57 | } 58 | } 59 | 60 | server { 61 | server_name 62 | demo.upper.io 63 | upper-demo.wip.xiam.dev 64 | upper-demo.sit.xiam.dev 65 | ; 66 | 67 | set $webapp http://upper-playground-webapp:3000; 68 | set $executor http://upper-playground-executor:3003; 69 | 70 | resolver 127.0.0.11 valid=10s; 71 | 72 | location = / { 73 | return 302 {{ tour_url }}; 74 | } 75 | 76 | location /compile { 77 | if ($request_method = 'OPTIONS') { 78 | add_header 'Access-Control-Allow-Origin' '*'; 79 | add_header 'Access-Control-Allow-Methods' 'POST, OPTIONS'; 80 | add_header 'Access-Control-Max-Age' 1728000; 81 | add_header 'Content-Type' 'text/plain; charset=utf-8'; 82 | add_header 'Content-Length' 0; 83 | return 204; 84 | } 85 | 86 | if ($request_method = 'POST') { 87 | add_header 'Access-Control-Allow-Origin' '*'; 88 | add_header 'Access-Control-Allow-Methods' 'POST, OPTIONS'; 89 | add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range'; 90 | } 91 | 92 | proxy_set_header X-Real-IP $remote_addr; 93 | proxy_set_header Host $host; 94 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 95 | 96 | proxy_pass $executor; 97 | } 98 | 99 | location / { 100 | proxy_set_header X-Real-IP $remote_addr; 101 | proxy_set_header Host $host; 102 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 103 | 104 | proxy_pass $webapp; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /pgserver/ansible/tasks.yml: -------------------------------------------------------------------------------- 1 | - name: "set container facts" 2 | set_fact: 3 | image_name: "postgres" 4 | image_tag: "17" 5 | container_name: "{{ container_prefix }}postgres" 6 | container_base_dir: "{{ services_base_dir }}/postgres" 7 | postgres_user: "postgres" 8 | postgres_password: "{{ lookup('password', '/dev/null length=20 chars=ascii_letters,digits') }}" 9 | - name: "set docker facts" 10 | set_fact: 11 | image_full_name: "{{ public_docker_registry}}/{{ image_name }}:{{ image_tag }}" 12 | booktown_sql: "{{ container_base_dir }}/booktown.sql" 13 | - name: "create data directory" 14 | file: 15 | path: "{{ container_base_dir }}" 16 | state: "directory" 17 | - name: "upload booktown.sql" 18 | copy: 19 | src: "../booktown.sql" 20 | dest: "{{ container_base_dir }}/booktown.sql" 21 | - name: "pull docker image: {{ image_full_name }}" 22 | docker_image: 23 | name: "{{ image_full_name }}" 24 | source: "pull" 25 | state: "present" 26 | - name: "stop container: {{ container_name }}" 27 | docker_container: 28 | name: "{{ container_name }}" 29 | state: "absent" 30 | - name: "(re)start container: {{ container_name }}" 31 | docker_container: 32 | name: "{{ container_name }}" 33 | image: "{{ image_full_name }}" 34 | state: "started" 35 | memory: 256m 36 | hostname: "postgres" 37 | memory_swap: 0 38 | memory_swappiness: 0 39 | ulimits: 40 | - nofile:256:512 41 | - nproc:128 42 | volumes: 43 | - "{{ booktown_sql }}:/booktown.sql" 44 | restart_policy: "always" 45 | ports: 46 | - "{{ container_bind_ip }}:5432:5432" 47 | env: 48 | POSTGRES_USER: "{{ postgres_user }}" 49 | POSTGRES_PASSWORD: "{{ postgres_password }}" 50 | - name: "wait until postgres is ready" 51 | postgresql_ping: 52 | login_host: "{{ container_bind_ip }}" 53 | login_password: "{{ postgres_password }}" 54 | login_user: "{{ postgres_user }}" 55 | register: pgserver_ping 56 | until: pgserver_ping.is_available == true and pgserver_ping.failed == false 57 | retries: "{{ healthcheck_retries }}" 58 | delay: "{{ healthcheck_interval }}" 59 | - name: "remove demo database" 60 | postgresql_db: 61 | login_host: "{{ container_bind_ip }}" 62 | login_password: "{{ postgres_password }}" 63 | login_user: "{{ postgres_user }}" 64 | name: "booktown" 65 | state: "absent" 66 | - name: "remove demo user" 67 | postgresql_user: 68 | login_host: "{{ container_bind_ip }}" 69 | login_password: "{{ postgres_password }}" 70 | login_user: "{{ postgres_user }}" 71 | name: "{{ pgserver_demo_user }}" 72 | state: "absent" 73 | - name: "add demo user" 74 | postgresql_user: 75 | login_host: "{{ container_bind_ip }}" 76 | login_password: "{{ postgres_password }}" 77 | login_user: "{{ postgres_user }}" 78 | role_attr_flags: "LOGIN" 79 | name: "{{ pgserver_demo_user }}" 80 | password: "{{ pgserver_demo_password }}" 81 | - name: "add demo database" 82 | postgresql_db: 83 | login_host: "{{ container_bind_ip }}" 84 | login_password: "{{ postgres_password }}" 85 | login_user: "{{ postgres_user }}" 86 | name: booktown 87 | owner: "{{ pgserver_demo_user }}" 88 | - name: "load demo data" 89 | shell: "docker exec -t {{ container_name }} bash -c 'psql -U{{ pgserver_demo_user }} booktown < /booktown.sql'" 90 | - name: "remove privileges from demo user" 91 | postgresql_privs: 92 | login_host: "{{ container_bind_ip }}" 93 | login_password: "{{ postgres_password }}" 94 | login_user: "{{ postgres_user }}" 95 | db: booktown 96 | state: absent 97 | roles: "{{ pgserver_demo_user }}" 98 | priv: ALL 99 | objs: ALL_IN_SCHEMA 100 | - name: "grant read-only privileges to demo user" 101 | postgresql_privs: 102 | login_host: "{{ container_bind_ip }}" 103 | login_password: "{{ postgres_password }}" 104 | login_user: "{{ postgres_user }}" 105 | db: booktown 106 | roles: "{{ pgserver_demo_user }}" 107 | priv: SELECT 108 | objs: ALL_IN_SCHEMA 109 | - name: "add container to network" 110 | docker_network: 111 | name: "{{ network_name }}" 112 | state: "present" 113 | appends: "yes" 114 | connected: 115 | - "{{ container_name }}" 116 | -------------------------------------------------------------------------------- /playbook.yml: -------------------------------------------------------------------------------- 1 | - hosts: "all" 2 | tasks: 3 | - name: "create services directory" 4 | file: 5 | path: "{{ services_base_dir }}" 6 | state: "directory" 7 | mode: "0755" 8 | run_once: true 9 | - name: "push requirements" 10 | copy: 11 | src: "./requirements.txt" 12 | dest: "{{ services_base_dir }}/requirements.txt" 13 | run_once: true 14 | - name: "install requirements and create virtualenv" 15 | pip: 16 | requirements: "{{ services_base_dir }}/requirements.txt" 17 | virtualenv: "{{ virtualenv_dir }}" 18 | virtualenv_command: "python3 -m venv" 19 | run_once: true 20 | 21 | - hosts: "all" 22 | tasks: 23 | - name: "set ansible_python_interpreter" 24 | set_fact: 25 | ansible_python_interpreter: "{{ virtualenv_dir }}/bin/python3" 26 | 27 | - hosts: "all" 28 | tasks: 29 | - name: "create shared network" 30 | docker_network: 31 | name: "{{ network_name }}" 32 | driver: "bridge" 33 | state: "present" 34 | run_once: true 35 | 36 | - hosts: "pgserver" 37 | tasks: 38 | - import_tasks: pgserver/ansible/tasks.yml 39 | 40 | - hosts: "playground_executor" 41 | tasks: 42 | - import_tasks: playground-executor/ansible/tasks.yml 43 | 44 | - hosts: "playground_webapp" 45 | tasks: 46 | - import_tasks: playground-webapp/ansible/tasks.yml 47 | 48 | - hosts: "tour" 49 | tasks: 50 | - import_tasks: tour/ansible/tasks.yml 51 | 52 | - hosts: "site" 53 | tasks: 54 | - import_tasks: site/ansible/tasks.yml 55 | 56 | - hosts: "vanity" 57 | tasks: 58 | - import_tasks: vanity/ansible/tasks.yml 59 | 60 | - hosts: "nginx" 61 | tasks: 62 | - import_tasks: nginx/ansible/tasks.yml 63 | -------------------------------------------------------------------------------- /playground-executor/.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !canary/go.sum 3 | !canary/go.mod 4 | !canary/main.go 5 | !entrypoint.sh 6 | -------------------------------------------------------------------------------- /playground-executor/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM xiam/go-playground:latest 2 | 3 | RUN apk update 4 | 5 | RUN apk add --no-cache \ 6 | bash \ 7 | curl \ 8 | gcc \ 9 | git \ 10 | htop \ 11 | iftop \ 12 | iptables \ 13 | net-tools \ 14 | netcat-openbsd \ 15 | shadow 16 | 17 | ENV GOROOT=/usr/local/go 18 | 19 | ENV WORKDIR=/home/playground 20 | 21 | RUN useradd -u 1000 -ms /bin/sh playground && \ 22 | usermod -aG playground playground 23 | 24 | COPY entrypoint.sh /bin/entrypoint.sh 25 | RUN chmod +x /bin/entrypoint.sh 26 | 27 | WORKDIR ${WORKDIR} 28 | 29 | # use canary build as base for the playground 30 | COPY canary ./playground 31 | 32 | RUN cd ./playground && \ 33 | go get -u ./... && \ 34 | go mod tidy && \ 35 | go mod vendor -o vendor && \ 36 | go build -v -o /dev/null && \ 37 | rm -f main.go 38 | 39 | EXPOSE 3000 40 | EXPOSE 3003 41 | 42 | ENTRYPOINT ["/bin/entrypoint.sh"] 43 | -------------------------------------------------------------------------------- /playground-executor/ansible/tasks.yml: -------------------------------------------------------------------------------- 1 | - set_fact: 2 | image_name: "upper/playground-executor" 3 | image_tag: "{{ docker_image_tag }}" 4 | container_name: "{{ container_prefix }}playground-executor" 5 | app_bind_port: 3003 6 | container_bind_port: 3003 7 | - set_fact: 8 | image_full_name: "{{ public_docker_registry }}/{{ image_name }}:{{ image_tag }}" 9 | - name: "pull docker image: {{ image_full_name }}" 10 | docker_image: 11 | name: "{{ image_full_name }}" 12 | source: "pull" 13 | state: "present" 14 | force_source: "yes" 15 | - name: "stop container: {{ container_name }}" 16 | docker_container: 17 | name: "{{ container_name }}" 18 | state: "absent" 19 | ignore_errors: "yes" 20 | - name: "(re)start container: {{ container_name }}" 21 | docker_container: 22 | name: "{{ container_name }}" 23 | image: "{{ image_full_name }}" 24 | state: "started" 25 | restart_policy: "always" 26 | memory: "2048M" 27 | memory_swap: "0" 28 | memory_swappiness: "0" 29 | privileged: "yes" 30 | ulimits: 31 | - nofile:512:1024 32 | - nproc:128 33 | networks: 34 | - name: "{{ network_name }}" 35 | ports: 36 | - "{{ container_bind_ip }}:{{ container_bind_port }}:{{ app_bind_port }}" 37 | env: 38 | ENV: "{{ env }}" 39 | - name: "health check (compile)" 40 | uri: 41 | url: "http://{{ container_bind_ip }}:{{ container_bind_port }}/compile" 42 | method: POST 43 | body: 'version=2&body=package+main%0A%0Aimport+%22fmt%22%0A%0Afunc+main()+%7B%0A%09fmt.Println(%22Hello%2C+playground%22)%0A%7D%0A' 44 | status_code: 45 | - 200 46 | return_content: yes 47 | register: response 48 | until: response.status == 200 49 | retries: "{{ healthcheck_retries }}" 50 | delay: "{{ healthcheck_interval }}" 51 | -------------------------------------------------------------------------------- /playground-executor/canary/go.mod: -------------------------------------------------------------------------------- 1 | module playground 2 | 3 | go 1.23.2 4 | 5 | require github.com/upper/db/v4 v4.9.0 6 | 7 | require ( 8 | filippo.io/edwards25519 v1.1.0 // indirect 9 | github.com/denisenkom/go-mssqldb v0.12.3 // indirect 10 | github.com/edsrzf/mmap-go v1.2.0 // indirect 11 | github.com/go-sql-driver/mysql v1.9.0 // indirect 12 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect 13 | github.com/golang-sql/sqlexp v0.1.0 // indirect 14 | github.com/jackc/pgio v1.0.0 // indirect 15 | github.com/jackc/pgpassfile v1.0.0 // indirect 16 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 17 | github.com/jackc/pgtype v1.14.4 // indirect 18 | github.com/jackc/pgx/v5 v5.7.2 // indirect 19 | github.com/jackc/puddle/v2 v2.2.2 // indirect 20 | github.com/lib/pq v1.10.9 // indirect 21 | github.com/mattn/go-sqlite3 v1.14.24 // indirect 22 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 23 | github.com/segmentio/fasthash v1.0.3 // indirect 24 | golang.org/x/crypto v0.35.0 // indirect 25 | golang.org/x/sync v0.11.0 // indirect 26 | golang.org/x/sys v0.30.0 // indirect 27 | golang.org/x/text v0.22.0 // indirect 28 | gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 // indirect 29 | modernc.org/b v1.1.0 // indirect 30 | modernc.org/db v1.0.13 // indirect 31 | modernc.org/file v1.0.9 // indirect 32 | modernc.org/fileutil v1.3.0 // indirect 33 | modernc.org/golex v1.1.0 // indirect 34 | modernc.org/internal v1.1.1 // indirect 35 | modernc.org/lldb v1.0.8 // indirect 36 | modernc.org/mathutil v1.7.1 // indirect 37 | modernc.org/ql v1.4.11 // indirect 38 | modernc.org/sortutil v1.2.1 // indirect 39 | modernc.org/strutil v1.2.1 // indirect 40 | modernc.org/zappy v1.1.0 // indirect 41 | ) 42 | -------------------------------------------------------------------------------- /playground-executor/canary/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/upper/db/v4/adapter/cockroachdb" 5 | "github.com/upper/db/v4/adapter/mongo" 6 | "github.com/upper/db/v4/adapter/mssql" 7 | "github.com/upper/db/v4/adapter/mysql" 8 | "github.com/upper/db/v4/adapter/postgresql" 9 | "github.com/upper/db/v4/adapter/ql" 10 | "github.com/upper/db/v4/adapter/sqlite" 11 | ) 12 | 13 | func main() { 14 | _ = cockroachdb.Adapter 15 | _ = mongo.Adapter 16 | _ = mssql.Adapter 17 | _ = mysql.Adapter 18 | _ = postgresql.Adapter 19 | _ = sqlite.Adapter 20 | _ = ql.Adapter 21 | } 22 | -------------------------------------------------------------------------------- /playground-executor/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -xe 4 | 5 | if [ -z "$WORKDIR" ]; then 6 | echo "WORKDIR is not set" 7 | exit 1 8 | fi 9 | 10 | export CHROOTDIR="$WORKDIR/chroot" 11 | 12 | rm -rf "$CHROOTDIR" 13 | 14 | mkdir -p $CHROOTDIR/{dev,lib,bin,etc,proc,sys,go,ephemeral} 15 | 16 | # /lib 17 | cp /lib/ld-musl-x86_64.so.1 $CHROOTDIR/lib/ 18 | 19 | # /dev 20 | mkdir -p $CHROOTDIR/dev 21 | mknod -m 666 $CHROOTDIR/dev/null c 1 3 22 | mknod -m 666 $CHROOTDIR/dev/zero c 1 5 23 | mknod -m 666 $CHROOTDIR/dev/random c 1 8 24 | mknod -m 666 $CHROOTDIR/dev/urandom c 1 9 25 | 26 | # /proc 27 | mount -t proc proc $CHROOTDIR/proc 28 | 29 | # /sys 30 | mount -t sysfs sysfs $CHROOTDIR/sys 31 | 32 | # /go 33 | mount -o ro,bind /usr/local/go $CHROOTDIR/go 34 | 35 | # create user entries 36 | echo "nobody:x:65534:65534:::" > $CHROOTDIR/etc/passwd 37 | echo "nobody:x:65534:" > $CHROOTDIR/etc/group 38 | 39 | # enable DNS resolution 40 | echo "hosts: files dns" > $CHROOTDIR/etc/nsswitch.conf 41 | cp /etc/resolv.conf $CHROOTDIR/etc/resolv.conf 42 | 43 | # copy playground executor 44 | cp /app/bin/go-playground-executor $CHROOTDIR/bin/ 45 | 46 | # set permissions 47 | chown nobody:nobody $CHROOTDIR/bin/go-playground-executor 48 | chmod +x $CHROOTDIR/bin/go-playground-executor 49 | chmod u+s $CHROOTDIR/bin/go-playground-executor 50 | 51 | # prepare playground 52 | mount -t tmpfs -o defaults,size=1024M,nosuid,noexec,nodev,mode=1755,uid=0,gid=0 tmpfs $CHROOTDIR/ephemeral 53 | 54 | cp -r $WORKDIR/playground $CHROOTDIR/ephemeral/playground 55 | 56 | chown -R root:root $CHROOTDIR/ephemeral 57 | chmod -R 755 $CHROOTDIR/ephemeral 58 | 59 | # prepare directory for builds 60 | mkdir -p $CHROOTDIR/ephemeral/playground/builds 61 | mount -t tmpfs -o defaults,size=512M,nosuid,nodev,mode=1755,uid=65534,gid=65534 tmpfs $CHROOTDIR/ephemeral/playground/builds 62 | 63 | # prepare GOCACHE 64 | mkdir -p $CHROOTDIR/ephemeral/.gocache 65 | chown nobody:nobody $CHROOTDIR/ephemeral/.gocache 66 | 67 | # prepare GOTMPDIR 68 | mkdir -p $CHROOTDIR/ephemeral/.gotmp 69 | chown nobody:nobody $CHROOTDIR/ephemeral/.gotmp 70 | 71 | export HOME=$WORKDIR 72 | export PATH=/bin:/go/bin 73 | export GOROOT=/go 74 | 75 | unset WORKDIR 76 | unset GOLANG_URL 77 | unset GOPATH 78 | unset HOME 79 | 80 | cd $CHROOTDIR 81 | unset CHROOTDIR 82 | 83 | export CGO_ENABLED=0 84 | export TMPDIR=/ephemeral/playground/builds 85 | export GOTMPDIR=/ephemeral/.gotmp 86 | export GOCACHE=/ephemeral/.gocache 87 | 88 | if [ ! -z "$DEBUG" ]; then 89 | cp /bin/sh $CHROOTDIR/bin/sh 90 | cp /bin/ls $CHROOTDIR/bin/ls 91 | cp /bin/touch $CHROOTDIR/bin/touch 92 | cp /bin/cat $CHROOTDIR/bin/cat 93 | cp /bin/rm $CHROOTDIR/bin/rm 94 | cp /bin/mkdir $CHROOTDIR/bin/mkdir 95 | 96 | chown nobody:nobody $CHROOTDIR/bin/sh 97 | chmod +x $CHROOTDIR/bin/sh 98 | chmod u+s $CHROOTDIR/bin/sh 99 | 100 | echo "Entering debug mode" 101 | /usr/sbin/chroot . /bin/sh 102 | fi 103 | 104 | echo "Starting go-playground-executor" 105 | /usr/sbin/chroot . /bin/go-playground-executor 106 | -------------------------------------------------------------------------------- /playground-webapp/ansible/tasks.yml: -------------------------------------------------------------------------------- 1 | - set_fact: 2 | image_name: "xiam/go-playground" 3 | image_tag: "latest" 4 | container_name: "{{ container_prefix }}playground-webapp" 5 | app_bind_port: 3000 6 | container_bind_port: 3000 7 | data_dir: "{{ services_base_dir }}/playground" 8 | - set_fact: 9 | image_full_name: "{{ public_docker_registry }}/{{ image_name }}:{{ image_tag }}" 10 | - name: "pull docker image: {{ image_full_name }}" 11 | docker_image: 12 | name: "{{ image_full_name }}" 13 | source: "pull" 14 | state: "present" 15 | force_source: "yes" 16 | - name: "stop container: {{ container_name }}" 17 | docker_container: 18 | name: "{{ container_name }}" 19 | state: "stopped" 20 | ignore_errors: "yes" 21 | - name: "(re)start container: {{ container_name }}" 22 | docker_container: 23 | name: "{{ container_name }}" 24 | image: "{{ image_full_name }}" 25 | state: "started" 26 | restart_policy: "always" 27 | volumes: 28 | - "{{ data_dir }}:/data" 29 | networks: 30 | - name: "{{ network_name }}" 31 | ports: 32 | - "{{ container_bind_ip }}:{{ container_bind_port }}:{{ app_bind_port }}" 33 | env: 34 | ENV: "{{ env }}" 35 | command: ["go-playground-webapp", "-allow-share", "-db", "/data/playground.db", "-c", "http://upper-unsafebox:3003/compile?output=json"] 36 | - name: "health check" 37 | uri: 38 | url: "http://{{ container_bind_ip }}:{{ container_bind_port }}/" 39 | method: GET 40 | status_code: 41 | - 200 42 | return_content: yes 43 | register: response 44 | until: response.status == 200 45 | retries: "{{ healthcheck_retries }}" 46 | delay: "{{ healthcheck_interval }}" 47 | -------------------------------------------------------------------------------- /site/.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !docs 3 | !plugins 4 | !package.json 5 | !src 6 | !static 7 | !yarn.lock 8 | !*.ts 9 | -------------------------------------------------------------------------------- /site/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /site/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:current-alpine3.14 AS builder 2 | 3 | WORKDIR /app 4 | COPY . /app 5 | 6 | RUN yarn install && \ 7 | yarn build 8 | 9 | FROM nginx:alpine 10 | 11 | COPY --from=builder /app/build /usr/share/nginx/html 12 | 13 | EXPOSE 80 14 | 15 | CMD ["nginx", "-g", "daemon off;"] 16 | -------------------------------------------------------------------------------- /site/ansible/tasks.yml: -------------------------------------------------------------------------------- 1 | - set_fact: 2 | image_name: "upper/site" 3 | image_tag: "{{ docker_image_tag }}" 4 | container_name: "{{ container_prefix }}site" 5 | - set_fact: 6 | image_full_name: "{{ public_docker_registry }}/{{ image_name }}:{{ image_tag }}" 7 | - name: "pull docker image: {{ image_full_name }}" 8 | docker_image: 9 | name: "{{ image_full_name }}" 10 | source: "pull" 11 | state: "present" 12 | force_source: "yes" 13 | - name: "stop container: {{ container_name }}" 14 | docker_container: 15 | name: "{{ container_name }}" 16 | state: "stopped" 17 | ignore_errors: "yes" 18 | - name: "(re)start container: {{ container_name }}" 19 | docker_container: 20 | name: "{{ container_name }}" 21 | image: "{{ image_full_name }}" 22 | state: "started" 23 | restart_policy: "always" 24 | networks: 25 | - name: "{{ network_name }}" 26 | ports: 27 | - "{{ container_bind_ip }}:8080:80" 28 | env: 29 | ENV: "{{ env }}" 30 | -------------------------------------------------------------------------------- /site/docs/authors.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 100 3 | --- 4 | 5 | # Authors 6 | 7 | `upper/db` is a project by [José Nieto](https://xiam.dev) and 8 | [contributors](https://github.com/upper/db/graphs/contributors). 9 | -------------------------------------------------------------------------------- /site/docs/license.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 10 3 | --- 4 | 5 | # License 6 | 7 | ``` 8 | Copyright (c) 2012-present The upper/db authors. All rights reserved. 9 | 10 | MIT License 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining a copy of 13 | this software and associated documentation files (the "Software"), to deal in 14 | the Software without restriction, including without limitation the rights to 15 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 16 | the Software, and to permit persons to whom the Software is furnished to do so, 17 | subject to the following conditions: 18 | 19 | The above copyright notice and this permission notice shall be included in all 20 | copies or substantial portions of the Software. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 24 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 25 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 26 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 27 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 28 | ``` 29 | -------------------------------------------------------------------------------- /site/docs/v4/adapter/cockroachdb/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: CockroachDB 3 | --- 4 | 5 | The `cockroachdb` adapter for [CockroachDB][2] wraps the `github.com/lib/pq` 6 | driver written by [Blake Mizerany][1]. 7 | 8 | > Before starting to read this detailed information, it is advisable that you 9 | > take a look at the [getting started](/v4/getting-started) page so you become 10 | > acquainted with the basics of `upper/db`, and you can grasp concepts better. 11 | 12 | ## Installation 13 | 14 | Use `go get` to download and install the adapter: 15 | 16 | ``` 17 | go get github.com/upper/db/v4/adapter/cockroachdb 18 | ``` 19 | 20 | ## Setup 21 | 22 | Import the `cockroachdb` package into your application: 23 | 24 | ```go 25 | package main 26 | 27 | import ( 28 | "github.com/upper/db/v4/adapter/cockroachdb" 29 | ) 30 | ``` 31 | 32 | Define the `cockroachdb.ConnectionURL{}` struct: 33 | 34 | ```go 35 | // ConnectionURL defines the DSN attributes. 36 | type ConnectionURL struct { 37 | User string 38 | Password string 39 | Host string 40 | Database string 41 | Options map[string]string 42 | } 43 | ``` 44 | 45 | Pass the `cockroachdb.ConnectionURL` value as argument to `cockroachdb.Open()` 46 | to establish a connection to the server, and create a database session: 47 | 48 | ```go 49 | settings = cockroachdb.ConnectionURL{ 50 | ... 51 | } 52 | 53 | sess, err = cockroachdb.Open(settings) 54 | ... 55 | ``` 56 | 57 | > The `cockroachdb.ParseURL()` function is also provided in case you need to 58 | > convert the DSN into a `cockroachdb.ConnectionURL`: 59 | 60 | ```go 61 | // ParseURL parses a DSN into a ConnectionURL struct. 62 | cockroachdb.ParseURL(dsn string) (ConnectionURL, error) 63 | ``` 64 | 65 | Once the connection is established, you can start performing operations on the 66 | database. 67 | 68 | ### Example 69 | 70 | In the following example, a table named 'birthday' consisting of two columns 71 | ('name' and 'born') will be created. Before starting, the table will be 72 | searched in the database and, in the event it already exists, it will be 73 | removed. Then, three rows will be inserted into the table and checked for 74 | accuracy. To this end, the database will be queried and the matches 75 | (insertions) will be printed to standard output. 76 | 77 | The `birthday` table with the `name` and `born` columns is created with these 78 | SQL statements: 79 | 80 | ```sql 81 | --' example.sql 82 | DROP TABLE IF EXISTS "birthday"; 83 | 84 | CREATE TABLE "birthday" ( 85 | "name" CHARACTER VARYING(50), 86 | "born" TIMESTAMP 87 | ); 88 | ``` 89 | 90 | The `psql` command line tool is used to run the statements in the 91 | `upperio_tests` database: 92 | 93 | ``` 94 | cat example.sql | PGPASSWORD=upperio psql -Uupperio -h127.0.0.1 -p26257 upperio_tests 95 | ``` 96 | 97 | The rows are inserted into the `birthday` table. The database is queried for 98 | the insertions and is set to print them to standard output. 99 | 100 | ```go 101 | package main 102 | 103 | import ( 104 | "fmt" 105 | "log" 106 | "time" 107 | 108 | "github.com/upper/db/v4/adapter/cockroachdb" 109 | ) 110 | 111 | var settings = cockroachdb.ConnectionURL{ 112 | Database: `upperio_tests`, // Database name 113 | Host: `localhost`, // Server IP or name 114 | User: `upperio`, // Username 115 | Password: `upperio`, // Password 116 | } 117 | 118 | type Birthday struct { 119 | // The 'name' column of the 'birthday' table 120 | // is mapped to the 'name' property. 121 | Name string `db:"name"` 122 | 123 | // The 'born' column of the 'birthday' table 124 | // is mapped to the 'born' property. 125 | Born time.Time `db:"born"` 126 | } 127 | 128 | func main() { 129 | 130 | // The database connection is attempted. 131 | sess, err := cockroachdb.Open(settings) 132 | if err != nil { 133 | log.Fatalf("db.Open(): %q\n", err) 134 | } 135 | defer sess.Close() // Closing the session is a good practice. 136 | 137 | // The 'birthday' table is referenced. 138 | birthdayCollection := sess.Collection("birthday") 139 | 140 | // Any rows that might have been added between the creation of 141 | // the table and the execution of this function are removed. 142 | err = birthdayCollection.Truncate() 143 | if err != nil { 144 | log.Fatalf("Truncate(): %q\n", err) 145 | } 146 | 147 | // Three rows are inserted into the 'Birthday' table. 148 | birthdayCollection.Insert(Birthday{ 149 | Name: "Hayao Miyazaki", 150 | Born: time.Date(1941, time.January, 5, 0, 0, 0, 0, time.Local), 151 | }) 152 | 153 | birthdayCollection.Insert(Birthday{ 154 | Name: "Nobuo Uematsu", 155 | Born: time.Date(1959, time.March, 21, 0, 0, 0, 0, time.Local), 156 | }) 157 | 158 | birthdayCollection.Insert(Birthday{ 159 | Name: "Hironobu Sakaguchi", 160 | Born: time.Date(1962, time.November, 25, 0, 0, 0, 0, time.Local), 161 | }) 162 | 163 | // The database is queried for the rows inserted. 164 | res := birthdayCollection.Find() 165 | 166 | // The 'birthdays' variable is filled with the results found. 167 | var birthdays []Birthday 168 | 169 | err = res.All(&birthdays) 170 | if err != nil { 171 | log.Fatalf("res.All(): %q\n", err) 172 | } 173 | 174 | // The 'birthdays' variable is printed to stdout. 175 | for _, birthday := range birthdays { 176 | fmt.Printf("%s was born in %s.\n", 177 | birthday.Name, 178 | birthday.Born.Format("January 2, 2006"), 179 | ) 180 | } 181 | } 182 | ``` 183 | 184 | Compile the example and run it: 185 | 186 | ``` 187 | go run example.go 188 | ``` 189 | 190 | The output will be: 191 | 192 | 193 | ``` 194 | Hayao Miyazaki was born in January 5, 1941. 195 | Nobuo Uematsu was born in March 21, 1959. 196 | Hironobu Sakaguchi was born in November 25, 1962. 197 | ``` 198 | 199 | ## Driver particularities 200 | 201 | ### JSON Types 202 | 203 | You can save and retrieve data when using [JSON 204 | types](https://www.cockroachdb.org/docs/9.4/static/datatype-json.html). If you 205 | want to try this out, make sure the column type is `jsonb` and the field type 206 | is `cockroachdb.JSONB`: 207 | 208 | ``` 209 | import ( 210 | // ... 211 | "github.com/upper/db/v4/adapter/cockroachdb" 212 | // ... 213 | ) 214 | 215 | type Person struct { 216 | ... 217 | Properties cockroachdb.JSONB `db:"properties"` 218 | Meta cockroachdb.JSONB `db:"meta"` 219 | } 220 | ``` 221 | 222 | ### SQL builder 223 | 224 | You can use the SQL builder for any complex SQL query: 225 | 226 | ```go 227 | q := sess.SQL().Select( 228 | "p.id", 229 | "p.title AD publication_title", 230 | "a.name AS artist_name", 231 | ).From("artists AS a", "publication AS p"). 232 | Where("a.id = p.author_id") 233 | 234 | var publications []Publication 235 | if err = q.All(&publications); err != nil { 236 | log.Fatal(err) 237 | } 238 | ``` 239 | 240 | ### Auto-incremental Keys (Serial) 241 | 242 | If you want tables to generate a unique number automatically whenever a new 243 | record is inserted, you can use auto-incremental keys. In this case, the column 244 | must be defined as `SERIAL`. 245 | 246 | > In order for the ID to be returned by `db.Collection.Insert()`, the `SERIAL` 247 | > column must be set as `PRIMARY KEY` too. 248 | 249 | ```sql 250 | CREATE TABLE foo( 251 | id SERIAL PRIMARY KEY, 252 | title VARCHAR 253 | ); 254 | ``` 255 | 256 | Remember to use `omitempty` to specify that the ID field should be ignored if 257 | it has a zero value: 258 | 259 | ```go 260 | type Foo struct { 261 | ID int64 `db:"id,omitempty"` 262 | Title string `db:"title"` 263 | } 264 | ``` 265 | 266 | otherwise, an error will be returned. 267 | 268 | ### Helper functions 269 | 270 | Use `db.Func` to escape function names and arguments: 271 | 272 | ```go 273 | res = sess.Find().Select(db.Func("DISTINCT", "name")) 274 | ``` 275 | 276 | Use the `db.Raw()` function for strings that have to be interpreted literally: 277 | 278 | ```go 279 | res = sess.Find().Select(db.Raw("DISTINCT(name)")) 280 | ``` 281 | 282 | > `db.Raw` can also be used as a condition argument, similarly to `db.Cond`. 283 | 284 | ## Take the tour 285 | 286 | Get the real `upper/db` experience, take the [tour](/tour). 287 | 288 | [1]: https://github.com/lib/pq 289 | [2]: https://www.cockroachlabs.com/ 290 | -------------------------------------------------------------------------------- /site/docs/v4/adapter/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Adapters 3 | sidebar_position: 10 4 | --- 5 | -------------------------------------------------------------------------------- /site/docs/v4/adapter/mongo/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: MongoDB 3 | --- 4 | 5 | The `mongo` adapter for [MongoDB][3] wraps the `labix.org/v2/mgo` driver 6 | written by [Gustavo Niemeyer][1]. 7 | 8 | Please note that MongoDB: 9 | 10 | * Does not support transactions. 11 | * Does not support query builder. 12 | * Uses [bson][4] tags instead of `db` for mapping. 13 | 14 | ## Installation 15 | 16 | To use the package, you'll need the [bazaar][2] version control system: 17 | 18 | ``` 19 | sudo apt-get install bzr -y 20 | ``` 21 | 22 | Once this requirement is met, use `go get` to download and install the adapter: 23 | 24 | ``` 25 | go get github.com/upper/db/v4/adapter/mongo 26 | ``` 27 | 28 | ## Setup 29 | 30 | Import the `github.com/upper/db/v4/adapter/mongo` package into your 31 | application: 32 | 33 | ```go 34 | // main.go 35 | package main 36 | 37 | import ( 38 | "github.com/upper/db/v4/adapter/mongo" 39 | ) 40 | ``` 41 | 42 | Define the `mongo.ConnectionURL{}` struct: 43 | 44 | ```go 45 | // ConnectionURL defines the DSN attributes. 46 | type ConnectionURL struct { 47 | User string 48 | Password string 49 | Host string 50 | Database string 51 | Options map[string]string 52 | } 53 | ``` 54 | 55 | Pass the `mongo.ConnectionURL` value as argument to `mongo.Open()` so the 56 | `mongo.Database` session is created. 57 | 58 | ```go 59 | settings = mongo.ConnectionURL{ 60 | ... 61 | } 62 | 63 | sess, err = mongo.Open(settings) 64 | ... 65 | ``` 66 | 67 | > The `mongo.ParseURL()` function is also provided in case you need to convert 68 | > a DSN into a `mongo.ConnectionURL`: 69 | 70 | ```go 71 | // ParseURL parses s into a ConnectionURL struct. 72 | mongo.ParseURL(s string) (ConnectionURL, error) 73 | ``` 74 | 75 | Once the connection is established, you can start performing operations on the 76 | database. 77 | 78 | ### Example 79 | 80 | In the following example, a table named 'birthday' consisting of two columns 81 | ('name' and 'born') will be created. Before starting, the table will be 82 | searched in the database and, in the event it already exists, it will be 83 | removed. Then, three rows will be inserted into the table and checked for 84 | accuracy. To this end, the database will be queried and the matches 85 | (insertions) will be printed to standard output. 86 | 87 | The rows are inserted into the `birthday` table. The database is queried for 88 | the insertions and is set to print them to standard output. 89 | 90 | ```go 91 | // example.go 92 | 93 | package main 94 | 95 | import ( 96 | "fmt" 97 | "log" 98 | "time" 99 | 100 | "github.com/upper/db/v4/adapter/mongo" 101 | ) 102 | 103 | var settings = mongo.ConnectionURL{ 104 | Database: `upperio_tests`, // Database name 105 | Host: `127.0.0.1`, // Server IP or name 106 | } 107 | 108 | type Birthday struct { 109 | // The 'name' column of the 'birthday' table 110 | // is mapped to the 'name' property. 111 | Name string `bson:"name"` 112 | // The 'born' column of the 'birthday' table 113 | // is mapped to the 'born' property. 114 | Born time.Time `bson:"born"` 115 | } 116 | 117 | func main() { 118 | 119 | // The database connection is attempted. 120 | sess, err := mongo.Open(settings) 121 | if err != nil { 122 | log.Fatalf("db.Open(): %q\n", err) 123 | } 124 | defer sess.Close() // Closing the session is a good practice. 125 | 126 | // The 'birthday' table is referenced. 127 | birthdayCollection := sess.Collection("birthday") 128 | 129 | // Any rows that might have been added between the creation of 130 | // the table and the execution of this function are removed. 131 | err = birthdayCollection.Truncate() 132 | if err != nil { 133 | log.Fatalf("Truncate(): %q\n", err) 134 | } 135 | 136 | // Three rows are inserted into the 'Birthday' table. 137 | birthdayCollection.Insert(Birthday{ 138 | Name: "Hayao Miyazaki", 139 | Born: time.Date(1941, time.January, 5, 0, 0, 0, 0, time.Local), 140 | }) 141 | 142 | birthdayCollection.Insert(Birthday{ 143 | Name: "Nobuo Uematsu", 144 | Born: time.Date(1959, time.March, 21, 0, 0, 0, 0, time.Local), 145 | }) 146 | 147 | birthdayCollection.Insert(Birthday{ 148 | Name: "Hironobu Sakaguchi", 149 | Born: time.Date(1962, time.November, 25, 0, 0, 0, 0, time.Local), 150 | }) 151 | 152 | // The database is queried for the rows inserted. 153 | res := birthdayCollection.Find() 154 | 155 | // The 'birthdays' variable is filled with the results found. 156 | var birthday []Birthday 157 | 158 | err = res.All(&birthday) 159 | if err != nil { 160 | log.Fatalf("res.All(): %q\n", err) 161 | } 162 | 163 | // The 'birthdays' variable is printed to stdout. 164 | for _, birthday := range birthday { 165 | fmt.Printf( 166 | "%s was born in %s.\n", 167 | birthday.Name, 168 | birthday.Born.Format("January 2, 2006"), 169 | ) 170 | } 171 | } 172 | ``` 173 | 174 | Compile and run the example: 175 | 176 | ``` 177 | go run example.go 178 | ``` 179 | 180 | The output will be: 181 | 182 | 183 | ``` 184 | Hayao Miyazaki was born in January 5, 1941. 185 | Nobuo Uematsu was born in March 21, 1959. 186 | Hironobu Sakaguchi was born in November 25, 1962. 187 | ``` 188 | 189 | ## Take the tour 190 | 191 | Get the real `upper/db` experience, take the [tour](/tour). 192 | 193 | [1]: http://labix.org/v2/mgo 194 | [2]: http://bazaar.canonical.com/en/ 195 | [3]: http://www.mongodb.org/ 196 | [4]: http://labix.org/gobson 197 | -------------------------------------------------------------------------------- /site/docs/v4/adapter/mssql/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Microsoft SQL Server 3 | --- 4 | 5 | The `mssql` adapter for [SQL Server][2] wraps the 6 | `github.com/denisenkom/go-mssqldb` driver written by [denisenkom][1]. 7 | 8 | > Before starting to read this detailed information, it is advisable that you 9 | > take a look at the [getting started](/v4/getting-started) page so you become 10 | > acquainted with the basics of `upper/db`, and you can grasp concepts better. 11 | 12 | ## Installation 13 | 14 | Use `go get` to download and install the adapter: 15 | 16 | ``` 17 | go get github.com/upper/db/v4/adapter/mssql 18 | ``` 19 | 20 | ## Setup 21 | 22 | Import the `mssql` package into your application: 23 | 24 | ```go 25 | package main 26 | 27 | import ( 28 | "github.com/upper/db/v4/adapter/mssql" 29 | ) 30 | ``` 31 | 32 | Define the `mssql.ConnectionURL{}` struct: 33 | 34 | ```go 35 | // ConnectionURL defines the DSN attributes. 36 | type ConnectionURL struct { 37 | User string 38 | Password string 39 | Host string 40 | Database string 41 | Options map[string]string 42 | } 43 | ``` 44 | 45 | Pass the `mssql.ConnectionURL` value as argument to `mssql.Open()` so the 46 | session is created. 47 | 48 | ```go 49 | settings = mssql.ConnectionURL{ 50 | ... 51 | } 52 | 53 | sess, err = mssql.Open(settings) 54 | ... 55 | ``` 56 | 57 | > Use the `mssql.ParseURL()` function to convert a DSN into a 58 | > `mssql.ConnectionURL`: 59 | 60 | ```go 61 | // ParseURL parses a DSN into a ConnectionURL struct. 62 | mssql.ParseURL(dsn string) (ConnectionURL, error) 63 | ``` 64 | 65 | Once the connection is established, you can start performing operations on the 66 | database. 67 | 68 | ### Example 69 | 70 | In the following example, a table named ‘birthday’ consisting of two columns 71 | (‘name’ and ‘born’) will be created. Before starting, the table will be 72 | searched in the database and, in the event it already exists, it will be 73 | removed. Then, three rows will be inserted into the table and checked for 74 | accuracy. To this end, the database will be queried and the matches 75 | (insertions) will be printed to standard output. 76 | 77 | The `birthday` table with the `name` and `born` columns is created with these 78 | SQL statements: 79 | 80 | ```sql 81 | --' example.sql 82 | CREATE TABLE [birthdays] ( 83 | id BIGINT PRIMARY KEY NOT NULL IDENTITY(1,1), 84 | name NVARCHAR(50), 85 | born DATETIME, 86 | born_ut BIGINT 87 | ); 88 | ``` 89 | 90 | The `sqlcmd` command line tool is used to run the statements in the 91 | `upperio_tests` database: 92 | 93 | ``` 94 | sqlcmd -U upperio -P upperio -i example.sql 95 | ``` 96 | 97 | The rows are inserted into the `birthday` table. The database is queried for 98 | the insertions and is set to print them to standard output. 99 | 100 | ```go 101 | :13:51: missing ',' before newline in composite literal 102 | ``` 103 | 104 | Compile and run the example: 105 | 106 | ``` 107 | go run example.go 108 | ``` 109 | 110 | The output will be: 111 | 112 | ``` 113 | Hayao Miyazaki was born in January 5, 1941. 114 | Nobuo Uematsu was born in March 21, 1959. 115 | Hironobu Sakaguchi was born in November 25, 1962. 116 | ``` 117 | 118 | ## Adapter particularities 119 | 120 | ### JSON Types 121 | 122 | You can save and retrieve data when using [JSON 123 | types](https://docs.microsoft.com/en-us/sql/relational-databases/json/json-data-sql-server?view=sql-server-2017). 124 | If you want to try this out, make sure the column type is `json` and the field 125 | type is `mssql.JSON`: 126 | 127 | ``` 128 | import ( 129 | ... 130 | "github.com/upper/db/v4/adapter/mssql" 131 | ... 132 | ) 133 | 134 | type Person struct { 135 | ... 136 | Properties mssql.JSON `db:"properties"` 137 | Meta mssql.JSON `db:"meta"` 138 | } 139 | ``` 140 | 141 | > JSON types area supported on SQL Server 2016+. 142 | 143 | ### SQL Builder 144 | 145 | You can use the SQL builder for any complex SQL query: 146 | 147 | ```go 148 | q := sess.SQL().Select( 149 | "p.id", 150 | "p.title AD publication_title", 151 | "a.name AS artist_name", 152 | ).From("artists AS a", "publication AS p"). 153 | Where("a.id = p.author_id") 154 | 155 | var publications []Publication 156 | if err = q.All(&publications); err != nil { 157 | log.Fatal(err) 158 | } 159 | ``` 160 | 161 | ### Identity Columns 162 | 163 | If you want tables to generate a unique number automatically whenever a new 164 | record is inserted, you can use auto-incremental keys. In this case, the column 165 | must be defined as `IDENTITY(1, 1)`. 166 | 167 | > In order for the ID to be returned by `db.Collection.Insert()`, the 168 | > `IDENTITY` column must be set as `PRIMARY KEY` too. 169 | 170 | ```sql 171 | CREATE TABLE foo( 172 | id BIGINT PRIMARY KEY NOT NULL IDENTITY(1,1), 173 | title NVARCHAR(50) 174 | ); 175 | ``` 176 | 177 | Remember to use `omitempty` to specify that the ID field should be ignored if 178 | it has an empty value: 179 | 180 | ```go 181 | type Foo struct { 182 | ID int64 `db:"id,omitempty"` 183 | Title string `db:"title"` 184 | } 185 | ``` 186 | 187 | Otherwise, an error will be returned. 188 | 189 | ### Helper functions 190 | 191 | Use `db.Func` to escape function names and arguments: 192 | 193 | ```go 194 | res = sess.Find().Select(db.Func("DISTINCT", "name")) 195 | ``` 196 | 197 | Use the `db.Raw()` function for strings that have to be interpreted literally: 198 | 199 | ```go 200 | res = sess.Find().Select(db.Raw("DISTINCT(name)")) 201 | ``` 202 | 203 | > `db.Raw` can also be used as a condition argument, similarly to `db.Cond`. 204 | 205 | ## Take the tour 206 | 207 | Get the real `upper/db` experience, take the [tour](/tour). 208 | 209 | [1]: https://github.com/denisenkom 210 | [2]: https://www.microsoft.com/en-us/sql-server/sql-server-2016 211 | -------------------------------------------------------------------------------- /site/docs/v4/adapter/mysql/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: MySQL 3 | --- 4 | 5 | The `mysql` adapter for [MySQL][2] wraps the `github.com/go-sql-driver/mysql` 6 | driver written by [Julien Schmidt][1]. 7 | 8 | > Before starting to read this detailed information, it is advisable that you 9 | > take a look at the [getting started](/v4/getting-started) page so you become 10 | > acquainted with the basics of `upper/db`, and you can grasp concepts better. 11 | 12 | ## Installation 13 | 14 | Use `go get` to download and install the adapter: 15 | 16 | ``` 17 | go get github.com/upper/db/v4/adapter/mysql 18 | ``` 19 | 20 | ## Setup 21 | 22 | Import the `mysql` package into your application: 23 | 24 | ```go 25 | // main.go 26 | package main 27 | 28 | import ( 29 | "github.com/upper/db/v4/adapter/mysql" 30 | ) 31 | ``` 32 | 33 | Define the `mysql.ConnectionURL{}` struct: 34 | 35 | ```go 36 | // ConnectionURL defines the DSN attributes. 37 | type ConnectionURL struct { 38 | User string 39 | Password string 40 | Host string 41 | Database string 42 | Options map[string]string 43 | } 44 | ``` 45 | 46 | Pass the `mysql.ConnectionURL` value as argument to `mysql.Open()` to establish 47 | a connection to the server, and create a database session: 48 | 49 | ```go 50 | settings = mysql.ConnectionURL{ 51 | ... 52 | } 53 | 54 | sess, err = mysql.Open(settings) 55 | ... 56 | ``` 57 | 58 | > Use the `mysql.ParseURL()` function to convert a DSN into a 59 | > `mysql.ConnectionURL`: 60 | 61 | ```go 62 | // ParseURL parses a DSN into a ConnectionURL struct. 63 | mysql.ParseURL(dsn string) (ConnectionURL, error) 64 | ``` 65 | 66 | Once the connection is established, you can start performing operations on the 67 | database. 68 | 69 | ### Example 70 | 71 | In the following example, a table named ‘birthday’ consisting of two columns 72 | (‘name’ and ‘born’) will be created. Before starting, the table will be 73 | searched in the database and, in the event it already exists, it will be 74 | removed. Then, three rows will be inserted into the table and checked for 75 | accuracy. To this end, the database will be queried and the matches 76 | (insertions) will be printed to standard output. 77 | 78 | The `birthday` table with the `name` and `born` columns is created with these 79 | SQL statements: 80 | 81 | ```sql 82 | --' example.sql 83 | DROP TABLE IF EXISTS birthday; 84 | 85 | CREATE TABLE birthday ( 86 | `name` VARCHAR(50), 87 | `born` DATE 88 | ); 89 | ``` 90 | 91 | The `mysql` command line tool is used to run the statements in the 92 | `upperio_tests` database. 93 | 94 | ``` 95 | cat example.sql | mysql -uupperio -pupperio upperio_tests 96 | ``` 97 | 98 | The rows are inserted into the `birthday` table. The database is queried for 99 | the insertions and is set to print them to standard output. 100 | 101 | ```go 102 | package main 103 | 104 | import ( 105 | "fmt" 106 | "log" 107 | "time" 108 | 109 | "github.com/upper/db/v4/adapter/mysql" // Imports the mysql adapter. 110 | ) 111 | 112 | var settings = mysql.ConnectionURL{ 113 | Database: "upperio_tests", // Database name 114 | Host: "localhost", // Server IP or name 115 | User: "upperio", // Username 116 | Password: "upperio", // Password 117 | } 118 | 119 | type Birthday struct { 120 | // The 'name' column of the 'birthday' table 121 | // is mapped to the 'name' property. 122 | Name string `db:"name"` 123 | // The 'born' column of the 'birthday' table 124 | // is mapped to the 'born' property. 125 | Born time.Time `db:"born"` 126 | } 127 | 128 | func main() { 129 | 130 | // The database connection is attempted. 131 | sess, err := mysql.Open(settings) 132 | if err != nil { 133 | log.Fatalf("db.Open(): %q\n", err) 134 | } 135 | defer sess.Close() // Closing the session is a good practice. 136 | 137 | // The 'birthday' table is referenced. 138 | birthdayCollection := sess.Collection("birthday") 139 | 140 | // Any rows that might have been added between the creation of 141 | // the table and the execution of this function are removed. 142 | err = birthdayCollection.Truncate() 143 | if err != nil { 144 | log.Fatalf("Truncate(): %q\n", err) 145 | } 146 | 147 | // Three rows are inserted into the 'Birthday' table. 148 | birthdayCollection.Insert(Birthday{ 149 | Name: "Hayao Miyazaki", 150 | Born: time.Date(1941, time.January, 5, 0, 0, 0, 0, time.Local), 151 | }) 152 | 153 | birthdayCollection.Insert(Birthday{ 154 | Name: "Nobuo Uematsu", 155 | Born: time.Date(1959, time.March, 21, 0, 0, 0, 0, time.Local), 156 | }) 157 | 158 | birthdayCollection.Insert(Birthday{ 159 | Name: "Hironobu Sakaguchi", 160 | Born: time.Date(1962, time.November, 25, 0, 0, 0, 0, time.Local), 161 | }) 162 | 163 | // The database is queried for the rows inserted. 164 | var res db.Result 165 | res = birthdayCollection.Find() 166 | 167 | // The 'birthdays' variable is filled with the results found. 168 | var birthdays []Birthday 169 | 170 | err = res.All(&birthdays) 171 | if err != nil { 172 | log.Fatalf("res.All(): %q\n", err) 173 | } 174 | 175 | // The 'birthdays' variable is printed to stdout. 176 | for _, birthday := range birthdays { 177 | fmt.Printf("%s was born in %s.\n", 178 | birthday.Name, 179 | birthday.Born.Format("January 2, 2006"), 180 | ) 181 | } 182 | } 183 | ``` 184 | 185 | Compile and run the example: 186 | 187 | 188 | ``` 189 | go run example.go 190 | ``` 191 | 192 | The output will be: 193 | 194 | 195 | ``` 196 | Hayao Miyazaki was born in January 5, 1941. 197 | Nobuo Uematsu was born in March 21, 1959. 198 | Hironobu Sakaguchi was born in November 25, 1962. 199 | ``` 200 | 201 | ## Adapter particularities 202 | 203 | ### JSON Types 204 | 205 | You can save and retrieve data when using [JSON 206 | types](https://dev.mysql.com/doc/refman/8.0/en/json.html). If you want to try 207 | this out, make sure the column type is `json` and the field type is 208 | `mysql.JSON`: 209 | 210 | ``` 211 | import ( 212 | ... 213 | "github.com/upper/db/v4/adapter/mysql" 214 | ... 215 | ) 216 | 217 | type Person struct { 218 | ... 219 | Properties mysql.JSON `db:"properties"` 220 | Meta mysql.JSON `db:"meta"` 221 | } 222 | ``` 223 | 224 | > JSON types area supported on MySQL 8.0+. 225 | 226 | ### SQL Builder 227 | 228 | Use the SQL builder for any complex SQL query: 229 | 230 | ```go 231 | q := b.SQL().Select( 232 | "p.id", 233 | "p.title AS publication_title", 234 | "a.name AS artist_name", 235 | ).From("artists AS a", "publication AS p"). 236 | Where("a.id = p.author_id") 237 | 238 | var publications []Publication 239 | if err = q.All(&publications); err != nil { 240 | log.Fatal(err) 241 | } 242 | ``` 243 | 244 | ### Auto-incremental Keys 245 | 246 | If you want to generate a unique number automatically whenever a new record is 247 | inserted, you can use auto-incremental keys. In this case, the column must be 248 | defined as `NOT NULL AUTO_INCREMENT`. 249 | 250 | > In order for the ID to be returned by `db.Collection.Insert()`, the 251 | > `AUTO_INCREMENT` column must be set as `PRIMARY KEY` too. 252 | 253 | ```sql 254 | CREATE TABLE foo( 255 | id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, PRIMARY KEY(id), 256 | title VARCHAR(255) 257 | ); 258 | ``` 259 | 260 | Remember to use `omitempty` to specify that the ID field should be ignored if 261 | it has an empty value: 262 | 263 | ```go 264 | type Foo struct { 265 | ID int64 `db:"id,omitempty"` 266 | Title string `db:"title"` 267 | } 268 | ``` 269 | 270 | Otherwise, an error will be returned. 271 | 272 | ### Helper functions 273 | 274 | Use `db.Func` to escape function names and arguments: 275 | 276 | 277 | ```go 278 | res = sess.Find().Select(db.Func("DISTINCT", "name")) 279 | ``` 280 | 281 | Use the `db.Raw()` function for strings that have to be interpreted literally: 282 | 283 | ```go 284 | res = sess.Find().Select(db.Raw("DISTINCT(name)")) 285 | ``` 286 | 287 | > `db.Raw` can also be used as a condition argument, similarly to `db.Cond`. 288 | 289 | ## Take the tour 290 | 291 | Get the real `upper/db` experience, take the [tour](/tour). 292 | 293 | [1]: https://github.com/go-sql-driver/mysql 294 | [2]: http://www.mysql.com 295 | -------------------------------------------------------------------------------- /site/docs/v4/adapter/postgresql/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: PostgreSQL 3 | --- 4 | 5 | The `postgresql` adapter for PostgreSQL wraps the `github.com/lib/pq` driver 6 | written by Blake Mizerany. 7 | 8 | > Before starting to read this detailed information, it is advisable that you 9 | > take a look at the [getting started](/v4/getting-started) page so you become 10 | > acquainted with the basics of `upper/db`, and you can grasp concepts better. 11 | 12 | ## Installation 13 | 14 | Use `go get` to download and install the adapter: 15 | 16 | ``` 17 | go get github.com/upper/db/v4/adapter/postgresql 18 | ``` 19 | 20 | ## Setup 21 | 22 | Import the `postgresql` package into your application: 23 | 24 | ```go 25 | // main.go 26 | package main 27 | 28 | import ( 29 | "github.com/upper/db/v4/adapter/postgresql" 30 | ) 31 | ``` 32 | 33 | Define the `postgresql.ConnectionURL` struct: 34 | 35 | ```go 36 | // ConnectionURL defines the DSN attributes. 37 | type ConnectionURL struct { 38 | User string 39 | Password string 40 | Host string 41 | Database string 42 | Options map[string]string 43 | } 44 | ``` 45 | 46 | Pass the `postgresql.ConnectionURL` value as argument to `postgresql.Open()` to 47 | establish a connection to the server, and create a database session: 48 | 49 | ```go 50 | settings = postgresql.ConnectionURL{ 51 | ... 52 | } 53 | 54 | sess, err = postgresql.Open(settings) 55 | ... 56 | ``` 57 | 58 | > Use the `postgresql.ParseURL()` function to convert a DSN into 59 | > a `postgresql.ConnectionURL`: 60 | 61 | ```go 62 | // ParseURL parses a DSN into a ConnectionURL struct. 63 | postgresql.ParseURL(dsn string) (ConnectionURL, error) 64 | ``` 65 | 66 | Once the connection is established, you can start performing operations on the 67 | database. 68 | 69 | ## Example 70 | 71 | In the following example, a table named 'birthday' consisting of two columns 72 | ('name' and 'born') will be created. Before starting, the table will be 73 | searched in the database and, in the event it already exists, it will be 74 | removed. Then, three rows will be inserted into the table and checked for 75 | accuracy. To this end, the database will be queried and the matches 76 | (insertions) will be printed to standard output. 77 | 78 | The `birthday` table with the `name` and `born` columns is created with these 79 | SQL statements: 80 | 81 | ```sql 82 | --' example.sql 83 | DROP TABLE IF EXISTS "birthday"; 84 | 85 | CREATE TABLE "birthday" ( 86 | "name" CHARACTER VARYING(50), 87 | "born" TIMESTAMP 88 | ); 89 | ``` 90 | 91 | The `psql` command line tool is used to run the statements in 92 | the `upperio_tests` database: 93 | 94 | ```sql 95 | cat example.sql | PGPASSWORD=upperio psql -Uupperio upperio_tests 96 | ``` 97 | 98 | The rows are inserted into the birthday table. The database is queried for the 99 | insertions and is set to print them to standard output. 100 | 101 | ```go 102 | package main 103 | 104 | import ( 105 | "fmt" 106 | "log" 107 | "time" 108 | 109 | "github.com/upper/db/v4/adapter/postgresql" 110 | ) 111 | 112 | var settings = postgresql.ConnectionURL{ 113 | Database: `upperio_tests`, // Database name 114 | Host: `localhost`, // Server IP or name 115 | User: `upperio`, // Username 116 | Password: `upperio`, // Password 117 | } 118 | 119 | type Birthday struct { 120 | // The 'name' column of the 'birthday' table 121 | // is mapped to the 'name' property. 122 | Name string `db:"name"` 123 | 124 | // The 'born' column of the 'birthday' table 125 | // is mapped to the 'born' property. 126 | Born time.Time `db:"born"` 127 | } 128 | 129 | func main() { 130 | 131 | // The database connection is attempted. 132 | sess, err := postgresql.Open(settings) 133 | if err != nil { 134 | log.Fatalf("db.Open(): %q\n", err) 135 | } 136 | defer sess.Close() // Closing the session is a good practice. 137 | 138 | // The 'birthday' table is referenced. 139 | birthdayCollection := sess.Collection("birthday") 140 | 141 | // Any rows that might have been added between the creation of 142 | // the table and the execution of this function are removed. 143 | err = birthdayCollection.Truncate() 144 | if err != nil { 145 | log.Fatalf("Truncate(): %q\n", err) 146 | } 147 | 148 | // Three rows are inserted into the 'Birthday' table. 149 | birthdayCollection.Insert(Birthday{ 150 | Name: "Hayao Miyazaki", 151 | Born: time.Date(1941, time.January, 5, 0, 0, 0, 0, time.Local), 152 | }) 153 | 154 | birthdayCollection.Insert(Birthday{ 155 | Name: "Nobuo Uematsu", 156 | Born: time.Date(1959, time.March, 21, 0, 0, 0, 0, time.Local), 157 | }) 158 | 159 | birthdayCollection.Insert(Birthday{ 160 | Name: "Hironobu Sakaguchi", 161 | Born: time.Date(1962, time.November, 25, 0, 0, 0, 0, time.Local), 162 | }) 163 | 164 | // The database is queried for the rows inserted. 165 | res := birthdayCollection.Find() 166 | 167 | // The 'birthdays' variable is filled with the results found. 168 | var birthdays []Birthday 169 | 170 | err = res.All(&birthdays) 171 | if err != nil { 172 | log.Fatalf("res.All(): %q\n", err) 173 | } 174 | 175 | // The 'birthdays' variable is printed to stdout. 176 | for _, birthday := range birthdays { 177 | fmt.Printf("%s was born in %s.\n", 178 | birthday.Name, 179 | birthday.Born.Format("January 2, 2006"), 180 | ) 181 | } 182 | } 183 | ``` 184 | 185 | Compile the example and run it: 186 | 187 | ```go 188 | go run example.go 189 | ``` 190 | 191 | The output will be: 192 | 193 | ``` 194 | Hayao Miyazaki was born in January 5, 1941. 195 | Nobuo Uematsu was born in March 21, 1959. 196 | Hironobu Sakaguchi was born in November 25, 1962. 197 | ``` 198 | 199 | ## Adapter particularities 200 | 201 | ### JSON Types 202 | 203 | You can save and retrieve data when using JSON types. If you want to try this 204 | out, make sure the column type is JSONB and the field type 205 | is `postgresql.JSONB`: 206 | 207 | ```go 208 | package main 209 | 210 | import ( 211 | // ... 212 | "github.com/upper/db/v4/adapter/postgresql" 213 | // ... 214 | ) 215 | 216 | type Person struct { 217 | ... 218 | Properties postgresql.JSONB `db:"properties"` 219 | Meta postgresql.JSONB `db:"meta"` 220 | } 221 | ``` 222 | 223 | JSON types are supported on PostgreSQL 9.4+. In addition to these, the adapter 224 | features other custom types 225 | like `postgresql.StringArray` and `postgresql.Int64Array`. 226 | 227 | ### SQL builder 228 | 229 | Use the SQL builder for any complex SQL query: 230 | 231 | ```go 232 | q := sess.SQL().Select( 233 | "p.id", 234 | "p.title AD publication_title", 235 | "a.name AS artist_name", 236 | ).From("artists AS a", "publication AS p"). 237 | Where("a.id = p.author_id") 238 | 239 | var publications []Publication 240 | if err = q.All(&publications); err != nil { 241 | log.Fatal(err) 242 | } 243 | ``` 244 | 245 | ### Auto-incremental Keys (Serial) 246 | 247 | If you want tables to generate a unique number automatically whenever a new 248 | record is inserted, you can use auto-incremental keys. In this case, the column 249 | must be defined as SERIAL. 250 | 251 | For the ID to be returned by `db.Collection.Insert()`, the SERIAL column must 252 | be set as PRIMARY KEY too. 253 | 254 | ```sql 255 | CREATE TABLE foo( 256 | id SERIAL PRIMARY KEY, 257 | title VARCHAR 258 | ); 259 | ``` 260 | 261 | Remember to use `omitempty` to specify that the ID field should be ignored if 262 | it has a zero value: 263 | 264 | ```go 265 | type Foo struct { 266 | ID int64 `db:"id,omitempty"` 267 | Title string `db:"title"` 268 | } 269 | ``` 270 | 271 | otherwise, an error will be returned. 272 | 273 | ### Helper functions 274 | 275 | Use `db.Func` to escape function names and arguments: 276 | 277 | ```go 278 | res = sess.Find().Select(db.Func("DISTINCT", "name")) 279 | ``` 280 | 281 | Use the `db.Raw()` function for strings that have to be interpreted literally: 282 | 283 | ```go 284 | res = sess.Find().Select(db.Raw("DISTINCT(name)")) 285 | ``` 286 | 287 | > `db.Raw` can also be used as a condition argument, similarly to db.Cond. 288 | 289 | ## Take the tour 290 | 291 | Get the real `upper/db` experience, take the [tour](/tour). 292 | 293 | [1]: https://github.com/lib/pq 294 | -------------------------------------------------------------------------------- /site/docs/v4/adapter/ql/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: QL 3 | --- 4 | 5 | The `ql` adapter for [QL][1] wraps the `github.com/cznic/ql/ql` driver 6 | written by [Jan Mercl][1]. 7 | 8 | > Before starting to read this detailed information, it is advisable that you 9 | > take a look at the [getting started](/v4/getting-started) page so you become 10 | > acquainted with the basics of `upper/db`, and you can grasp concepts better. 11 | 12 | ## Installation 13 | 14 | Use `go get` to download and install the adapter: 15 | 16 | ```go 17 | go get github.com/upper/db/adapter/ql 18 | ``` 19 | 20 | ## Setup 21 | 22 | Import the `ql` package into your application: 23 | 24 | ```go 25 | // main.go 26 | package main 27 | 28 | import ( 29 | "github.com/upper/db/adapter/ql" 30 | ) 31 | ``` 32 | 33 | Define the `ql.ConnectionURL{}` struct: 34 | 35 | ```go 36 | // ConnectionURL defines the DSN attributes. 37 | type ConnectionURL struct { 38 | Database string 39 | Options map[string]string 40 | } 41 | ``` 42 | 43 | Pass the `ql.ConnectionURL` value as argument to `ql.Open()` so the session is 44 | created. 45 | 46 | ```go 47 | settings = ql.ConnectionURL{ 48 | ... 49 | } 50 | 51 | sess, err = ql.Open(settings) 52 | ... 53 | ``` 54 | 55 | > Use the `ql.ParseURL()` function to convert a DSN into a `ql.ConnectionURL`: 56 | 57 | ```go 58 | // ParseURL parses a DSN into a ConnectionURL struct. 59 | ql.ParseURL(dsn string) (ConnectionURL, error) 60 | ``` 61 | 62 | Once the connection is established, you can start performing operations on the 63 | database. 64 | 65 | ### Example 66 | 67 | In the following example, a table named ‘birthday’ consisting of two columns 68 | (‘name’ and ‘born’) will be created. Before starting, the table will be 69 | searched in the database and, in the event it already exists, it will be 70 | removed. Then, three rows will be inserted into the table and checked for 71 | accuracy. To this end, the database will be queried and the matches 72 | (insertions) will be printed to standard output. 73 | 74 | The `birthday` table with the `name` and `born` columns is created with these 75 | SQL statements: 76 | 77 | ```sql 78 | --' example.sql 79 | DROP TABLE IF EXISTS birthday; 80 | 81 | CREATE TABLE birthday ( 82 | name string, 83 | born time 84 | ); 85 | ``` 86 | 87 | The `ql` command line tool is used to create an `example.db` database file: 88 | 89 | ``` 90 | rm -f example.db 91 | cat example.sql | ql -db example.db 92 | ``` 93 | 94 | The rows are inserted into the `birthday` table. The database is queried for 95 | the insertions and is set to print them to standard output. 96 | 97 | ```go 98 | package main 99 | 100 | import ( 101 | "fmt" 102 | "log" 103 | "time" 104 | 105 | "github.com/upper/db/adapter/ql" 106 | ) 107 | 108 | var settings = ql.ConnectionURL{ 109 | Database: `example.db`, // Path to database file 110 | } 111 | 112 | type Birthday struct { 113 | // The 'name' column of the 'birthday' table 114 | // is mapped to the 'name' property. 115 | Name string `db:"name"` 116 | // The 'born' column of the 'birthday' table 117 | // is mapped to the 'born' property. 118 | Born time.Time `db:"born"` 119 | } 120 | 121 | func main() { 122 | 123 | // Attempt to open the 'example.db' database file 124 | sess, err := ql.Open(settings) 125 | if err != nil { 126 | log.Fatalf("db.Open(): %q\n", err) 127 | } 128 | defer sess.Close() // Closing the session is a good practice. 129 | 130 | // The 'birthday' table is referenced. 131 | birthdayCollection := sess.Collection("birthday") 132 | 133 | // Any rows that might have been added between the creation of 134 | // the table and the execution of this function are removed. 135 | err = birthdayCollection.Truncate() 136 | if err != nil { 137 | log.Fatalf("Truncate(): %q\n", err) 138 | } 139 | 140 | // Three rows are inserted into the 'Birthday' table. 141 | birthdayCollection.Insert(Birthday{ 142 | Name: "Hayao Miyazaki", 143 | Born: time.Date(1941, time.January, 5, 0, 0, 0, 0, time.Local), 144 | }) 145 | 146 | birthdayCollection.Insert(Birthday{ 147 | Name: "Nobuo Uematsu", 148 | Born: time.Date(1959, time.March, 21, 0, 0, 0, 0, time.Local), 149 | }) 150 | 151 | birthdayCollection.Insert(Birthday{ 152 | Name: "Hironobu Sakaguchi", 153 | Born: time.Date(1962, time.November, 25, 0, 0, 0, 0, time.Local), 154 | }) 155 | 156 | // The database is queried for the rows inserted. 157 | res := birthdayCollection.Find() 158 | 159 | // The 'birthdays' variable is filled with the results found. 160 | var birthdays []Birthday 161 | 162 | err = res.All(&birthdays) 163 | if err != nil { 164 | log.Fatalf("res.All(): %q\n", err) 165 | } 166 | 167 | // The 'birthdays' variable is printed to stdout. 168 | for _, birthday := range birthday { 169 | fmt.Printf("%s was born in %s.\n", 170 | birthday.Name, 171 | birthday.Born.Format("January 2, 2006"), 172 | ) 173 | } 174 | } 175 | 176 | ``` 177 | 178 | Compile the example and run it: 179 | 180 | ``` 181 | go run example.go 182 | ``` 183 | 184 | The output will be: 185 | 186 | ``` 187 | Hayao Miyazaki was born in January 5, 1941. 188 | Nobuo Uematsu was born in March 21, 1959. 189 | Hironobu Sakaguchi was born in November 25, 1962. 190 | ``` 191 | 192 | ## Adapter particularities 193 | 194 | ### SQL Builder 195 | 196 | You can use the SQL builder for any complex SQL query: 197 | 198 | ```go 199 | q := b.SQL().Select( 200 | "p.id", 201 | "p.title AD publication_title", 202 | "a.name AS artist_name", 203 | ).From("artists AS a", "publication AS p"). 204 | Where("a.id = p.author_id") 205 | 206 | var publications []Publication 207 | if err = q.All(&publications); err != nil { 208 | log.Fatal(err) 209 | } 210 | ``` 211 | 212 | ### Helper functions 213 | 214 | Use `db.Func` to escape function names and arguments: 215 | 216 | ```go 217 | res = sess.Find().Select(db.Func("DISTINCT", "name")) 218 | ``` 219 | 220 | Use the `db.Raw()` function for strings that have to be interpreted literally: 221 | 222 | ```go 223 | res = sess.Find().Select(db.Raw("DISTINCT(name)")) 224 | ``` 225 | 226 | > `db.Raw` can also be used as a condition argument, similarly to `db.Cond`. 227 | 228 | ## Take the tour 229 | 230 | Get the real `upper/db` experience, take the [tour](/tour). 231 | 232 | [1]: https://github.com/cznic/ql 233 | [2]: http://golang.org/doc/effective_go.html#blank 234 | -------------------------------------------------------------------------------- /site/docs/v4/adapter/sqlite/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: SQLite 3 | --- 4 | 5 | The `sqlite` adapter for [SQLite3][3] wraps the `github.com/mattn/go-sqlite3` 6 | driver written by [Yasuhiro Matsumoto][1]. 7 | 8 | > Here you’ll learn about the particularities of the [SQLite][2] adapter. 9 | > Before starting to read this detailed information, it is advisable that you 10 | > take a look at the [getting started](/v4/getting-started) page so you become 11 | > acquainted with the basics of `upper/db` and you can grasp concepts better. 12 | 13 | ## Installation 14 | 15 | This package uses [cgo][4]. To use it, you'll need a C compiler, such as `gcc`: 16 | 17 | ``` 18 | # Debian 19 | sudo apt-get install gcc 20 | 21 | # FreeBSD 22 | sudo pkg install gcc 23 | sudo ln -s /usr/local/bin/gcc47 /usr/local/bin/gcc 24 | ``` 25 | 26 | > If you're using Mac, you'll need [Xcode](https://developer.apple.com/xcode/) 27 | > and Command Line Tools. 28 | 29 | Once this requirement is met, you can use `go get` to download, compile and 30 | install the adapter: 31 | 32 | ``` 33 | go get github.com/upper/db/v4/adapter/sqlite 34 | ``` 35 | 36 | Otherwise, you'll see the following error: 37 | 38 | ``` 39 | # github.com/mattn/go-sqlite3 40 | exec: "gcc": executable file not found in $PATH 41 | ``` 42 | 43 | ## Setup 44 | 45 | ### Database Session 46 | 47 | Import the `sqlite` package into your application: 48 | 49 | ```go 50 | package main 51 | 52 | import ( 53 | "github.com/upper/db/v4/adapter/sqlite" 54 | ) 55 | ``` 56 | 57 | Define the `sqlite.ConnectionURL{}` struct: 58 | 59 | ```go 60 | // ConnectionURL defines the DSN attributes. 61 | type ConnectionURL struct { 62 | Database string 63 | Options map[string]string 64 | } 65 | ``` 66 | 67 | Pass the `sqlite.ConnectionURL` value as argument to `sqlite.Open()` so the 68 | session is created. 69 | 70 | ```go 71 | settings = sqlite.ConnectionURL{ 72 | ... 73 | } 74 | 75 | sess, err = sqlite.Open(settings) 76 | ... 77 | ``` 78 | 79 | > The `sqlite.ParseURL()` function is also provided in case you need to convert 80 | > the DSN into a `sqlite.ConnectionURL`: 81 | 82 | ```go 83 | // ParseURL parses a DSN into a ConnectionURL struct. 84 | sqlite.ParseURL(dsn string) (ConnectionURL, error) 85 | ``` 86 | 87 | ## Common Database Operations 88 | 89 | Once the connection is established, you can start performing operations on the 90 | database. 91 | 92 | ### Example 93 | 94 | In the following example, a table named ‘birthday’ consisting of two columns 95 | (‘name’ and ‘born’) will be created. Before starting, the table will be 96 | searched in the database and, in the event it already exists, it will be 97 | removed. Then, three rows will be inserted into the table and checked for 98 | accuracy. To this end, the database will be queried and the matches 99 | (insertions) will be printed to standard output. 100 | 101 | The `birthday` table with the `name` and `born` columns is created with these 102 | SQL statements: 103 | 104 | ```sql 105 | --' example.sql 106 | DROP TABLE IF EXISTS "birthday"; 107 | 108 | CREATE TABLE "birthday" ( 109 | "name" varchar(50) DEFAULT NULL, 110 | "born" DATETIME DEFAULT CURRENT_TIMESTAMP 111 | ); 112 | ``` 113 | 114 | The `sqlite3` command line tool is used to create an `example.db` database 115 | file: 116 | 117 | ``` 118 | rm -f example.db 119 | cat example.sql | sqlite3 example.db 120 | ``` 121 | 122 | The rows are inserted into the `birthday` table. The database is queried for 123 | the insertions and is set to print them to standard output. 124 | 125 | ```go 126 | package main 127 | 128 | import ( 129 | "fmt" 130 | "log" 131 | "time" 132 | 133 | "github.com/upper/db/v4/adapter/sqlite" 134 | ) 135 | 136 | var settings = sqlite.ConnectionURL{ 137 | Database: `example.db`, // Path to database file 138 | } 139 | 140 | type Birthday struct { 141 | // The 'name' column of the 'birthday' table 142 | // is mapped to the 'name' property. 143 | Name string `db:"name"` 144 | // The 'born' column of the 'birthday' table 145 | // is mapped to the 'born' property. 146 | Born time.Time `db:"born"` 147 | } 148 | 149 | func main() { 150 | 151 | // Attempt to open the 'example.db' database file 152 | sess, err := sqlite.Open(settings) 153 | if err != nil { 154 | log.Fatalf("db.Open(): %q\n", err) 155 | } 156 | defer sess.Close() // Closing the session is a good practice. 157 | 158 | // The 'birthday' table is referenced. 159 | birthdayCollection := sess.Collection("birthday") 160 | 161 | // Any rows that might have been added between the creation of 162 | // the table and the execution of this function are removed. 163 | err = birthdayCollection.Truncate() 164 | if err != nil { 165 | log.Fatalf("Truncate(): %q\n", err) 166 | } 167 | 168 | // Three rows are inserted into the 'birthday' table. 169 | birthdayCollection.Insert(Birthday{ 170 | Name: "Hayao Miyazaki", 171 | Born: time.Date(1941, time.January, 5, 0, 0, 0, 0, time.Local), 172 | }) 173 | 174 | birthdayCollection.Insert(Birthday{ 175 | Name: "Nobuo Uematsu", 176 | Born: time.Date(1959, time.March, 21, 0, 0, 0, 0, time.Local), 177 | }) 178 | 179 | birthdayCollection.Insert(Birthday{ 180 | Name: "Hironobu Sakaguchi", 181 | Born: time.Date(1962, time.November, 25, 0, 0, 0, 0, time.Local), 182 | }) 183 | 184 | // The database is queried for the rows inserted. 185 | res := birthdayCollection.Find() 186 | 187 | // The 'birthdays' variable is filled with the results found. 188 | var birthdays []Birthday 189 | 190 | err = res.All(&birthdays) 191 | if err != nil { 192 | log.Fatalf("res.All(): %q\n", err) 193 | } 194 | 195 | // The 'birthdays' variable is printed to stdout. 196 | for _, birthday := range birthday { 197 | fmt.Printf("%s was born in %s.\n", 198 | birthday.Name, 199 | birthday.Born.Format("January 2, 2006"), 200 | ) 201 | } 202 | } 203 | ``` 204 | 205 | The Go file is compiled and executed using `go run`: 206 | 207 | ``` 208 | go run example.go 209 | ``` 210 | 211 | The output consists of three rows including names and birthdates: 212 | 213 | ``` 214 | Hayao Miyazaki was born in January 5, 1941. 215 | Nobuo Uematsu was born in March 21, 1959. 216 | Hironobu Sakaguchi was born in November 25, 1962. 217 | ``` 218 | 219 | ## Specifications 220 | 221 | ### JSON Types 222 | 223 | You can save and retrieve data when using [JSON 224 | types](https://www.sqlite.org/json1.html). If you want to try this out, make 225 | sure the column type is `json` and the field type is `sqlite.JSON`: 226 | 227 | ``` 228 | import ( 229 | ... 230 | "github.com/upper/db/v4/adapter/sqlite" 231 | ... 232 | ) 233 | 234 | type Person struct { 235 | ... 236 | Properties sqlite.JSON `db:"properties"` 237 | Meta sqlite.JSON `db:"meta"` 238 | } 239 | ``` 240 | 241 | > JSON types area supported on SQLite 3.9.0+. 242 | 243 | ### SQL Builder 244 | 245 | You can use the SQL builder for any complex SQL query: 246 | 247 | ```go 248 | q := b.SQL().Select( 249 | "p.id", 250 | "p.title AD publication_title", 251 | "a.name AS artist_name", 252 | ).From("artists AS a", "publication AS p"). 253 | Where("a.id = p.author_id") 254 | 255 | var publications []Publication 256 | if err = q.All(&publications); err != nil { 257 | log.Fatal(err) 258 | } 259 | ``` 260 | 261 | ### Auto-incremental Keys 262 | 263 | If you want tables to generate a unique number automatically whenever a new 264 | record is inserted, you can use auto-incremental keys. In this case, the column 265 | must be defined as `INTEGER PRIMARY KEY`. 266 | 267 | ```sql 268 | CREATE TABLE foo( 269 | id INTEGER PRIMARY KEY, 270 | title VARCHAR(255) 271 | ); 272 | ``` 273 | 274 | Remember to use `omitempty` to specify that the ID field should be ignored if 275 | it has an empty value: 276 | 277 | ```go 278 | type Foo struct { 279 | Id int64 `db:"id,omitempty"` 280 | Title string `db:"title"` 281 | } 282 | ``` 283 | 284 | Otherwise, an error will be returned. 285 | 286 | ### Escape Sequences 287 | 288 | There might be characters that cannot be typed in the context you're working, 289 | or else would have an undesired interpretation. Through `db.Func` you can 290 | encode the syntactic entities that cannot be directly represented by the 291 | alphabet: 292 | 293 | ```go 294 | res = sess.Find().Select(db.Func("DISTINCT", "name")) 295 | ``` 296 | 297 | On the other hand, you can use the `db.Raw` function so a given value is taken 298 | literally: 299 | 300 | ```go 301 | res = sess.Find().Select(db.Raw("DISTINCT(name)")) 302 | ``` 303 | 304 | > `db.Raw` can also be used as a condition argument, similarly to `db.Cond`. 305 | 306 | ## Take the tour 307 | 308 | Get the real `upper/db` experience, take the [tour](/tour). 309 | 310 | [1]: https://github.com/mattn/go-sqlite3 311 | [2]: http://golang.org/doc/effective_go.html#blank 312 | [3]: http://www.sqlite.org/ 313 | [4]: https://golang.org/cmd/cgo/ 314 | -------------------------------------------------------------------------------- /site/docs/v4/getting-started/connect-to-a-database.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Connecting to a database 3 | --- 4 | 5 | Use `go get` to import the database adapter: 6 | 7 | ``` 8 | go get -v -u github.com/upper/db/v4/adapter/$ADAPTER_NAME 9 | ``` 10 | 11 | Import the adapter package into your application: 12 | 13 | ```go 14 | import ( 15 | "github.com/upper/db/v4/adapter/{{adapter_name}}" 16 | ) 17 | ``` 18 | 19 | The `{{adapter_name}}` could be any of the following supported adapters: 20 | `postgresql`, `mysql`, `cockroachdb`, `mssql`, `sqlite`, `ql` or `mongo`. 21 | 22 | In this example we'll use the PostgreSQL adapter: 23 | 24 | ```go 25 | import ( 26 | "github.com/upper/db/v4/adapter/postgresql" 27 | ) 28 | ``` 29 | 30 | All adapters come with a `ConnectionURL` struct that you can use to describe 31 | parameters to open a database: 32 | 33 | ```go 34 | import ( 35 | "github.com/upper/db/v4/adapter/postgresql" 36 | ) 37 | 38 | // Use the `ConnectionURL` struct to create a DSN: 39 | var settings = postgresql.ConnectionURL{ 40 | User: "maria", 41 | Password: "p4ss", 42 | Address: "10.0.0.99", 43 | Database: "myprojectdb", 44 | } 45 | 46 | fmt.Printf("DSN: %s", settings) 47 | ``` 48 | 49 | Every adapter comes with an `Open()` function that takes a `ConnectionURL` and 50 | attempts to create a database session: 51 | 52 | ```go 53 | // sess is a db.Session type 54 | sess, err := postgresql.Open(settings) 55 | ... 56 | ``` 57 | 58 | Instead of `postgresql.ConnectionURL` you can use `mysql.ConnectionURL`, 59 | `mssql.ConnectionURL`, etc. All of these structs satisfy `db.ConnectionURL`. 60 | 61 | It is also possible to use a DSN string like 62 | `[adapter]://[user]:[password]@[host]/[database]`; you can easily convert it 63 | into a `ConnectionURL` struct and use it to connect to a database by using the 64 | `ParseURL` function that comes with your adapter: 65 | 66 | ```go 67 | import ( 68 | ... 69 | "github.com/upper/db/v4/adapter/postgresql" 70 | ... 71 | ) 72 | 73 | const connectDSN = `postgres://demouser:demop4ss@demo.upper.io/booktown` 74 | 75 | // Convert the DSN into a ConnectionURL 76 | settings, err := postgresql.ParseURL(connectDSN) 77 | ... 78 | 79 | // And use it to connect to your database. 80 | sess, err := postgresql.Open(settings) 81 | ... 82 | 83 | log.Println("Now you're connected to the database!") 84 | ``` 85 | 86 | Once you finish working with the database session, use `Close()` to free all 87 | associated resources and caches. Keep in mind that Go apps are long-lived 88 | processes, so you may only need to manually close a session if you don't need 89 | it at all anymore. 90 | 91 | ``` 92 | // Closing session 93 | err = sess.Close() 94 | ... 95 | ``` 96 | 97 | The following example demonstrates how to connect, ping and disconnect from a 98 | PostgreSQL database. 99 | 100 | ```go 101 | package main 102 | 103 | import ( 104 | "fmt" 105 | "log" 106 | 107 | "github.com/upper/db/v4/adapter/postgresql" 108 | ) 109 | 110 | var settings = postgresql.ConnectionURL{Database: "booktown", Host: "demo.upper.io", User: "demouser", Password: "demop4ss"} 111 | 112 | func main() { 113 | sess, err := postgresql.Open(settings) 114 | if err != nil { 115 | log.Fatal("Open: ", err) 116 | } 117 | defer sess.Close() 118 | if err := sess.Ping(); err != nil { 119 | log.Fatal("Ping: ", err) 120 | } 121 | 122 | fmt.Printf("Successfully connected to database: %q", sess.Name()) 123 | } 124 | ``` 125 | 126 | Please note that different databases may have particular ways of connecting to a database server or opening a database file; some databases, like SQLite, use plain files for storage instead of a central server. Please refer to the page of the adapter you're using to see such particularities. 127 | 128 | Underlying Driver 129 | 130 | If you require methods only available from the underlying driver, you can use the `Driver()` method, which returns an `interface{}`. For instance, if you need the mgo.Session.Ping method, you can retrieve the underlying `*mgo.Session` as an `interface{}`, cast it into the appropriate type, and use `Ping()`, as shown below: 131 | 132 | ```go 133 | drv = sess.Driver().(*mgo.Session) // The driver is cast into the 134 | // the appropriate type. 135 | err = drv.Ping() 136 | ``` 137 | 138 | You can do the same when working with an SQL adapter by changing the casting: 139 | 140 | ```go 141 | drv = sess.Driver().(*sql.DB) 142 | rows, err = drv.Query("SELECT name FROM users WHERE age = ?", age) 143 | ``` 144 | -------------------------------------------------------------------------------- /site/docs/v4/getting-started/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction to `upper/db` 3 | sidebar_position: 1 4 | --- 5 | 6 | `upper/db` provides a simple API for developers to use when working with 7 | different SQL and NoSQL database engines. Its main goal is to provide Go 8 | developers with the right tools to focus on writing business logic with a 9 | reasonable compromise between productivity, development speed, and computing 10 | resources. 11 | 12 | Using well-known database drivers, `upper/db` communicates with the most 13 | popular database engines ([PostgreSQL](../adapter/postgresql), 14 | [MySQL](../adapter/mysql), [CockroachDB](../adapter/cockroachdb), [Microsoft 15 | SQL Server](../adapter/mssql), [SQLite](../adapter/sqlite), 16 | [QL](../adapter/ql), and [MongoDB](../adapter/mongo)). 17 | 18 | ## Core components 19 | 20 | ### The `db` package 21 | 22 | The `db` package provides an **agnostic Go API** focused on working with 23 | collections of items. This API is modeled after basic set theory concepts that 24 | apply to relational and document-based database engines. 25 | 26 | This API provides you with enough tools for most of the tasks you perform with 27 | a database, such as: 28 | 29 | * Basic CRUD (Create, Read, Update, and Delete). 30 | * Search and delimitation of result sets. 31 | * Mapping between Go structs (or slices of structs) and query results. 32 | * Limit-offset pagination (page numbers). 33 | * Cursor-based pagination (previous and next). 34 | * Transactions. 35 | 36 | ```go 37 | package main 38 | 39 | import ( 40 | "fmt" 41 | "log" 42 | 43 | "github.com/upper/db/v4/adapter/postgresql" 44 | ) 45 | 46 | var settings = postgresql.ConnectionURL{ 47 | Database: "booktown", 48 | Host: "demo.upper.io", 49 | User: "demouser", 50 | Password: "demop4ss", 51 | } 52 | 53 | func main() { 54 | sess, err := postgresql.Open(settings) 55 | if err != nil { 56 | log.Fatal("postgresql.Open: ", err) 57 | } 58 | defer sess.Close() 59 | 60 | // The `db` API is portable, you can expect code to work the same on 61 | // different databases 62 | booksCounter, err := sess.Collection("books").Find().Count() 63 | if err != nil { 64 | log.Fatal("Find: ", err) 65 | } 66 | 67 | fmt.Printf("There are %d books in the database.\n", booksCounter) 68 | } 69 | ``` 70 | 71 | ### The SQL builder 72 | 73 | Sometimes, an agnostic API won't be enough; for those tasks, `upper/db` also 74 | provides a **SQL builder interface**, which provides more direct access to the 75 | database with the additional advantage of using a SQL-like Go API or raw SQL 76 | sentences. 77 | 78 | ```go 79 | package main 80 | 81 | import ( 82 | "fmt" 83 | "log" 84 | 85 | "github.com/upper/db/v4/adapter/postgresql" 86 | ) 87 | 88 | var settings = postgresql.ConnectionURL{ 89 | Database: "booktown", 90 | Host: "demo.upper.io", 91 | User: "demouser", 92 | Password: "demop4ss", 93 | } 94 | 95 | func main() { 96 | sess, err := postgresql.Open(settings) 97 | if err != nil { 98 | log.Fatal("postgresql.Open: ", err) 99 | } 100 | defer sess.Close() 101 | 102 | // Define a query 103 | row, err := sess.SQL().QueryRow("SELECT COUNT(1) FROM books") 104 | if err != nil { 105 | log.Fatal("Find: ", err) 106 | } 107 | 108 | // Do what you'd normally do with `database/sql` 109 | var counter int 110 | if err = row.Scan(&counter); err != nil { 111 | log.Fatal("Scan: ", err) 112 | } 113 | 114 | fmt.Printf("We have %d books in our database.\n", counter) 115 | } 116 | ``` 117 | 118 | ### The (optional) ORM-like interface 119 | 120 | `upper/db` provides an (optional) ORM-like layer that allows developers to 121 | represent data structures and relationships between them in a more opinionated 122 | way. This layer simplifies the process of working with complex data models and 123 | relationships, providing a higher level of abstraction and reducing the amount 124 | of boilerplate code required. 125 | 126 | ```go 127 | package main 128 | 129 | import ( 130 | "fmt" 131 | "log" 132 | 133 | "github.com/upper/db/v4" 134 | "github.com/upper/db/v4/adapter/postgresql" 135 | ) 136 | 137 | var settings = postgresql.ConnectionURL{ 138 | Database: "booktown", 139 | Host: "demo.upper.io", 140 | User: "demouser", 141 | Password: "demop4ss", 142 | } 143 | 144 | // Book represents a record from the "books" table. 145 | type Book struct { 146 | ID uint `db:"id,omitempty"` 147 | Title string `db:"title"` 148 | AuthorID uint `db:"author_id,omitempty"` 149 | SubjectID uint `db:"subject_id,omitempty"` 150 | } 151 | 152 | func (*Book) Store(sess db.Session) db.Store { 153 | return sess.Collection("books") 154 | } 155 | 156 | func main() { 157 | sess, err := postgresql.Open(settings) 158 | if err != nil { 159 | log.Fatal("postgresql.Open: ", err) 160 | } 161 | defer sess.Close() 162 | 163 | var book Book 164 | err = sess.Get(&book, db.Cond{"id": 7808}) 165 | if err != nil { 166 | log.Fatal("Find: ", err) 167 | } 168 | 169 | fmt.Printf("Book: %#v", book) 170 | } 171 | ``` 172 | 173 | ## Getting started 174 | 175 | * [Key concepts](/v4/getting-started/key-concepts) 176 | * [Connect to a database](/v4/getting-started/connect-to-a-database) 177 | * [Mapping database records to Go structs](/v4/getting-started/struct-mapping) 178 | * [Using the agnostic db API](/v4/getting-started/agnostic-db-api) 179 | * [Using the SQL builder API](/v4/getting-started/sql-builder-api) 180 | * [Transactions](/v4/getting-started/transactions) 181 | * [Logger](/v4/getting-started/logger) 182 | 183 | ## Tutorials 184 | 185 | * [ORM-like behavior with `db.Record`, `db.Store`, and 186 | hooks](/v4/tutorial/record-store-and-hooks) 187 | 188 | ## Supported adapters 189 | 190 | * [PostgreSQL](/v4/adapter/postgresql) 191 | * [MySQL](/v4/adapter/mysql) 192 | * [CockroachDB](/v4/adapter/cockroachdb) 193 | * [Microsoft SQL Server](/v4/adapter/mssql) 194 | * [SQLite](/v4/adapter/sqlite) 195 | * [QL](/v4/adapter/ql) 196 | * [MongoDB](/v4/adapter/mongo) 197 | -------------------------------------------------------------------------------- /site/docs/v4/getting-started/key-concepts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Key concepts 3 | --- 4 | 5 | Through this documentation, you'll find a few particular concepts, the most 6 | important are the ones defined below: 7 | 8 | **Session**: a representation of an established connection with a database. 9 | 10 | **Collection**: a set of items that belong to a concrete SQL _table_ or a 11 | NoSQL _collection_. 12 | 13 | > The term 'collection' is used indistinctively by methods that work on both 14 | > SQL and NoSQL databases. 15 | 16 | **Result set**: a subset of items in a collection that match specific 17 | conditions. Use `Find()` to define a result set. The whole result set can be 18 | delimited or modified through different methods, like `Update()`, `Delete()`, 19 | `Insert()`, `All()`, or `One()`. 20 | 21 | The figure below illustrates the session, collection, and result-set concepts: 22 | 23 |
24 | 25 | ![session collections and results](/img/session-collection-result.png) 26 | 27 |
28 | 29 | ## General considerations 30 | 31 | In order to use `upper/db` efficiently, it is advisable that you: 32 | 33 | 1. Understand the database you're working with (relational or 34 | document-oriented) 35 | 1. Use Go structs to describe data models. One struct per collection is a good 36 | practice. 37 | 1. Try to use `db.Collection` methods applicable to both SQL and NoSQL first. 38 | 1. Use the SQL builder or raw SQL when needed. 39 | -------------------------------------------------------------------------------- /site/docs/v4/getting-started/logger.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Logging 3 | --- 4 | 5 | `upper/db` can be set to print SQL statements and errors to standard output 6 | through the `UPPER_DB_LOG` environment variable: `UPPER_DB_LOG=DEBUG 7 | ./go-program` 8 | 9 | Use `UPPER_DB_LOG=$LOG_LEVEL ./program` to enable the built-in query logger at 10 | the desired logging level, you'll see the generated SQL queries and the result 11 | from their execution printed to stdout. 12 | 13 | ``` 14 | 2020/08/26 00:05:44 upper/db: log_level=DEBUG file=/go/src/github.com/upper/db/v4/internal/sqladapter/session.go:648 15 | Session ID: 00001 16 | Query: SELECT "pg_attribute"."attname" AS "pkey" FROM "pg_index", "pg_class", "pg_attribute" WHERE (pg_class.oid = '"books"'::regclass AND indrelid = pg_class.oid AND pg_attribute.attrelid = pg_class.oid AND pg_attribute.attnum = ANY(pg_index.indkey) AND indisprimary) ORDER BY "pkey" ASC 17 | Time taken: 0.02538s 18 | Context: context.Background 19 | 20 | 2020/08/26 00:05:44 upper/db: log_level=DEBUG file=/go/src/github.com/upper/db/v4/internal/sqladapter/session.go:648 21 | Session ID: 00001 22 | Query: SELECT * FROM "books" WHERE ("id" = $1) 23 | Arguments: []interface {}{1} 24 | Time taken: 0.00317s 25 | Context: context.Background 26 | ... 27 | ``` 28 | 29 | These are the logging levels `upper/db` comes with, ranging from the lowest 30 | severity (trace) to the highest (panic). 31 | 32 | * `db.LogLevelTrace` (`UPPER_DB_LOG=TRACE`) 33 | * `db.LogLevelDebug` (`UPPER_DB_LOG=DEBUG`) 34 | * `db.LogLevelInfo` (`UPPER_DB_LOG=INFO`) 35 | * `db.LogLevelWarn` (`UPPER_DB_LOG=WARN`) 36 | * `db.LogLevelError` (`UPPER_DB_LOG=ERROR`) 37 | * `db.LogLevelFatal` (`UPPER_DB_LOG=FATAL`) 38 | * `db.LogLevelPanic` (`UPPER_DB_LOG=PANIC`) 39 | 40 | By default, `upper/db` is set to `db.LogLevelWarn`. 41 | 42 | Use `db.LC().SetLevel()` to set a different logging level: 43 | 44 | ```go 45 | db.LC().SetLevel(db.LogLevelDebug) // or UPPER_DB_LOG=DEBUG 46 | ``` 47 | 48 | Use `sess.SetLogger` to overwrite the built-in logger: 49 | `sess.LC().SetLogger(&customLogger{})`, for instance: 50 | 51 | ```go 52 | import "github.com/sirupsen/logrus" 53 | // ... 54 | 55 | db.LC().SetLogger(logrus.New()) 56 | ``` 57 | 58 | If you want to restore the built-in logger, set the logger to nil: 59 | `sess.LC().SetLogger(nil)` 60 | 61 | Make sure to set an appropriate logging level in production, using levels lower 62 | than `db.LogLevelWarn` could make things pretty slow and verbose. 63 | 64 | ```go 65 | db.LC().SetLevel(db.LogLevelError) 66 | ``` 67 | -------------------------------------------------------------------------------- /site/docs/v4/getting-started/sql-builder-api.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: SQL builder API 3 | --- 4 | 5 | The SQL builder API provides tools to represent SQL expressions with Go code, 6 | which gives you some additional advantages over regular string queries: 7 | 8 | * You can benefit from the Go compiler syntax check. 9 | * It is easier to compose and reuse queries. 10 | 11 | Using the agnostic data API or the SQL API depends on the specific needs of 12 | your application. 13 | 14 | The SQL builder methods are available on all SQL adapters: 15 | 16 | ```go 17 | sess, err := postgresql.Open(settings) 18 | ... 19 | 20 | sqlbuilder := sess.SQL() 21 | ``` 22 | 23 | ## Select statements 24 | 25 | Use the `Select()` method on a session to begin a `SELECT` statement 26 | (a db.Selector). 27 | 28 | ```go 29 | q = sess.SQL(). 30 | Select("id", "name") 31 | ``` 32 | 33 | If you compiled the select statement, it would look like `SELECT "id", 34 | "name";` which is an incomplete SQL query; you still need to specify which 35 | table to select from: chain the `From()` method to do that: 36 | 37 | ```go 38 | q = sess.SQL(). 39 | Select("id", "name").From("accounts") 40 | ``` 41 | 42 | Now you have a complete query that you can compile into a full SQL statement: 43 | 44 | ```go 45 | var accounts []Account 46 | q = sess.SQL(). 47 | Select("id", "name").From("accounts") 48 | 49 | fmt.Println(q) // SELECT id, name FROM accounts 50 | ``` 51 | 52 | That query is wired to the database session, but it's not compiled nor executed 53 | unless you require data from it. Use the `All()` method on a query to execute 54 | it and map all the resulting rows into a slice of structs or maps: 55 | 56 | ```go 57 | var accounts []Account 58 | ... 59 | 60 | // All() executes the query and maps the resulting rows into an slice of 61 | // structs or maps. 62 | err = q.All(&accounts) 63 | ... 64 | ``` 65 | 66 | If you're only interested in one result, use `One()` instead of `All()`, and 67 | provide a single pointer to struct or map: 68 | 69 | ```go 70 | var account Account 71 | ... 72 | 73 | err = q.One(&account) 74 | ... 75 | ``` 76 | 77 | To select all the columns instead of specific ones, you can use 78 | the `SelectFrom()` method: 79 | 80 | ```go 81 | // SELECT * FROM accounts 82 | q = sess.SQL(). 83 | SelectFrom("accounts") 84 | ... 85 | 86 | err = q.All(&accounts) 87 | ... 88 | 89 | // Which is basically equivalent to: 90 | q = sess.SQL(). 91 | Select().From("accounts") 92 | ``` 93 | 94 | Using `All()` could be expensive for large databas; it's probably more 95 | efficient to get results one by one using an iterator: 96 | 97 | ``` 98 | var account Account 99 | 100 | iter := q.Iterator() 101 | for iter.Next(&account) { 102 | log.Printf("account: %v", account) 103 | ... 104 | } 105 | 106 | // Remember to check for iterator errors 107 | if err = iter.Err(); err != nil { 108 | ... 109 | } 110 | ``` 111 | 112 | Iterators are automatically closed at the end of the `Next()`-based loop. If 113 | you need to exit the iterator before the loop is completed use `iter.Close()`: 114 | 115 | ```go 116 | for iter.Next() { 117 | if somethingHappened() { 118 | iter.Close() 119 | break 120 | } 121 | } 122 | ``` 123 | 124 | Depending on your specific needs, you have to decide whether you want to 125 | use `All()`, `One()` or an iterator . 126 | 127 | ### SELECT statements and joins 128 | 129 | The `Join()` method is part of a `Selector`, you can use it to represent SELECT 130 | statements that use JOINs. 131 | 132 | ```go 133 | q = sess.SQL(). 134 | Select("a.name").From("accounts AS a"). 135 | Join("profiles AS p"). 136 | On("p.account_id = a.id") 137 | ... 138 | 139 | q = sess.SQL(). 140 | Select("name").From("accounts"). 141 | Join("owners"). 142 | Using("employee_id") 143 | ... 144 | ``` 145 | 146 | In addition to `Join()` you can also 147 | use `FullJoin()`, `CrossJoin()`, `RightJoin()` and `LeftJoin()`. 148 | 149 | ### INSERT statement 150 | 151 | The `InsertInto()` method begins an INSERT statement 152 | 153 | ```go 154 | q = sess.SQL(). 155 | InsertInto("people"). 156 | Columns("name"). 157 | Values("John") 158 | 159 | res, err = q.Exec() 160 | ... 161 | ``` 162 | 163 | You don't have to use the `Columns()` method, if you pass a map or a struct, 164 | you can omit it completely: 165 | 166 | ```go 167 | account := Account{ 168 | ... 169 | } 170 | 171 | q = sess.SQL(). 172 | InsertInto("people").Values(account) 173 | 174 | res, err = q.Exec() // res is a sql.Result 175 | ... 176 | ``` 177 | 178 | ## UPDATE statement 179 | 180 | The `Update()` method takes a table name and begins an UPDATE statement 181 | (an db.Updater ): 182 | 183 | ```go 184 | q = sess.SQL(). 185 | Update("people"). 186 | Set("name", "John"). 187 | Where("id = ?", 5) 188 | 189 | res, err = q.Exec() 190 | ... 191 | ``` 192 | 193 | You can update many columns at once by providing column-value pairs to `Set()`: 194 | 195 | ```go 196 | q = sess.SQL(). 197 | Update("people"). 198 | Set( 199 | "name", "John", 200 | "last_name", "Smith", 201 | ).Where("id = ?", 5) 202 | 203 | res, err = q.Exec() 204 | ... 205 | ``` 206 | 207 | You don't always have to provide column-value pairs, `Set()` also accepts maps 208 | or structs: 209 | 210 | ```go 211 | q = sess.SQL(). 212 | Update("people"). 213 | Set(map[string]interface{}{ 214 | "name": "John", 215 | "last_name": "Smith", 216 | }).Where("id = ?", 5) 217 | 218 | res, err = q.Exec() 219 | ... 220 | ``` 221 | 222 | ### DELETE statement 223 | 224 | You can begin a `DELETE` statement with the `DeleteFrom()` method: 225 | 226 | ```go 227 | q = sess.SQL(). 228 | DeleteFrom("accounts").Where("id", 5) 229 | 230 | res, err = q.Exec() 231 | ... 232 | ``` 233 | 234 | ### WHERE clause 235 | 236 | Use `Where()` to define conditions on 237 | a `Selector`, Deleter` or `Updater` interfaces. 238 | 239 | For instance, let's suppose we have a `Selector`: 240 | 241 | ```go 242 | q = sess.SQL(). 243 | SelectFrom("accounts") 244 | ``` 245 | 246 | We can use the `Where()` method to add conditions to the above query. 247 | 248 | How about constraining the results only to rows that match `id = 5?`: 249 | 250 | ```go 251 | q = q.Where("id = ?", 5) 252 | ``` 253 | 254 | We use a `?` as a placeholder for the argument, which is required to sanitize 255 | arguments and prevent SQL injections. You can use as many arguments as you need 256 | as long as you provide a value for each one of them: 257 | 258 | ```go 259 | q = q.Where("id = ? OR id = ?", 5, 4) // Two place holders and two values. 260 | ``` 261 | 262 | The above condition is a list of ORs, and sometimes we can rewrite such 263 | conditions into this: 264 | 265 | ```go 266 | q = q.Where("id IN ?", []int{5,4}) // id IN (5, 4) 267 | ``` 268 | 269 | Placeholders are not always necessary; if you're looking for an equality and 270 | you're only going to provide one argument, you can drop the `?` at the end: 271 | 272 | ```go 273 | q = q.Where("id", 5) 274 | ... 275 | 276 | q = q.Where("id IN", []int{5,4}) 277 | ... 278 | ``` 279 | 280 | It is also possible to use other operators besides the equality, but you have 281 | to be explicit about them: 282 | 283 | ```go 284 | q = q.Where("id >", 5) 285 | ... 286 | 287 | q = q.Where("id > ? AND id < ?", 5, 10) 288 | ... 289 | ``` 290 | 291 | You can also use `Cond` to define conditions for `Where()` just like you would 292 | normally do when using `Find()`: 293 | 294 | ```go 295 | // ...WHERE "id" > 5 296 | q = q.Where(db.Cond{ 297 | "id >": 5, 298 | }) 299 | ... 300 | 301 | // ...WHERE "id" > 5 AND "id" < 10 302 | q = q.Where(db.Cond{"id >": 5, "id <": 10}) 303 | ... 304 | 305 | // ...WHERE ("id" = 5 OR "id" = 9 OR "id" = 12) 306 | q = q.Where(db.Or( 307 | db.Cond{"id": 5}, 308 | db.Cond{"id": 9}, 309 | db.Cond{"id": 12}, 310 | )) 311 | ``` 312 | 313 | ## Plain SQL statements 314 | 315 | If the builder does not provide you with enough flexibility to create complex 316 | SQL queries, you can always use plain SQL: 317 | 318 | ```go 319 | rows, err = sess.SQL(). 320 | Query(`SELECT * FROM accounts WHERE id = ?`, 5) 321 | ... 322 | 323 | row, err = sess.SQL(). 324 | QueryRow(`SELECT * FROM accounts WHERE id = ? LIMIT ?`, 5, 1) 325 | ... 326 | 327 | res, err = sess.SQL(). 328 | Exec(`DELETE FROM accounts WHERE id = ?`, 5) 329 | ... 330 | ``` 331 | 332 | The `Query()` method returns a `*sql.Rows` object and of course, you can do 333 | whatever you would normally do with it: 334 | 335 | ```go 336 | err = rows.Scan(&id, &name) 337 | ... 338 | ``` 339 | 340 | If you don't want to use `Scan()` directly, you could create an iterator using 341 | any `*sql.Rows` value: 342 | 343 | 344 | ```go 345 | rows, err = sess.SQL(). 346 | Query(`SELECT * FROM accounts WHERE last_name = ?`, "Smith") 347 | ... 348 | 349 | var accounts []Account 350 | iter := sess.SQL().NewIterator(rows) 351 | err = iter.All(&accounts) 352 | ... 353 | ``` 354 | -------------------------------------------------------------------------------- /site/docs/v4/getting-started/struct-mapping.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Struct mapping 3 | --- 4 | 5 | Use Go structs and field tags to define a mapping between your Go application 6 | and database tables or collections. 7 | 8 | ```go 9 | type User struct { 10 | Name `db:"name"` 11 | } 12 | ``` 13 | 14 | add the `omitempty` option to struct fields that you don't want to send to the 15 | database if they don't have a value, like IDs that are set to auto-increment 16 | (or auto-generate) themselves: 17 | 18 | ```go 19 | // Person represents an item from the "people" collection. 20 | type Person struct { 21 | ID uint64 `db:"id,omitempty"` // Use `omitempty` for fields 22 | // that are not to be sent by 23 | // the adapter when empty. 24 | Name string `db:"name"` 25 | LastName string `db:"last_name"` 26 | } 27 | 28 | // Employee represents an item from the "employees" collection. 29 | type Employee struct { 30 | ID uint64 `db:"id,omitempty"` // Skip `id` column when zero. 31 | FirstName sql.NullString `db:"first_name"` 32 | LastName string `db:"last_name"` 33 | } 34 | ``` 35 | 36 | You can provide different values in struct tags, including those used to map 37 | JSON values to fields: 38 | 39 | ```go 40 | type Person struct { 41 | ID uint64 `db:"id,omitempty" json:"id"` 42 | Name string `db:"name" json:"name"` 43 | ... 44 | Password string `db:"password,omitempty" json:"-"` 45 | } 46 | ``` 47 | 48 | Fields that don't have a db struct tag will be omitted from queries: 49 | 50 | ```go 51 | type Person struct { 52 | ... 53 | Token string 54 | } 55 | ``` 56 | 57 | ## Embedding structs​ 58 | 59 | Using the `inline` option you can embed structs into other structs. See 60 | this struct, for instance: 61 | 62 | ```go 63 | type Person struct { 64 | FirstName string `db:"first_name"` 65 | LastName string `db:"last_name"` 66 | } 67 | ``` 68 | 69 | This is a common struct that can be shared with other structs which also need `FirstName` and `LastName`: 70 | 71 | ```go 72 | type Author struct { 73 | ID int `db:"id,omitempty"` 74 | 75 | Person `db:",inline"` // Embedded Person 76 | } 77 | 78 | type Employee struct { 79 | ID int `db:"id,omitempty"` 80 | 81 | Person `db:",inline"` // Embedded Person 82 | } 83 | ``` 84 | 85 | See the following example: embedding the `Person` struct into `Author` and `Employee` 86 | 87 | ```go 88 | package main 89 | 90 | import ( 91 | "fmt" 92 | "log" 93 | 94 | "github.com/upper/db/v4" 95 | "github.com/upper/db/v4/adapter/postgresql" 96 | ) 97 | 98 | // Person represents a person with a name. 99 | type Person struct { 100 | FirstName string `db:first_name` 101 | LastName string `db:last_name` 102 | } 103 | 104 | // Author represents a person that is an author. 105 | type Author struct { 106 | ID int `db:"id"` 107 | Person `db:",inline"` 108 | } 109 | 110 | // Employee represents a person that is an employee. 111 | type Employee struct { 112 | ID int `db:"id"` 113 | Person `db:",inline"` 114 | } 115 | 116 | func Authors(sess db.Session) db.Collection { 117 | return sess.Collection("authors") 118 | } 119 | 120 | func Employees(sess db.Session) db.Collection { 121 | return sess.Collection("employees") 122 | } 123 | 124 | var settings = postgresql.ConnectionURL{ 125 | Database: booktown, 126 | Host: demo.upper.io, 127 | User: demouser, 128 | Password: demop4ss, 129 | } 130 | 131 | func main() { 132 | sess, err := postgresql.Open(settings) 133 | if err != nil { 134 | log.Fatal("Open: ", err) 135 | } 136 | defer sess.Close() 137 | 138 | // Get and print the first 5 authors ordered by last name 139 | res := Authors(sess).Find().OrderBy("last_name").Limit(5) 140 | 141 | var authors []Author 142 | if err := res.All(&authors); err != nil { 143 | log.Fatal("All: ", err) 144 | } 145 | 146 | fmt.Println("Authors (5):") 147 | 148 | for _, author := range authors { 149 | fmt.Printf("Last Name: %s\tID: %d\n", author.LastName, author.ID) 150 | } 151 | 152 | fmt.Println("") 153 | 154 | // Get and print the first 5 employees ordered by last name 155 | res = Employees(sess).Find().OrderBy("last_name").Limit(5) 156 | 157 | var employees []Author 158 | if err := res.All(&employees); err != nil { 159 | log.Fatal("All: ", err) 160 | } 161 | 162 | fmt.Println("Employees (5):") 163 | 164 | for _, employee := range employees { 165 | fmt.Printf("Last Name: %s\tID: %d\n", employee.LastName, employee.ID) 166 | } 167 | } 168 | ``` 169 | 170 | ### Solving mapping ambiguities on JOINs​ 171 | 172 | The previous example will work as long as you use the `db:",inline"` tag. You 173 | can even embed more than one struct into an other, but you should be careful 174 | with column ambiguities: 175 | 176 | ```go 177 | // Book that has ID. 178 | type Book struct { 179 | ID int `db:"id"` // Has an ID column. 180 | Title string `db:"title"` 181 | AuthorID int `db:"author_id"` 182 | SubjectID int `db:"subject_id"` 183 | } 184 | 185 | // Author that has ID. 186 | type Author struct { 187 | ID int `db:"id"` // Also has an ID column. 188 | LastName string `db:"last_name"` 189 | FirstName string `db:"first_name"` 190 | } 191 | ``` 192 | 193 | Embedding these two structs into a third one will cause a conflict of IDs, to 194 | solve this conflict you can add an extra `book_id` column mapping and use 195 | a `book_id` alias when querying for the book ID. 196 | 197 | ``` 198 | // BookAuthor 199 | type BookAuthor struct { 200 | // Both Author and Book have and ID column, we need this extra field to tell 201 | // the difference between the ID of the book and the ID of the author. 202 | BookID int `db:"book_id"` 203 | 204 | Author `db:",inline"` 205 | Book `db:",inline"` 206 | } 207 | ``` 208 | 209 | ```go 210 | package main 211 | 212 | import ( 213 | "fmt" 214 | "log" 215 | 216 | "github.com/upper/db/v4" 217 | "github.com/upper/db/v4/adapter/postgresql" 218 | ) 219 | 220 | // Book represents a book. 221 | type Book struct { 222 | ID int `db:"id"` 223 | Title string `db:"title"` 224 | AuthorID int `db:"author_id"` 225 | SubjectID int `db:"subject_id"` 226 | } 227 | 228 | // Author represents the author of a book. 229 | type Author struct { 230 | ID int `db:"id"` 231 | LastName string `db:"last_name"` 232 | FirstName string `db:"first_name"` 233 | } 234 | 235 | // BookAuthor represents join data from books and authors. 236 | type BookAuthor struct { 237 | // Both Author and Book have and ID column, we need this to tell the ID of 238 | // the book from the ID of the author. 239 | BookID int `db:"book_id"` 240 | Author `db:",inline"` 241 | Book `db:",inline"` 242 | } 243 | 244 | var settings = postgresql.ConnectionURL{ 245 | Database: booktown, 246 | Host: demo.upper.io, 247 | User: demouser, 248 | Password: demop4ss, 249 | } 250 | 251 | func main() { 252 | sess, err := postgresql.Open(settings) 253 | if err != nil { 254 | log.Fatal(err) 255 | } 256 | defer sess.Close() 257 | 258 | req := sess.SQL(). 259 | Select( 260 | "b.id AS book_id", 261 | db.Raw("b."), 262 | db.Raw("a."), 263 | ).From("books b"). 264 | Join("authors a").On("b.author_id = a.id").OrderBy("b.title") 265 | 266 | var books []BookAuthor 267 | if err := req.All(&books); err != nil { 268 | log.Fatal(err) 269 | } 270 | 271 | for _, book := range books { 272 | fmt.Printf("ID: %d\tAuthor: %s\t\tBook: %q\n", book.BookID, book.Author.LastName, book.Book.Title) 273 | } 274 | } 275 | ``` 276 | -------------------------------------------------------------------------------- /site/docs/v4/getting-started/transactions.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Transactions 3 | --- 4 | 5 | Transactions are special operations that you can carry out with the guarantee 6 | that the whole batch will fail if one fails. A typical example of a transaction 7 | is a bank operation in which you want to move money from one account to another 8 | without worrying about a power failure or a writing error in the middle of a 9 | transaction that would create an inconsistency. 10 | 11 | You can create and use transaction blocks with the `Tx` method: 12 | 13 | ```go 14 | package main 15 | 16 | import ( 17 | "log" 18 | 19 | "github.com/upper/db/v4" 20 | ) 21 | 22 | func main() { 23 | ... 24 | err := sess.Tx(func(tx db.Session) error { 25 | // Use `tx` as you would normally use `sess.` 26 | ... 27 | id, err := tx.Collection("accounts").Insert(...) 28 | if err != nil { 29 | // Roll-back the transaction by returning an error value. 30 | return err 31 | } 32 | ... 33 | 34 | err := tx.Collection("accounts").Update(...) 35 | if err != nil { 36 | // Roll-back the transaction by returning an error value. 37 | return err 38 | } 39 | ... 40 | 41 | rows, err := tx.SQL().Query(...) 42 | ... 43 | 44 | ... 45 | // Commit the transaction by returning `nil`. 46 | return nil 47 | }) 48 | if err != nil { 49 | log.Fatal("Transaction failed: ", err) 50 | } 51 | } 52 | ``` 53 | 54 | See the tour example on how to use transactions. 55 | -------------------------------------------------------------------------------- /site/docs/v4/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: upper/db/v4 (current) 3 | sidebar_position: 1 4 | --- 5 | -------------------------------------------------------------------------------- /site/docs/v4/tutorial/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Tutorials 3 | sidebar_position: 100 4 | --- 5 | -------------------------------------------------------------------------------- /site/docs/v4/tutorial/record-store-and-hooks.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ORM-like behaviour with `db.Record`, `db.Store` and hooks 3 | --- 4 | 5 | `upper/db` provides two complementary interfaces that can help you model apps 6 | in a more ORM-ish way: `Record` and `Store` 7 | 8 | ## Using `Record` for single entries 9 | 10 | `Record` is an interface that can be satisfied by structs that represent single 11 | records from a collection: 12 | 13 | ```go 14 | type Record interface { 15 | Store(sess Session) Store 16 | } 17 | ``` 18 | 19 | If you have a `Book` struct that looks like this: 20 | 21 | ```go 22 | type Book struct { 23 | Title string `db:"title"` 24 | } 25 | ``` 26 | 27 | you can make it compatible with `Record` by adding a `Store()` method: 28 | 29 | ```go 30 | type Book struct { 31 | // ... 32 | } 33 | 34 | func (b *Book) Store(sess db.Session) Store { 35 | return sess.Collection("books") 36 | } 37 | 38 | var _ = db.Record(&Book{}) 39 | ``` 40 | 41 | Records can be used with `Session` methods: 42 | 43 | ```go 44 | // Retrieving a record 45 | sess.Get(&book, 123) 46 | 47 | // Creating or updating a record 48 | sess.Save(&book) 49 | 50 | // Delete a record 51 | sess.Delete(&book) 52 | ``` 53 | 54 | See the tour example on `Record`. 55 | 56 | ## Using `Store` for collections 57 | 58 | `Store` is an interface that can be satisfied by collections: 59 | 60 | ```go 61 | type Store interface { 62 | Collection 63 | } 64 | ``` 65 | 66 | Let's suppose we want to create a `Store` for book records, we'd name 67 | it `BookStore`: 68 | 69 | ```go 70 | type BooksStore struct { 71 | db.Collection 72 | } 73 | 74 | var _ = db.Store(&BooksStore{}) 75 | ``` 76 | 77 | You can extend `Store` structs with custom methods. The following method 78 | returns a book that matches a given title: 79 | 80 | ```go 81 | func (books *BooksStore) FindByTitle(title string) (*Book, error) { 82 | var book Book 83 | if err := books.Find(db.Cond{"title": title}).One(&book); err != nil { 84 | return nil, err 85 | } 86 | return &book, nil 87 | } 88 | ``` 89 | 90 | A recommended pattern for stores is creating a function to enclose the store's 91 | initialization: 92 | 93 | ```go 94 | func Books(sess db.Session) *BooksStore { 95 | return &BooksStore{sess.Collection("books")} 96 | } 97 | ``` 98 | 99 | Use the `Books()` method instead of `Collection("books")`: 100 | 101 | ```go 102 | err := Books(sess).Find(...).All(...) 103 | ``` 104 | 105 | See the tour example on `Store`. 106 | 107 | ## Using `Record` and `Store`​ 108 | 109 | The `Record` and `Store` interfaces do not depend on each other but can be 110 | mixed together. See the following example: 111 | 112 | ```go 113 | type BooksStore struct { 114 | db.Collection 115 | } 116 | 117 | func (books *BooksStore) FindByTitle(title string) (*Book, error) { 118 | // ... 119 | } 120 | 121 | // Books initializes a BooksStore 122 | func Books(sess db.Session) *BooksStore { 123 | return &BooksStore{sess.Collection("books")} 124 | } 125 | 126 | type Book struct { 127 | Title string `db:"title"` 128 | } 129 | 130 | func (b *Book) Store(sess db.Session) Store { 131 | // Note that we're using the Books function defined above instead 132 | // of sess.Collection. 133 | return Books(sess) 134 | } 135 | 136 | var _ = db.Store(&BooksStore{}) 137 | var _ = db.Record(&Book{}) 138 | ``` 139 | 140 | ## Using `Record` hooks 141 | 142 | Hooks are tasks to be performed before or after a specific action happens on a 143 | record. You can add hooks to models by defining unique methods 144 | like `BeforeCreate`, `AfterUpdate`, or `Validate` that satisfy specific 145 | signatures: 146 | 147 | ```go 148 | type User struct { 149 | // ... 150 | } 151 | 152 | func (u *User) Store(sess db.Session) db.Store { 153 | // ... 154 | } 155 | 156 | // BeforeCreate hook 157 | func (u *User) BeforeCreate(sess db.Session) error { 158 | // ... 159 | } 160 | 161 | // Validate hook 162 | func (u *User) Validate() error { 163 | // ... 164 | } 165 | 166 | // Interface checks 167 | var _ = interface{ 168 | db.Record 169 | db.BeforeCreateHook 170 | db.Validator 171 | }(&User{}) 172 | ``` 173 | 174 | Hooks are only executed when using methods that explicitly require `db.Record`, 175 | such a `sess.Get`, `sess.Save` or `sess.Delete`: 176 | 177 | ```go 178 | // Hooks will be executed 179 | sess.Save(&user) 180 | 181 | // Hooks won't be executed 182 | sess.Collection(...).Find().Update(&user) 183 | ``` 184 | ## `Validate()` hook 185 | 186 | The `Validate()` hook is called before creating or updating a record. 187 | 188 | If `Validate()` returns a non-nil error, the operation is aborted. 189 | 190 | The purpose of this method is for models to run preliminary checks on their own 191 | data before executing a query. 192 | 193 | If you're using hooks, make sure your model satisfies the 194 | `db.Validator` interface at compile time: 195 | 196 | ```go 197 | var _ = db.Validator(&User{}) 198 | ``` 199 | 200 | ## BeforeCreate​ 201 | 202 | The `BeforeCreate(db.Session) error` hook is called before inserting a record 203 | into a collection. If `BeforeCreate()` returns a non-nil error, the operation 204 | is canceled and rolled back. 205 | 206 | The purpose of this method is for models to run specific tasks before changing 207 | the state of a collection. 208 | 209 | ```go 210 | func (user *User) BeforeCreate(sess db.Session) error { 211 | // Check if the e-mail was already registered by another user. 212 | c, err := user.Store(sess). 213 | Find(db.Cond{"email": user.Email}). 214 | Count() 215 | if err != nil { 216 | return err 217 | } 218 | if c > 0 { 219 | return errors.New("e-mail already exists") 220 | } 221 | 222 | return nil 223 | } 224 | ``` 225 | 226 | Make sure your model satisfies the `db.BeforeCreateHook` interface at compile 227 | time: 228 | 229 | ```go 230 | var _ = db.BeforeCreateHook(&User{}) 231 | ``` 232 | 233 | ## AfterCreate 234 | 235 | The `AfterCreate(db.Session) error` hook is called after having inserted a 236 | record into a collection. If `AfterCreate()` returns a non-nil error, then the 237 | whole operation is canceled and rolled back. 238 | 239 | The purpose of this method is for models to run specific tasks after changing 240 | the state of a collection. 241 | 242 | ```go 243 | func (user *User) AfterCreate(sess db.Session) error { 244 | // Send log to somewhere else. 245 | events.Log("Item has been inserted.") 246 | return nil 247 | } 248 | ``` 249 | 250 | Make sure your model satisfies the `db.AfterCreateHook` interface at compile 251 | time: 252 | 253 | ```go 254 | var _ = db.AfterCreateHook(&User{}) 255 | ``` 256 | 257 | ## BeforeUpdate 258 | 259 | The `BeforeUpdate(db.Session) error` hook is called before updating a record 260 | from a collection. If `BeforeUpdate()` returns a non-nil error, then the whole 261 | operation is canceled and rolled back. 262 | 263 | The purpose of this method is for models to run specific tasks before changing 264 | the state of a collection. 265 | 266 | ```go 267 | func (user *User) BeforeUpdate(sess db.Session) error { 268 | // Check if the e-mail is already in use. 269 | c, err := user.Store(sess). 270 | Find(db.Cond{ 271 | "email": user.Email, 272 | "id": db.NotEq(user.ID), 273 | }). 274 | Count() 275 | if err != nil { 276 | return err 277 | } 278 | if c > 0 { 279 | return errors.New("e-mail is already in use") 280 | } 281 | 282 | return nil 283 | } 284 | ``` 285 | 286 | Make sure your model satisfies the `db.BeforeUpdateHook` interface at compile 287 | time: 288 | 289 | ```go 290 | var _ = db.BeforeUpdateHook(&User{}) 291 | ``` 292 | ## AfterUpdate 293 | 294 | The `AfterUpdate(db.Session) error` hook is called after having updated a 295 | record from a collection. If `AfterUpdate()` returns a non-nil error, then the 296 | whole operation is canceled and rolled back. 297 | 298 | The purpose of this method is for models to run specific tasks after changing 299 | the state of a collection. 300 | 301 | ```go 302 | func (user *User) AfterUpdate(sess db.Session) error { 303 | // Send log to somewhere 304 | events.Log("Item has been updated.") 305 | return nil 306 | } 307 | ``` 308 | 309 | Make sure your model satisfies the `AfterUpdate` interface at compile time: 310 | 311 | ```go 312 | var _ = db.AfterUpdate(&User{}) 313 | ``` 314 | 315 | ## BeforeDelete 316 | 317 | The `BeforeDelete(db.Session) error` hook is called before removing a record 318 | from a collection. If `BeforeDelete()` returns a non-nil error, then the whole 319 | operation is canceled and rolled back. 320 | 321 | The purpose of this method is for models to run specific tasks before changing 322 | the state of a collection. 323 | 324 | ```go 325 | func (post *Post) BeforeDelete(sess db.Session) error { 326 | // Check if the post is unpublished before deletion 327 | if post.Published { 328 | return errors.New("post must be unpublished before deletion") 329 | } 330 | return nil 331 | } 332 | ``` 333 | 334 | Make sure your model satisfies the `db.BeforeDeleteHook` interface at compile 335 | time: 336 | 337 | ```go 338 | var _ = db.BeforeDeleteHook(&Post{}) 339 | ``` 340 | 341 | ## AfterDelete 342 | 343 | The `AfterDelete(db.Session) error` hook is called after having deleted a 344 | record from a collection. If `AfterDelete()` returns a non-nil error, then the 345 | whole operation is canceled and rolled back. 346 | 347 | The purpose of this method is for models to run specific tasks after changing 348 | the state of a collection. 349 | 350 | ```go 351 | func (post *Post) AfterDelete(sess db.Session) error { 352 | // Update post counter 353 | Stats(sess).Update(...) 354 | return nil 355 | } 356 | ``` 357 | 358 | Make sure your model satisfies the `db.AfterDeleteHook` interface at compile 359 | time: 360 | 361 | ```go 362 | var _ = db.AfterDeleteHook(&Post{}) 363 | ``` 364 | -------------------------------------------------------------------------------- /site/docusaurus.config.ts: -------------------------------------------------------------------------------- 1 | import {themes as prismThemes} from 'prism-react-renderer'; 2 | import type {Config} from '@docusaurus/types'; 3 | import type * as Preset from '@docusaurus/preset-classic'; 4 | import codeBlock from './plugins/codeblock'; 5 | 6 | // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) 7 | 8 | const config: Config = { 9 | title: 'upper/db', 10 | tagline: 'A productive data access layer for Go', 11 | favicon: 'img/favicon.svg', 12 | 13 | markdown: { 14 | format: 'md', 15 | }, 16 | 17 | // Set the production url of your site here 18 | url: 'https://upper.io', // Your website URL 19 | // Set the // pathname under which your site is served 20 | // For GitHub pages deployment, it is often '//' 21 | baseUrl: '/', 22 | 23 | // GitHub pages deployment config. 24 | // If you aren't using GitHub pages, you don't need these. 25 | organizationName: 'upper.io', 26 | projectName: 'upper.io', 27 | 28 | onBrokenLinks: 'warn', 29 | onBrokenMarkdownLinks: 'warn', 30 | 31 | // Even if you don't use internationalization, you can use this field to set 32 | // useful metadata like html lang. For example, if your site is Chinese, you 33 | // may want to replace "en" with "zh-Hans". 34 | i18n: { 35 | defaultLocale: 'en', 36 | locales: ['en'], 37 | }, 38 | 39 | presets: [ 40 | [ 41 | 'classic', 42 | { 43 | docs: { 44 | sidebarPath: './sidebars.ts', 45 | routeBasePath: '/', 46 | remarkPlugins: [ 47 | codeBlock, 48 | ], 49 | }, 50 | theme: { 51 | customCss: './src/css/custom.css', 52 | }, 53 | } satisfies Preset.Options, 54 | ], 55 | ], 56 | 57 | themeConfig: { 58 | colorMode: { 59 | defaultMode: 'light', 60 | disableSwitch: true, 61 | }, 62 | 63 | // Replace with your project's social card 64 | //image: 'img/docusaurus-social-card.jpg', 65 | navbar: { 66 | hideOnScroll: true, 67 | title: 'upper/db', 68 | logo: { 69 | alt: 'upper/db', 70 | src: 'img/gopher.svg', 71 | }, 72 | items: [ 73 | { 74 | to: '/v4/getting-started', 75 | label: 'Getting Started', 76 | position: 'right', 77 | }, 78 | ], 79 | }, 80 | footer: { 81 | style: 'dark', 82 | links: [ 83 | { 84 | title: 'Learn', 85 | items: [ 86 | { 87 | label: 'Getting Started', 88 | to: '/v4/getting-started', 89 | }, 90 | { 91 | label: 'API Reference', 92 | to: 'https://pkg.go.dev/github.com/upper/db/v4', 93 | }, 94 | ], 95 | }, 96 | { 97 | title: 'Code', 98 | items: [ 99 | { 100 | label: 'File an issue', 101 | href: 'https://github.com/upper/db/issues', 102 | }, 103 | { 104 | label: 'Source code', 105 | href: 'https://github.com/upper/db', 106 | }, 107 | ], 108 | }, 109 | { 110 | title: 'About', 111 | items: [ 112 | { 113 | label: 'Authors', 114 | to: '/authors', 115 | }, 116 | { 117 | label: 'License', 118 | to: '/license', 119 | }, 120 | ], 121 | }, 122 | ], 123 | copyright: `Copyright © ${new Date().getFullYear()} upper/db`, 124 | }, 125 | prism: { 126 | defaultLanguage: 'go', 127 | theme: prismThemes.dracula, 128 | darkTheme: prismThemes.dracula, 129 | }, 130 | } satisfies Preset.ThemeConfig, 131 | }; 132 | 133 | export default config; 134 | -------------------------------------------------------------------------------- /site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "^3.6.2", 19 | "@docusaurus/preset-classic": "^3.6.2", 20 | "@mdx-js/react": "^3.0.0", 21 | "clsx": "^2.0.0", 22 | "prism-react-renderer": "^2.3.0", 23 | "react": "^18.0.0", 24 | "react-dom": "^18.0.0", 25 | "unist-util-map": "^4.0.0" 26 | }, 27 | "devDependencies": { 28 | "@docusaurus/module-type-aliases": "^3.6.2", 29 | "@docusaurus/tsconfig": "^3.6.2", 30 | "@docusaurus/types": "^3.6.2", 31 | "typescript": "~5.6.2" 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.5%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 3 chrome version", 41 | "last 3 firefox version", 42 | "last 5 safari version" 43 | ] 44 | }, 45 | "engines": { 46 | "node": ">=18.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /site/plugins/codeblock/index.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from 'unified'; 2 | import { Node } from 'unist'; 3 | import { visit } from 'unist-util-visit'; 4 | import { map } from 'unist-util-map'; 5 | 6 | // Remark plugin to convert text enclosed between `$$` into code blocks. 7 | const remarkCodeblockPlugin: Plugin = () => { 8 | return (tree: Node) => { 9 | return map(tree, (node) => { 10 | const { type, tagName, properties, children } = node 11 | if (type !== 'code') { 12 | return node 13 | } 14 | console.log({type, tagName}) 15 | console.log({node}) 16 | return node 17 | }) 18 | /* 19 | visit(tree, 'text', (node: Node) => { 20 | console.log('processing node', node) 21 | }) 22 | visit(tree, 'paragraph', (node: Parent) => { 23 | console.log('processing node', node) 24 | node.children = [] 25 | }) 26 | */ 27 | }; 28 | }; 29 | 30 | export default remarkCodeblockPlugin; 31 | -------------------------------------------------------------------------------- /site/sidebars.ts: -------------------------------------------------------------------------------- 1 | import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; 2 | 3 | // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) 4 | 5 | /** 6 | * Creating a sidebar enables you to: 7 | - create an ordered group of docs 8 | - render a sidebar for each doc of that group 9 | - provide next/previous navigation 10 | 11 | The sidebars can be generated from the filesystem, or explicitly defined here. 12 | 13 | Create as many sidebars as you want. 14 | */ 15 | const sidebars: SidebarsConfig = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], 18 | 19 | // But you can create a sidebar manually 20 | /* 21 | tutorialSidebar: [ 22 | 'intro', 23 | 'hello', 24 | { 25 | type: 'category', 26 | label: 'Tutorial', 27 | items: ['tutorial-basics/create-a-document'], 28 | }, 29 | ], 30 | */ 31 | }; 32 | 33 | export default sidebars; 34 | -------------------------------------------------------------------------------- /site/src/components/HomepageFeatures/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import Heading from '@theme/Heading'; 3 | import styles from './styles.module.css'; 4 | 5 | type FeatureItem = { 6 | title: string; 7 | svg: React.ComponentType>; 8 | description: JSX.Element; 9 | }; 10 | 11 | const FeatureList: FeatureItem[] = [ 12 | { 13 | title: 'Database agnostic', 14 | //Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default, 15 | description: ( 16 | <> 17 | upper/db supports the most popular databases and provides a consistent 18 | API across all of them. 19 | 20 | ), 21 | }, 22 | { 23 | title: 'ORM-ish API', 24 | //svg: require('@site/static/img/undraw_docusaurus_tree.svg').default, 25 | description: ( 26 | <> 27 | A simple API that abstracts common operations on the underlying 28 | database, making it easy to be productive from the start. 29 | 30 | ), 31 | }, 32 | { 33 | title: 'SQL friendly', 34 | //Svg: require('@site/static/img/undraw_docusaurus_react.svg').default, 35 | description: ( 36 | <> 37 | SQL is the most powerful way to interact with databases. You'll always 38 | have the option to write raw SQL queries anytime you choose. 39 | 40 | ), 41 | }, 42 | ]; 43 | 44 | function Feature({title, Svg, description}: FeatureItem) { 45 | return ( 46 |
47 |
48 |
49 |
50 | {title} 51 |

{description}

52 |
53 |
54 | ); 55 | 56 | /* 57 | return ( 58 |
59 |
60 | 61 |
62 |
63 | {title} 64 |

{description}

65 |
66 |
67 | ); 68 | */ 69 | } 70 | 71 | export default function HomepageFeatures(): JSX.Element { 72 | return ( 73 |
74 |
75 |
76 | {FeatureList.map((props, idx) => ( 77 | 78 | ))} 79 |
80 |
81 |
82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /site/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | -------------------------------------------------------------------------------- /site/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #2e8555; 10 | --ifm-color-primary-dark: #29784c; 11 | --ifm-color-primary-darker: #277148; 12 | --ifm-color-primary-darkest: #205d3b; 13 | --ifm-color-primary-light: #33925d; 14 | --ifm-color-primary-lighter: #359962; 15 | --ifm-color-primary-lightest: #3cad6e; 16 | --ifm-code-font-size: 95%; 17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 18 | } 19 | 20 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 21 | [data-theme='dark'] { 22 | --ifm-color-primary: #25c2a0; 23 | --ifm-color-primary-dark: #21af90; 24 | --ifm-color-primary-darker: #1fa588; 25 | --ifm-color-primary-darkest: #1a8870; 26 | --ifm-color-primary-light: #29d5b0; 27 | --ifm-color-primary-lighter: #32d8b4; 28 | --ifm-color-primary-lightest: #4fddbf; 29 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 30 | } 31 | 32 | .navbar { 33 | background-color: #9de0fd; 34 | xbackground-color: #8bf4ff; 35 | 36 | } 37 | -------------------------------------------------------------------------------- /site/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | 12 | background-color: #e0fbfc; 13 | background-image: url('/img/landscape.png'); 14 | background-size: auto 100%; 15 | background-position: top center; 16 | background-repeat: no-repeat; 17 | } 18 | 19 | .heroTitle { 20 | color: #555; 21 | } 22 | 23 | .heroTitleHighlight { 24 | color: #5281af; 25 | } 26 | 27 | .heroSubtitle { 28 | color: #5281af; 29 | } 30 | 31 | .getStarted { 32 | background-color: #0073e6; 33 | border: 1px solid #b2dcf8; 34 | color: #fff; 35 | } 36 | .getStarted:hover { 37 | background-color: #b2dcf8; 38 | color: #0073e6; 39 | } 40 | 41 | @media screen and (max-width: 996px) { 42 | .heroBanner { 43 | padding: 2rem; 44 | } 45 | } 46 | 47 | .buttons { 48 | display: flex; 49 | align-items: center; 50 | justify-content: center; 51 | } 52 | 53 | .heroPhrase { 54 | padding: 2rem 2rem; 55 | background-color: #fdf9d1; 56 | } 57 | -------------------------------------------------------------------------------- /site/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import Link from '@docusaurus/Link'; 3 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 4 | import Layout from '@theme/Layout'; 5 | import HomepageFeatures from '@site/src/components/HomepageFeatures'; 6 | import Heading from '@theme/Heading'; 7 | 8 | import styles from './index.module.css'; 9 | 10 | function HomepageHeader() { 11 | const {siteConfig} = useDocusaurusContext(); 12 | 13 | return ( 14 |
15 |
16 | 17 | upper/db 18 | 19 |

{siteConfig.tagline}

20 |
21 | 24 | Take the tour 25 | 26 |
27 |
28 |
29 | ); 30 | } 31 | 32 | function HeroPhrase() { 33 | return ( 34 |
35 |
36 |
37 |
38 | 39 | Built for productivity 40 | 41 |

42 | `upper/db` is the perfect library for developers who want to 43 | focus on the business logic of their applications without getting 44 | slowed down by writing simple SELECT * FROM table statements 45 | manually. It provides the convenience of an ORM while 46 | allowing you to use SQL whenever needed. 47 |

48 |

49 | `upper/db` aims to provide tools for handling the most common 50 | database operations while remaining unobtrusive in more advanced 51 | scenarios. 52 |

53 |
54 |
55 | upper/db 56 |
57 |
58 |
59 |
60 | ) 61 | } 62 | 63 | export default function Home(): JSX.Element { 64 | const {siteConfig} = useDocusaurusContext(); 65 | return ( 66 | 69 | 70 |
71 | 72 | 73 |
74 |
75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /site/src/theme/Root.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | 3 | export default function Root({ children }) { 4 | useEffect(() => { 5 | 6 | const catchTourLinks = (ev) => { 7 | const link = ev.target.closest("a"); 8 | if (link) { 9 | // disable SPA navigation for /tour links 10 | const href = link.getAttribute("href"); 11 | if (href.startsWith("/tour/") || href === "/tour") { 12 | ev.stopPropagation(); 13 | ev.preventDefault(); 14 | 15 | window.location.href = href; 16 | } 17 | } 18 | } 19 | 20 | document.addEventListener("click", catchTourLinks, true); 21 | return () => { 22 | document.removeEventListener("click", catchTourLinks, true); 23 | }; 24 | }, []); 25 | 26 | return <>{children}; 27 | } 28 | -------------------------------------------------------------------------------- /site/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upper/site/3db03597023a5b3927c8aa3e86f496160206b4ed/site/static/.nojekyll -------------------------------------------------------------------------------- /site/static/img/landscape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upper/site/3db03597023a5b3927c8aa3e86f496160206b4ed/site/static/img/landscape.png -------------------------------------------------------------------------------- /site/static/img/session-collection-result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upper/site/3db03597023a5b3927c8aa3e86f496160206b4ed/site/static/img/session-collection-result.png -------------------------------------------------------------------------------- /site/static/img/square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upper/site/3db03597023a5b3927c8aa3e86f496160206b4ed/site/static/img/square.png -------------------------------------------------------------------------------- /site/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@docusaurus/tsconfig", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tour/.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !go.mod 3 | !go.sum 4 | !tutorials/* 5 | !static/* 6 | !cmd 7 | -------------------------------------------------------------------------------- /tour/.gitignore: -------------------------------------------------------------------------------- 1 | *.sw? 2 | bin/ 3 | tour 4 | vendor 5 | -------------------------------------------------------------------------------- /tour/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.23.3 AS builder 2 | 3 | WORKDIR /go/src/github.com/upper/site/tour 4 | 5 | COPY ./ ./ 6 | 7 | RUN go install github.com/upper/site/tour/cmd/tour 8 | 9 | FROM alpine:3.20.3 10 | 11 | RUN apk update && \ 12 | apk add --no-cache \ 13 | libc6-compat 14 | 15 | WORKDIR /app 16 | 17 | RUN mkdir ./bin 18 | 19 | COPY --from=builder /go/bin/tour /app/bin/tour 20 | 21 | COPY --from=builder /go/src/github.com/upper/site/tour/static /app/static 22 | COPY --from=builder /go/src/github.com/upper/site/tour/tutorials /app/tutorials 23 | 24 | EXPOSE 4000 25 | 26 | CMD ["/app/bin/tour"] 27 | -------------------------------------------------------------------------------- /tour/ansible/tasks.yml: -------------------------------------------------------------------------------- 1 | - set_fact: 2 | image_name: "upper/tour" 3 | image_tag: "{{ docker_image_tag }}" 4 | container_name: "{{ container_prefix }}tour" 5 | app_bind_port: 4000 6 | container_bind_port: 4000 7 | - set_fact: 8 | image_full_name: "{{ public_docker_registry }}/{{ image_name }}:{{ image_tag }}" 9 | - name: "pull docker image: {{ image_full_name }}" 10 | docker_image: 11 | name: "{{ image_full_name }}" 12 | source: "pull" 13 | state: "present" 14 | force_source: "yes" 15 | - name: "stop container: {{ container_name }}" 16 | docker_container: 17 | name: "{{ container_name }}" 18 | state: "stopped" 19 | ignore_errors: "yes" 20 | - name: "(re)start container: {{ container_name }}" 21 | docker_container: 22 | name: "{{ container_name }}" 23 | image: "{{ image_full_name }}" 24 | state: "started" 25 | restart_policy: "always" 26 | networks: 27 | - name: "{{ network_name }}" 28 | ports: 29 | - "{{ container_bind_ip }}:{{ container_bind_port }}:{{ app_bind_port }}" 30 | env: 31 | ENV: "{{ env }}" 32 | PLAY_URL: "{{ playground_url }}" 33 | - name: "health check" 34 | uri: 35 | url: "http://{{ container_bind_ip }}:{{ container_bind_port }}/" 36 | method: GET 37 | status_code: 38 | - 200 39 | return_content: yes 40 | register: response 41 | until: response.status == 200 42 | retries: "{{ healthcheck_retries }}" 43 | delay: "{{ healthcheck_interval }}" 44 | -------------------------------------------------------------------------------- /tour/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/upper/site/tour 2 | 3 | go 1.23.2 4 | 5 | require ( 6 | github.com/go-chi/chi/v5 v5.2.1 7 | github.com/google/uuid v1.1.1 8 | github.com/russross/blackfriday/v2 v2.1.0 9 | github.com/upper/db/v4 v4.9.0 10 | ) 11 | 12 | require ( 13 | github.com/jackc/pgio v1.0.0 // indirect 14 | github.com/jackc/pgpassfile v1.0.0 // indirect 15 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 16 | github.com/jackc/pgtype v1.14.4 // indirect 17 | github.com/jackc/pgx/v5 v5.7.2 // indirect 18 | github.com/jackc/puddle/v2 v2.2.2 // indirect 19 | github.com/lib/pq v1.10.9 // indirect 20 | github.com/segmentio/fasthash v1.0.3 // indirect 21 | golang.org/x/crypto v0.35.0 // indirect 22 | golang.org/x/sync v0.11.0 // indirect 23 | golang.org/x/text v0.22.0 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /tour/static/css/codemirror-material.css: -------------------------------------------------------------------------------- 1 | /* 2 | Name: material 3 | Author: Mattia Astorino (http://github.com/equinusocio) 4 | Website: https://material-theme.site/ 5 | */ 6 | 7 | .cm-s-material.CodeMirror { 8 | background-color: #263238; 9 | color: #EEFFFF; 10 | } 11 | 12 | .cm-s-material .CodeMirror-gutters { 13 | background: #263238; 14 | color: #546E7A; 15 | border: none; 16 | } 17 | 18 | .cm-s-material .CodeMirror-guttermarker, 19 | .cm-s-material .CodeMirror-guttermarker-subtle, 20 | .cm-s-material .CodeMirror-linenumber { 21 | color: #546E7A; 22 | } 23 | 24 | .cm-s-material .CodeMirror-cursor { 25 | border-left: 1px solid #FFCC00; 26 | } 27 | .cm-s-material.cm-fat-cursor .CodeMirror-cursor { 28 | background-color: #5d6d5c80 !important; 29 | } 30 | .cm-s-material .cm-animate-fat-cursor { 31 | background-color: #5d6d5c80 !important; 32 | } 33 | 34 | .cm-s-material div.CodeMirror-selected { 35 | background: rgba(128, 203, 196, 0.2); 36 | } 37 | 38 | .cm-s-material.CodeMirror-focused div.CodeMirror-selected { 39 | background: rgba(128, 203, 196, 0.2); 40 | } 41 | 42 | .cm-s-material .CodeMirror-line::selection, 43 | .cm-s-material .CodeMirror-line>span::selection, 44 | .cm-s-material .CodeMirror-line>span>span::selection { 45 | background: rgba(128, 203, 196, 0.2); 46 | } 47 | 48 | .cm-s-material .CodeMirror-line::-moz-selection, 49 | .cm-s-material .CodeMirror-line>span::-moz-selection, 50 | .cm-s-material .CodeMirror-line>span>span::-moz-selection { 51 | background: rgba(128, 203, 196, 0.2); 52 | } 53 | 54 | .cm-s-material .CodeMirror-activeline-background { 55 | background: rgba(0, 0, 0, 0.5); 56 | } 57 | 58 | .cm-s-material .cm-keyword { 59 | color: #C792EA; 60 | } 61 | 62 | .cm-s-material .cm-operator { 63 | color: #89DDFF; 64 | } 65 | 66 | .cm-s-material .cm-variable-2 { 67 | color: #EEFFFF; 68 | } 69 | 70 | .cm-s-material .cm-variable-3, 71 | .cm-s-material .cm-type { 72 | color: #f07178; 73 | } 74 | 75 | .cm-s-material .cm-builtin { 76 | color: #FFCB6B; 77 | } 78 | 79 | .cm-s-material .cm-atom { 80 | color: #F78C6C; 81 | } 82 | 83 | .cm-s-material .cm-number { 84 | color: #FF5370; 85 | } 86 | 87 | .cm-s-material .cm-def { 88 | color: #82AAFF; 89 | } 90 | 91 | .cm-s-material .cm-string { 92 | color: #C3E88D; 93 | } 94 | 95 | .cm-s-material .cm-string-2 { 96 | color: #f07178; 97 | } 98 | 99 | .cm-s-material .cm-comment { 100 | color: #546E7A; 101 | } 102 | 103 | .cm-s-material .cm-variable { 104 | color: #f07178; 105 | } 106 | 107 | .cm-s-material .cm-tag { 108 | color: #FF5370; 109 | } 110 | 111 | .cm-s-material .cm-meta { 112 | color: #FFCB6B; 113 | } 114 | 115 | .cm-s-material .cm-attribute { 116 | color: #C792EA; 117 | } 118 | 119 | .cm-s-material .cm-property { 120 | color: #C792EA; 121 | } 122 | 123 | .cm-s-material .cm-qualifier { 124 | color: #DECB6B; 125 | } 126 | 127 | .cm-s-material .cm-variable-3, 128 | .cm-s-material .cm-type { 129 | color: #DECB6B; 130 | } 131 | 132 | 133 | .cm-s-material .cm-error { 134 | color: rgba(255, 255, 255, 1.0); 135 | background-color: #FF5370; 136 | } 137 | 138 | .cm-s-material .CodeMirror-matchingbracket { 139 | text-decoration: underline; 140 | color: white !important; 141 | } 142 | -------------------------------------------------------------------------------- /tour/static/css/main.css: -------------------------------------------------------------------------------- 1 | main.tour { 2 | margin-top: 60px; 3 | } 4 | main.tour .feature { 5 | padding-top: 0.5em; 6 | clear: both; 7 | } 8 | .exampleHeading { 9 | display: none; 10 | } 11 | main a, main a:link, main a:active, main a:visited { 12 | color: #1f8ace; 13 | } 14 | main .buttons a { 15 | padding: 0.5em 0.7em; 16 | line-height: 2em; 17 | cursor: pointer; 18 | } 19 | main .buttons a, 20 | main .buttons a:link, 21 | main .buttons a:visited, 22 | main .buttons a:active { 23 | color: #505055; 24 | } 25 | main a.run, main a.run:link, main a.run:active, main a.run:visited { 26 | color: #e74039; 27 | } 28 | 29 | ul.tour-nav { 30 | float: right; 31 | margin-top: 1.5em; 32 | } 33 | 34 | @media screen and (min-width: 980px) { 35 | ul.tour-nav { 36 | float: right; 37 | margin-top: 2em; 38 | } 39 | main.tour .feature { 40 | clear: none; 41 | } 42 | } 43 | 44 | ul.tour-nav li { 45 | line-height: 2.375em; 46 | font-size: 1.2em; 47 | float: left; 48 | text-align: center; 49 | } 50 | ul.tour-nav li a { 51 | display: block; 52 | white-space: nowrap; 53 | } 54 | ul.tour-nav li.pager { 55 | width: 60px; 56 | } 57 | 58 | .feature { 59 | z-index: 1; 60 | } 61 | .feature .code-snippet .CodeMirror { 62 | height: 100%; 63 | } 64 | 65 | blockquote { 66 | padding: 0.5em; 67 | padding-left: 3em; 68 | color: gray; 69 | background: #fafafa; 70 | margin: 0; 71 | } 72 | 73 | pre { 74 | font-family: monospace; 75 | } 76 | -------------------------------------------------------------------------------- /tour/static/css/syntax.css: -------------------------------------------------------------------------------- 1 | .highlight .hll { background-color: #ffc; } 2 | .highlight .c { color: #999; } /* Comment */ 3 | .highlight .err { color: #a00; background-color: #faa } /* Error */ 4 | .highlight .k { color: #069; } /* Keyword */ 5 | .highlight .o { color: #555 } /* Operator */ 6 | .highlight .cm { color: #09f; font-style: italic } /* Comment.Multiline */ 7 | .highlight .cp { color: #099 } /* Comment.Preproc */ 8 | .highlight .c1 { color: #999; } /* Comment.Single */ 9 | .highlight .cs { color: #999; } /* Comment.Special */ 10 | .highlight .gd { background-color: #fcc; border: 1px solid #c00 } /* Generic.Deleted */ 11 | .highlight .ge { font-style: italic } /* Generic.Emph */ 12 | .highlight .gr { color: #f00 } /* Generic.Error */ 13 | .highlight .gh { color: #030; } /* Generic.Heading */ 14 | .highlight .gi { background-color: #cfc; border: 1px solid #0c0 } /* Generic.Inserted */ 15 | .highlight .go { color: #aaa } /* Generic.Output */ 16 | .highlight .gp { color: #009; } /* Generic.Prompt */ 17 | .highlight .gs { } /* Generic.Strong */ 18 | .highlight .gu { color: #030; } /* Generic.Subheading */ 19 | .highlight .gt { color: #9c6 } /* Generic.Traceback */ 20 | .highlight .kc { color: #069; } /* Keyword.Constant */ 21 | .highlight .kd { color: #069; } /* Keyword.Declaration */ 22 | .highlight .kn { color: #069; } /* Keyword.Namespace */ 23 | .highlight .kp { color: #069 } /* Keyword.Pseudo */ 24 | .highlight .kr { color: #069; } /* Keyword.Reserved */ 25 | .highlight .kt { color: #078; } /* Keyword.Type */ 26 | .highlight .m { color: #f60 } /* Literal.Number */ 27 | .highlight .s { color: #d44950 } /* Literal.String */ 28 | .highlight .na { color: #4f9fcf } /* Name.Attribute */ 29 | .highlight .nb { color: #366 } /* Name.Builtin */ 30 | .highlight .nc { color: #0a8; } /* Name.Class */ 31 | .highlight .no { color: #360 } /* Name.Constant */ 32 | .highlight .nd { color: #99f } /* Name.Decorator */ 33 | .highlight .ni { color: #999; } /* Name.Entity */ 34 | .highlight .ne { color: #c00; } /* Name.Exception */ 35 | .highlight .nf { color: #c0f } /* Name.Function */ 36 | .highlight .nl { color: #99f } /* Name.Label */ 37 | .highlight .nn { color: #0cf; } /* Name.Namespace */ 38 | .highlight .nt { color: #2f6f9f; } /* Name.Tag */ 39 | .highlight .nv { color: #033 } /* Name.Variable */ 40 | .highlight .ow { color: #000; } /* Operator.Word */ 41 | .highlight .w { color: #bbb } /* Text.Whitespace */ 42 | .highlight .mf { color: #f60 } /* Literal.Number.Float */ 43 | .highlight .mh { color: #f60 } /* Literal.Number.Hex */ 44 | .highlight .mi { color: #f60 } /* Literal.Number.Integer */ 45 | .highlight .mo { color: #f60 } /* Literal.Number.Oct */ 46 | .highlight .sb { color: #c30 } /* Literal.String.Backtick */ 47 | .highlight .sc { color: #c30 } /* Literal.String.Char */ 48 | .highlight .sd { color: #c30; font-style: italic } /* Literal.String.Doc */ 49 | .highlight .s2 { color: #c30 } /* Literal.String.Double */ 50 | .highlight .se { color: #c30; } /* Literal.String.Escape */ 51 | .highlight .sh { color: #c30 } /* Literal.String.Heredoc */ 52 | .highlight .si { color: #a00 } /* Literal.String.Interpol */ 53 | .highlight .sx { color: #c30 } /* Literal.String.Other */ 54 | .highlight .sr { color: #3aa } /* Literal.String.Regex */ 55 | .highlight .s1 { color: #c30 } /* Literal.String.Single */ 56 | .highlight .ss { color: #fc3 } /* Literal.String.Symbol */ 57 | .highlight .bp { color: #366 } /* Name.Builtin.Pseudo */ 58 | .highlight .vc { color: #033 } /* Name.Variable.Class */ 59 | .highlight .vg { color: #033 } /* Name.Variable.Global */ 60 | .highlight .vi { color: #033 } /* Name.Variable.Instance */ 61 | .highlight .il { color: #f60 } /* Literal.Number.Integer.Long */ 62 | 63 | .css .o, 64 | .css .o + .nt, 65 | .css .nt + .nt { color: #999; } 66 | -------------------------------------------------------------------------------- /tour/static/css/upper.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | padding: 2rem 3rem; 3 | color: #333; 4 | text-align: center; 5 | } 6 | .footer .hearth { 7 | color: #e74039; 8 | } 9 | .footer a { 10 | text-decoration: underline; 11 | } 12 | main .CodeMirror { 13 | height: 100%; 14 | } 15 | -------------------------------------------------------------------------------- /tour/static/img/gopher-tip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upper/site/3db03597023a5b3927c8aa3e86f496160206b4ed/tour/static/img/gopher-tip.png -------------------------------------------------------------------------------- /tour/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | The upper/db tour 30 | 31 | 41 | 42 | 43 |
44 | 58 |
59 |
60 |
61 |
62 | 63 |
64 |
    65 | {{ if .Prev }} 66 | 67 | {{ end }} 68 |
  • {{ .Current }}/{{ .Total }}
  • 69 | {{ if .Next }} 70 | 71 | {{ end }} 72 |
73 |
74 | {{.Readme}} 75 |
76 |
    77 | {{ if .Prev }} 78 | 79 | {{ end }} 80 |
  • {{ .Current }}/{{ .Total }}
  • 81 | {{ if .Next }} 82 | 83 | {{ end }} 84 |
85 |
86 |
87 |
88 | 89 |
90 |
91 |
92 |
93 |
94 | 95 |
96 | 97 | Made with by @xiam 99 | 100 |
101 | 102 | 103 | 104 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /tour/static/js/main.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upper/site/3db03597023a5b3927c8aa3e86f496160206b4ed/tour/static/js/main.js -------------------------------------------------------------------------------- /tour/tutorials/basics/01/README.md: -------------------------------------------------------------------------------- 1 | # Connect to a database 2 | 3 | ## 1. Get a Database Adapter 4 | 5 | To connect to a database, you need an adapter. Use `go get` to fetch it like 6 | this: 7 | 8 | ```sh 9 | go get github.com/upper/db/v4/adapter/{$ADAPTER} 10 | ``` 11 | 12 | Where `$ADAPTER` could be any of the following: 13 | 14 | * `postgresql`: for [PostgreSQL](https://www.postgresql.org/) 15 | * `mysql`: for [MySQL](https://www.mysql.com/) 16 | * `sqlite`: for [SQLite](https://www.sqlite.org/index.html) 17 | * `cockroachdb`: for [CockroachDB](https://www.cockroachlabs.com/product/) 18 | * `mongo`: for [MongoDB](https://www.mongodb.com/) 19 | * `ql`: for [QL](https://pkg.go.dev/modernc.org/ql) 20 | 21 | For instance, if you’d like to use the PostgreSQL adapter, you’d first run: 22 | 23 | ```sh 24 | go get -u github.com/upper/db/v4/adapter/postgresql 25 | ``` 26 | 27 | to get the adapter, and then you can import it into your 28 | project: 29 | 30 | ```sh 31 | import ( 32 | "github.com/upper/db/v4/adapter/postgresql" 33 | ) 34 | ``` 35 | 36 | ## 2. Configure a Database Connection 37 | 38 | Set the database credentials using the `ConnectionURL` type provided by the 39 | adapter: 40 | 41 | ```go 42 | import ( 43 | "github.com/upper/v4/adapter/postgresql" 44 | ) 45 | 46 | var settings = postgresql.ConnectionURL{ 47 | Database: `booktown`, 48 | Host: `postgres`, 49 | User: `demo`, 50 | Password: `b4dp4ss`, 51 | } 52 | ``` 53 | 54 | Note that the `ConnectionURL` (which satisfies the [db.ConnectionURL][1] 55 | interface) varies from database engine to another. The connection properties 56 | required by each adapter are explained in detail [here][2]. 57 | 58 | ## 3. Establish a Connection 59 | 60 | Use the `Open` function to establish a connection with the database server: 61 | 62 | ```go 63 | sess, err := postgresql.Open(settings) 64 | ... 65 | ``` 66 | 67 | ## 4. Close the Connection 68 | 69 | Set the database connection to close automatically after completing all tasks. 70 | Use `Close` and `defer`: 71 | 72 | ```go 73 | defer sess.Close() 74 | ``` 75 | 76 | [1]: https://pkg.go.dev/github.com/upper/db/v4#ConnectionURL 77 | [2]: https://upper.io/v4/adapter/ 78 | -------------------------------------------------------------------------------- /tour/tutorials/basics/01/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | // Import an adapter 8 | "github.com/upper/db/v4/adapter/postgresql" 9 | ) 10 | 11 | // Set the database credentials using the ConnectionURL type provided by the 12 | // adapter. 13 | var settings = postgresql.ConnectionURL{ 14 | Database: "booktown", 15 | Host: "postgres", 16 | User: "demo", 17 | Password: "b4dp4ss", 18 | Options: map[string]string{ 19 | "sslmode": "disable", // Disable TLS 20 | }, 21 | } 22 | 23 | func main() { 24 | // Use Open to access the database. 25 | sess, err := postgresql.Open(settings) 26 | if err != nil { 27 | log.Fatal("Open: ", err) 28 | } 29 | defer sess.Close() 30 | 31 | // The settings variable has a String method that builds and returns a valid 32 | // DSN. This DSN may be different depending on the database you're connecting 33 | // to. 34 | fmt.Printf("Connected to %q with DSN:\n\t%q", sess.Name(), settings) 35 | } 36 | -------------------------------------------------------------------------------- /tour/tutorials/basics/02/README.md: -------------------------------------------------------------------------------- 1 | # List all Collections (Tables) in a Database 2 | 3 | Use the `Collections` method on a `db.Session` to get all the collections, or 4 | tables, in the database: 5 | 6 | ```go 7 | collections, err := sess.Collections() 8 | ... 9 | 10 | for i := range collections { 11 | log.Printf("-> %s", collections[i].Name()) 12 | } 13 | ``` 14 | 15 | The `db.Session` interface provides methods that work on both SQL and NoSQL 16 | databases. In light of this, sets of records, or rows, in a database are 17 | referred to as 'collections', and no particular distinction is made between SQL 18 | _tables_ and NoSQL _collections_. 19 | -------------------------------------------------------------------------------- /tour/tutorials/basics/02/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/upper/db/v4/adapter/postgresql" 8 | ) 9 | 10 | var settings = postgresql.ConnectionURL{ 11 | Database: "booktown", 12 | Host: "postgres", 13 | User: "demo", 14 | Password: "b4dp4ss", 15 | Options: map[string]string{ 16 | "sslmode": "disable", // Disable TLS 17 | }, 18 | } 19 | 20 | func main() { 21 | sess, err := postgresql.Open(settings) 22 | if err != nil { 23 | log.Fatal("Open: ", err) 24 | } 25 | defer sess.Close() 26 | 27 | fmt.Printf("Collections in database %q:\n", sess.Name()) 28 | 29 | // The Collections method returns references to all the collections in the 30 | // database. 31 | collections, err := sess.Collections() 32 | if err != nil { 33 | log.Fatal("Collections: ", err) 34 | } 35 | 36 | for i := range collections { 37 | // Name returns the name of the collection. 38 | fmt.Printf("-> %q\n", collections[i].Name()) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tour/tutorials/basics/03/README.md: -------------------------------------------------------------------------------- 1 | # Get a collection by name 2 | 3 | Use the `Collection` method on a [`db.Session`][1] to get a reference to a 4 | specific collection: 5 | 6 | ```go 7 | col := sess.Collection("books") 8 | ``` 9 | 10 | A collection reference satisfies [`db.Collection`][2] and gives you access to a 11 | set of methods for retrieving and manipulating data, such as `Find` (to search 12 | for specific records in the collection) and `Insert` (to add more records to a 13 | collection). 14 | 15 | Note that if you create a reference to a collection that doesn’t exist, you’ll 16 | see a warning message: 17 | 18 | ``` 19 | 2020/07/01 00:11:33 upper/db: log_level=WARNING file=/go/src/git... 20 | Session ID: 00001 21 | Query: SELECT "pg_attribute"."attname" AS "pkey" ... 22 | Error: pq: relation "fake_collection" does not exist 23 | Time taken: 0.00129s 24 | Context: context.Background 25 | ``` 26 | 27 | If you’d prefer not to see warning messages, set a higher logging level: 28 | 29 | ```go 30 | db.LC().SetLevel(db.LogLevelError) 31 | ``` 32 | 33 | Use the `Exists` method to check whether a collection exists or not: 34 | 35 | ```go 36 | exists, err := collection.Exists() 37 | if errors.Is(err, db.ErrCollectionDoesNotExist) { 38 | log.Printf("Collection does not exist: %v", err) 39 | } 40 | ``` 41 | 42 | [1]: https://pkg.go.dev/github.com/upper/db/v4#Session 43 | [2]: https://pkg.go.dev/github.com/upper/db/v4#Collection 44 | -------------------------------------------------------------------------------- /tour/tutorials/basics/03/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/upper/db/v4/adapter/postgresql" 8 | ) 9 | 10 | var settings = postgresql.ConnectionURL{ 11 | Database: "booktown", 12 | Host: "postgres", 13 | User: "demo", 14 | Password: "b4dp4ss", 15 | Options: map[string]string{ 16 | "sslmode": "disable", // Disable TLS 17 | }, 18 | } 19 | 20 | func main() { 21 | sess, err := postgresql.Open(settings) 22 | if err != nil { 23 | log.Fatal("Open: ", err) 24 | } 25 | defer sess.Close() 26 | 27 | // The Collection method returns a reference to a specific collection in the 28 | // database. In this case, the collection is a table named "books". 29 | col := sess.Collection("books") 30 | 31 | // Get the name of the collection. 32 | fmt.Printf("The name of the collection is %q.\n", col.Name()) 33 | 34 | // You can create references to collections that don't exist (yet). That 35 | // might be useful when working with document-based databases. 36 | nonExistentCollection := sess.Collection("fake") 37 | ok, err := nonExistentCollection.Exists() 38 | fmt.Printf("Q: Does collection %q exists?\n", nonExistentCollection.Name()) 39 | fmt.Printf("R: %v (%v)", ok, err) 40 | } 41 | -------------------------------------------------------------------------------- /tour/tutorials/final/01/README.md: -------------------------------------------------------------------------------- 1 | # Where to Go From Here? 2 | 3 | Thanks for taking the [upper/db][1] tour! 4 | 5 | We hope [upper/db][1] helps you be more productive when using databases and Go. 6 | 7 | You can check out the reference at 8 | [https://pkg.go.dev/github.com/upper/db/v4](https://pkg.go.dev/github.com/upper/db/v4), 9 | the code repository at [github.com/upper/db](https://github.com/upper/db), and 10 | the website at [upper.io/v4](https://upper.io/v4). 11 | 12 | Happy hacking! 13 | 14 | [1]: https://upper.io/v4 15 | -------------------------------------------------------------------------------- /tour/tutorials/final/01/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func main() { 8 | fmt.Println("Thanks!") 9 | } 10 | -------------------------------------------------------------------------------- /tour/tutorials/queries/01/README.md: -------------------------------------------------------------------------------- 1 | # Map database records to Go structs 2 | 3 | Let's suppose the database we're working on has a "books" table that was 4 | created like this: 5 | 6 | ```sql 7 | CREATE TABLE "books" ( 8 | "id" INTEGER NOT NULL, 9 | "title" VARCHAR NOT NULL, 10 | "author_id" INTEGER, 11 | "subject_id" INTEGER, 12 | CONSTRAINT "books_id_pkey" PRIMARY KEY ("id") 13 | ); 14 | ``` 15 | 16 | We can represent a single record from such a table and the fields accompanying 17 | it with a Go struct accompanied by struct tags in exported fields: 18 | 19 | ```go 20 | type Book struct { 21 | ID uint `db:"id"` 22 | Title string `db:"title"` 23 | AuthorID uint `db:"author_id"` 24 | SubjectID uint `db:"subject_id"` 25 | } 26 | ``` 27 | 28 | The `db` field tag is required so `upper/db` can map columns to struct fields. 29 | 30 | Please note that: 31 | 32 | * Fields and columns must be of compatible types (`upper/db` will handle most 33 | reasonable conversions automatically). 34 | * Fields must be exported and have a `db` tag; otherwise, they will be ignored. 35 | 36 | If the table contains a special column to represent automatically-generated 37 | values like IDs, serials, dates, etc. add the `omitempty` option to the field 38 | tag: 39 | 40 | ```go 41 | type Book struct { 42 | ID uint `db:"id,omitempty"` 43 | } 44 | ``` 45 | 46 | The `omitempty` option will make `upper/db` ignore zero-valued fields when 47 | building `INSERT` and `UPDATE` statements so the database can correctly 48 | generate them. 49 | -------------------------------------------------------------------------------- /tour/tutorials/queries/01/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | // "github.com/upper/db/v4" 8 | "github.com/upper/db/v4/adapter/postgresql" 9 | ) 10 | 11 | var settings = postgresql.ConnectionURL{ 12 | Database: "booktown", 13 | Host: "postgres", 14 | User: "demo", 15 | Password: "b4dp4ss", 16 | Options: map[string]string{ 17 | "sslmode": "disable", // Disable TLS 18 | }, 19 | } 20 | 21 | // Book represents an record from the "books" table. The fields accompanying 22 | // the record represent the columns in the table and are mapped to Go values 23 | // below. 24 | type Book struct { 25 | ID uint `db:"id,omitempty"` 26 | Title string `db:"title"` 27 | AuthorID uint `db:"author_id"` 28 | SubjectID uint `db:"subject_id"` 29 | 30 | SkippedField string 31 | } 32 | 33 | func main() { 34 | sess, err := postgresql.Open(settings) 35 | if err != nil { 36 | log.Fatal("postgresql.Open: ", err) 37 | } 38 | defer sess.Close() 39 | 40 | booksCol := sess.Collection("books") 41 | 42 | // Uncomment the following line (and the github.com/upper/db import path) to 43 | // write SQL statements to os.Stdout: 44 | // db.LC().SetLevel(db.LogLevelDebug) 45 | 46 | // Find().All() maps all the records from the books collection. 47 | books := []Book{} 48 | err = booksCol.Find().All(&books) 49 | if err != nil { 50 | log.Fatal("booksCol.Find: ", err) 51 | } 52 | 53 | // Print the queried information. 54 | fmt.Printf("Records in the %q collection:\n", booksCol.Name()) 55 | for i := range books { 56 | fmt.Printf("record #%d: %#v\n", i, books[i]) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tour/tutorials/queries/02/README.md: -------------------------------------------------------------------------------- 1 | # Query database records 2 | 3 | The `Collection` method takes the name of a table in the database and returns a 4 | value that satisfies the [db.Collection][1] interface: 5 | 6 | ```go 7 | booksTable := sess.Collection("books") 8 | ``` 9 | 10 | One of the methods defined by the `db.Collection` interface is `Find`. 11 | 12 | Use `Find` to search for specific records within the collection hierarchy. 13 | `Find` returns a [db.Result][2] object, which is delimited by the condition 14 | passed to `Find` and can contain zero, one, or many database records. 15 | 16 | The `db.Result` interface works the same on all supported databases. 17 | 18 | The following query fetches and maps all the records from the "books" table: 19 | 20 | ```go 21 | var books []Book 22 | 23 | res := booksTable.Find() 24 | err := res.All(&books) 25 | ``` 26 | 27 | You can build the query to return records in different ways, such as sorted by 28 | title (descending order): 29 | 30 | ```go 31 | var books []Book 32 | 33 | res := booksTable.Find().OrderBy("-title") 34 | err := res.All(&books) 35 | ``` 36 | 37 | Use `One` instead of `All` if you want to retrieve a single record from the 38 | set: 39 | 40 | ```go 41 | var book Book 42 | 43 | res := booksTable.Find(db.Cond{"id": 4}) 44 | err := res.One(&book) 45 | ``` 46 | 47 | You can also determine the total number of records in the result set with 48 | `Count`: 49 | 50 | ```go 51 | res := booksTable.Find() 52 | 53 | total, err := res.Count() 54 | ... 55 | ``` 56 | 57 | Depending on your database type, you have many [options for defining 58 | queries][3]. 59 | 60 | ## Query builder and raw SQL 61 | 62 | In the particular case of adapters for SQL databases, you can also choose to 63 | use a query builder (for more control over your query): 64 | 65 | ```go 66 | q := sess.SQL().Select().From("books") 67 | 68 | var books []Book 69 | err := q.All(&books) 70 | ``` 71 | 72 | ... or raw SQL (for absolute control over your query): 73 | 74 | ``` 75 | rows, err := sess.SQL().Query("SELECT * FROM books") 76 | // rows is a regular *sql.Rows object. 77 | ``` 78 | 79 | [1]: https://pkg.go.dev/github.com/upper/db/v4#Collection 80 | [2]: https://pkg.go.dev/github.com/upper/db/v4#Result 81 | [3]: https://upper.io/v4/getting-started/agnostic-db-api 82 | -------------------------------------------------------------------------------- /tour/tutorials/queries/02/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/upper/db/v4/adapter/postgresql" 8 | ) 9 | 10 | var settings = postgresql.ConnectionURL{ 11 | Database: "booktown", 12 | Host: "postgres", 13 | User: "demo", 14 | Password: "b4dp4ss", 15 | Options: map[string]string{ 16 | "sslmode": "disable", // Disable TLS 17 | }, 18 | } 19 | 20 | type Book struct { 21 | ID uint `db:"id"` 22 | Title string `db:"title"` 23 | AuthorID uint `db:"author_id"` 24 | SubjectID uint `db:"subject_id"` 25 | } 26 | 27 | func main() { 28 | sess, err := postgresql.Open(settings) 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | defer sess.Close() 33 | 34 | // "books" is a table that already exists in our database. 35 | booksTable := sess.Collection("books") 36 | 37 | // Use Find to create a result set (db.Result). 38 | res := booksTable.Find() 39 | 40 | // The result set can be modified by chaining different db.Result methods 41 | // (like Where, And, OrderBy, Select Limit, and Group). Keep in mind that 42 | // db.Result is immutable so you'll probably have to reassign the variable 43 | // that is holding that object. 44 | res = res.OrderBy("-title") // ORDER BY title DESC 45 | 46 | // Result sets are lazy, meaning that the query will be built or sent to the 47 | // database until one of the methods that require database interaction is 48 | // used (for example, One or All). 49 | var books []Book 50 | if err := res.All(&books); err != nil { 51 | log.Fatal("res.All: ", err) 52 | } 53 | 54 | // The All method copies every single record in the result set into a Go slice. 55 | fmt.Printf("Records in the %q table:\n", booksTable.Name()) 56 | for _, book := range books { 57 | fmt.Printf("%d:\t%q\n", book.ID, book.Title) 58 | } 59 | fmt.Println("") 60 | 61 | // Find out how many elements the result set has with Count. 62 | total, err := res.Count() 63 | if err != nil { 64 | log.Fatal("Count: ", err) 65 | } 66 | fmt.Printf("There are %d records on %q", total, booksTable.Name()) 67 | fmt.Println("") 68 | 69 | // Since result sets are stateless and immutable, they can be reused many 70 | // times on different queries. 71 | recordsThatBeginWithP := res.And("title LIKE", "P%") // WHERE ... AND title LIKE 'P%' 72 | 73 | // The original `res` result set is not altered. 74 | total1, err := res.Count() 75 | if err != nil { 76 | log.Fatal("Count: ", err) 77 | } 78 | 79 | // ... while the new result set is modified. 80 | total2, err := recordsThatBeginWithP.Count() 81 | if err != nil { 82 | log.Fatal("Count: ", err) 83 | } 84 | 85 | fmt.Printf("There are still %d records on %q\n", total1, booksTable.Name()) 86 | fmt.Printf("And there are %d records on %q that begin with \"P\"\n", total2, booksTable.Name()) 87 | } 88 | -------------------------------------------------------------------------------- /tour/tutorials/queries/03/README.md: -------------------------------------------------------------------------------- 1 | # Query large result sets 2 | 3 | If you're working with significantly large data sets, copying all matching 4 | records into a slice might be impractical for memory and performance reasons. 5 | 6 | In this case, you might want to use `Next` to map all the records in the 7 | result-set one by one: 8 | 9 | ```go 10 | res := booksTable.Find().OrderBy("-id") 11 | 12 | var book Book 13 | for res.Next(&book) { 14 | // ... 15 | } 16 | ``` 17 | 18 | `Next` will return `true` until there are no more records left to be read in 19 | the result set. 20 | 21 | When handling results individually, you'll also need to manually check for 22 | errors (with `Err`) and free locked resources (with `Close`). 23 | 24 | ```go 25 | res := booksTable.Find(...) 26 | defer res.Close() 27 | 28 | for res.Next(&book) { 29 | ... 30 | } 31 | 32 | if err := res.Err(); err != nil { 33 | ... 34 | } 35 | ``` 36 | 37 | Calling `Close` is not required when using `One` or `All`, as they're closed 38 | automatically. 39 | -------------------------------------------------------------------------------- /tour/tutorials/queries/03/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/upper/db/v4/adapter/postgresql" 8 | ) 9 | 10 | var settings = postgresql.ConnectionURL{ 11 | Database: "booktown", 12 | Host: "postgres", 13 | User: "demo", 14 | Password: "b4dp4ss", 15 | Options: map[string]string{ 16 | "sslmode": "disable", // Disable TLS 17 | }, 18 | } 19 | 20 | type Book struct { 21 | ID uint `db:"id"` 22 | Title string `db:"title"` 23 | AuthorID uint `db:"author_id"` 24 | SubjectID uint `db:"subject_id"` 25 | 26 | IgnoredField string `db:"-"` 27 | } 28 | 29 | func main() { 30 | sess, err := postgresql.Open(settings) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | defer sess.Close() 35 | 36 | booksTable := sess.Collection("books") 37 | 38 | // Order by "id" (descending) 39 | res := booksTable.Find().OrderBy("-ID") 40 | defer res.Close() // Remember to close the result set. 41 | 42 | // Next goes over all records one by one. It proves useful when copying large 43 | // data sets into a slice is impractical. 44 | var book Book 45 | for res.Next(&book) { 46 | fmt.Printf("%d:\t%q\n", book.ID, book.Title) 47 | } 48 | 49 | // In the event of a problem Next returns false, that will break the loop and 50 | // generate an error (which can be retrieved by calling Err). On the other 51 | // hand, when the loop is successfully completed (even if the data set had no 52 | // records), Err will be nil. 53 | if err := res.Err(); err != nil { 54 | log.Printf("ERROR: %v", err) 55 | log.Fatalf(`SUGGESTION: change OrderBy("-ID") into OrderBy("id") and try again.`) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tour/tutorials/queries/04/README.md: -------------------------------------------------------------------------------- 1 | # Paginate results 2 | 3 | The pagination API lets you split the results of a query into chunks containing 4 | a maximum number of records. 5 | 6 | ### Number-based pagination 7 | 8 | Number-based pagination splits the results into a fixed number of pages: 9 | 10 | ```go 11 | // Create a result-set 12 | res = sess.Collection("posts").Find() 13 | 14 | // Set the number of records by chunk 15 | p := res.Paginate(20) 16 | 17 | // Get the first chunk of results (page 1) 18 | err = p.All(&posts) 19 | 20 | // Get the second chunk of results (limit 20, offset 40) 21 | err = p.Page(2).All(&posts) 22 | ``` 23 | 24 | If you're working with the SQL builder, use `SelectFrom` instead of 25 | `Collection`: 26 | 27 | ```go 28 | q = sess.SQL().SelectFrom("posts").Paginate(20) 29 | ``` 30 | 31 | ### Cursor-based pagination 32 | 33 | If number-based pagination does not fit your case, you can also set the record 34 | where you want to begin and the results you want to fetch thereon: 35 | 36 | ```go 37 | res = sess.Collection("posts"). 38 | Find(). 39 | Paginate(20). 40 | Cursor("id") 41 | 42 | err = res.All(&posts) 43 | 44 | // Get the results that follow the last record of the previous 45 | // query in groups of 20. 46 | res = res.NextPage(posts[len(posts)-1].ID) 47 | 48 | // Get the first 20 results (limit 20, offset 20) 49 | err = res.All(&posts) 50 | ``` 51 | 52 | ### Pagination API tools 53 | 54 | To know the total number of entries and pages into which the result set was 55 | divided, you can use: 56 | 57 | ```go 58 | res = res.Paginate(23) 59 | 60 | totalNumberOfEntries, err = res.TotalEntries() 61 | ... 62 | 63 | totalNumberOfPages, err = res.TotalPages() 64 | ... 65 | ``` 66 | -------------------------------------------------------------------------------- /tour/tutorials/queries/04/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/upper/db/v4/adapter/postgresql" 8 | ) 9 | 10 | var settings = postgresql.ConnectionURL{ 11 | Database: "booktown", 12 | Host: "postgres", 13 | User: "demo", 14 | Password: "b4dp4ss", 15 | Options: map[string]string{ 16 | "sslmode": "disable", // Disable TLS 17 | }, 18 | } 19 | 20 | type Customer struct { 21 | ID uint `db:"id"` 22 | FirstName string `db:"first_name"` 23 | LastName string `db:"last_name"` 24 | } 25 | 26 | func main() { 27 | sess, err := postgresql.Open(settings) 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | defer sess.Close() 32 | 33 | customersCol := sess.Collection("customers") 34 | 35 | // Create a paginator and sets 10 records by page. 36 | res := customersCol.Find(). 37 | OrderBy("last_name", "first_name") 38 | 39 | p := res.Paginate(10) 40 | 41 | // Try changing the page number and running the example 42 | const pageNumber = 2 43 | 44 | // Copy all the records from the current page into the customers slice. 45 | var customers []Customer 46 | err = p.Page(pageNumber).All(&customers) 47 | if err != nil { 48 | log.Fatal(err) 49 | } 50 | 51 | fmt.Printf("List of costumers (page %d):\n", pageNumber) 52 | for i, customer := range customers { 53 | fmt.Printf("%d: %q, %q\n", i, customer.LastName, customer.FirstName) 54 | } 55 | 56 | totalNumberOfEntries, err := p.TotalEntries() 57 | if err != nil { 58 | log.Fatal("p.TotalEntries: ", err) 59 | } 60 | 61 | totalNumberOfPages, err := p.TotalPages() 62 | if err != nil { 63 | log.Fatal("p.TotalPages: ", err) 64 | } 65 | 66 | fmt.Println("") 67 | fmt.Printf("Total entries: %d. Total pages: %d", totalNumberOfEntries, totalNumberOfPages) 68 | } 69 | -------------------------------------------------------------------------------- /tour/tutorials/queries/05/README.md: -------------------------------------------------------------------------------- 1 | # Debug queries 2 | 3 | These are the logging levels `upper/db` comes with, ranging from the lowest 4 | severity (trace) to the highest (panic). 5 | 6 | * `db.LogLevelTrace` 7 | * `db.LogLevelDebug` 8 | * `db.LogLevelInfo` 9 | * `db.LogLevelWarn` 10 | * `db.LogLevelError` 11 | * `db.LogLevelFatal` 12 | * `db.LogLevelPanic` 13 | 14 | By default, `upper/db` is set to `db.LogLevelWarn`. Use `db.LC()` to set a 15 | different logging level: 16 | 17 | ```go 18 | db.LC().SetLevel(db.LogLevelDebug) 19 | ``` 20 | 21 | Set an appropriate logging level in production, as using levels lower than 22 | `db.LogLevelWarn` could make things pretty slow and verbose. 23 | 24 | ```go 25 | db.LC().SetLevel(db.LogLevelError) 26 | ``` 27 | 28 | # Handle Errors 29 | 30 | Error scenarios may or may not be fatal depending on the nature of your 31 | application, so make sure you're handling them properly: 32 | 33 | ```go 34 | err = booksTable.Find(1).One(&book) 35 | if err != nil { 36 | if errors.Is(err, db.ErrNoMoreRows) { 37 | // No rows found, which is okay. 38 | return nil 39 | } 40 | return err 41 | } 42 | ``` 43 | -------------------------------------------------------------------------------- /tour/tutorials/queries/05/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/upper/db/v4" 8 | "github.com/upper/db/v4/adapter/postgresql" 9 | ) 10 | 11 | var settings = postgresql.ConnectionURL{ 12 | Database: "booktown", 13 | Host: "postgres", 14 | User: "demo", 15 | Password: "demopass", 16 | Options: map[string]string{ 17 | "sslmode": "disable", // Disable TLS 18 | }, 19 | } 20 | 21 | // Book represents a record from the "books" table. This table has an integer 22 | // primary key ("id"): 23 | // 24 | // booktown=> \d books 25 | // 26 | // Table "public.books" 27 | // Column | Type | Modifiers 28 | // 29 | // ------------+---------+----------- 30 | // 31 | // id | integer | not null 32 | // title | varchar | not null 33 | // author_id | integer | 34 | // subject_id | integer | 35 | // 36 | // Indexes: 37 | // 38 | // "books_id_pkey" PRIMARY KEY, btree (id) 39 | // "books_title_idx" btree (title) 40 | type Book struct { 41 | ID uint `db:"id"` 42 | Title string `db:"title"` 43 | AuthorID uint `db:"author_id"` 44 | SubjectID uint `db:"subject_id"` 45 | } 46 | 47 | func main() { 48 | // Set logging level to DEBUG 49 | db.LC().SetLevel(db.LogLevelDebug) 50 | 51 | sess, err := postgresql.Open(settings) 52 | if err != nil { 53 | fmt.Println("ERROR: Could not establish a connection with database: %v.", err) 54 | log.Fatalf(`SUGGESTION: Set password to "b4dp4ss" and try again.`) 55 | } 56 | defer sess.Close() 57 | 58 | fmt.Printf("Connected to %q using %q\n", sess.Name(), sess.ConnectionURL()) 59 | 60 | booksTable := sess.Collection("books") 61 | 62 | // Find looks for a record that matches the integer primary key of the 63 | // "books" table. 64 | var book Book 65 | err = booksTable.Find(1).One(&book) 66 | if err != nil { 67 | if err == db.ErrNoMoreRows { 68 | fmt.Printf("ERROR: %v\n", err) 69 | log.Fatalf("SUGGESTION: Change Find(1) into Find(4267).") 70 | } else { 71 | fmt.Printf("ERROR: %v", err) 72 | } 73 | log.Fatal("An error ocurred, cannot continue.") 74 | } 75 | 76 | fmt.Printf("Book: %#v", book) 77 | } 78 | -------------------------------------------------------------------------------- /tour/tutorials/queries/06/README.md: -------------------------------------------------------------------------------- 1 | # Update, insert, or delete records in a result set 2 | 3 | The records in result sets can not only be queried but also modified and 4 | removed. 5 | 6 | If you want to modify the properties of a whole result set, use `Update`: 7 | 8 | ```go 9 | var book Book 10 | res := booksCol.Find(4267) 11 | 12 | err = res.One(&book) 13 | ... 14 | 15 | book.Title = "New title" 16 | 17 | err = res.Update(book) 18 | ... 19 | ``` 20 | 21 | Note that the result above set consists of only one element, whereas the next 22 | result set consists of all the records in the collection: 23 | 24 | ```go 25 | res := booksCol.Find() 26 | 27 | // Updating all records in the result set. 28 | err := res.Update(map[string]int{ 29 | "author_id": 23, 30 | }) 31 | ``` 32 | 33 | If you want to remove all the records in a result set, use `Delete`: 34 | 35 | ```go 36 | res := booksCol.Find(4267) 37 | 38 | err := res.Delete() 39 | // ... 40 | ``` 41 | 42 | As with the `Update` examples, in the previous case, only one record will be 43 | affected, and in the following scenario, all records will be deleted: 44 | 45 | ```go 46 | res := booksCol.Find() 47 | 48 | // Deleting all records in the result-set. 49 | err := res.Delete() 50 | ... 51 | ``` 52 | 53 | Given that the examples in this tour are based on an SQL database, we'll 54 | elaborate on the use of both a) SQL builder methods and b) raw SQL statements. -------------------------------------------------------------------------------- /tour/tutorials/queries/06/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/upper/db/v4" 8 | "github.com/upper/db/v4/adapter/postgresql" 9 | ) 10 | 11 | var settings = postgresql.ConnectionURL{ 12 | Database: "booktown", 13 | Host: "postgres", 14 | User: "demo", 15 | Password: "b4dp4ss", 16 | Options: map[string]string{ 17 | "sslmode": "disable", // Disable TLS 18 | }, 19 | } 20 | 21 | type Book struct { 22 | ID uint `db:"id"` 23 | Title string `db:"title"` 24 | AuthorID uint `db:"author_id"` 25 | SubjectID uint `db:"subject_id"` 26 | } 27 | 28 | func main() { 29 | db.LC().SetLevel(db.LogLevelDebug) 30 | 31 | sess, err := postgresql.Open(settings) 32 | if err != nil { 33 | log.Fatal("Open: ", err) 34 | } 35 | defer sess.Close() 36 | 37 | booksTable := sess.Collection("books") 38 | 39 | // This result set includes a single record. 40 | res := booksTable.Find(4267) 41 | 42 | // The record is retrieved with the given ID. 43 | var book Book 44 | err = res.One(&book) 45 | if err != nil { 46 | log.Fatal("Find: ", err) 47 | } 48 | 49 | fmt.Printf("Book: %#v", book) 50 | 51 | // A change is made to a property. 52 | book.Title = "New title" 53 | 54 | fmt.Printf("Book (modified): %#v", book) 55 | fmt.Println("") 56 | 57 | // The result set is updated. 58 | if err := res.Update(book); err != nil { 59 | fmt.Printf("Update: %v\n", err) 60 | fmt.Printf("This is OK, we're running on a sandbox with a read-only database.\n") 61 | fmt.Println("") 62 | } 63 | 64 | // The result set is deleted. 65 | if err := res.Delete(); err != nil { 66 | fmt.Printf("Delete: %v\n", err) 67 | fmt.Printf("This is OK, we're running on a sandbox with a read-only database.\n") 68 | fmt.Println("") 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tour/tutorials/records/01/README.md: -------------------------------------------------------------------------------- 1 | # The `db.Record` interface 2 | 3 | The `db.Record` interface provides a bare minimum support for you to operate 4 | with Go structs in a more ORM-ish way: 5 | 6 | ```go 7 | package db 8 | 9 | type Record interface { 10 | Store(sess Session) Store 11 | } 12 | ``` 13 | 14 | The `db.Store` interface is a container for `db.Collection` that is defined 15 | like this: 16 | 17 | ```go 18 | type Store interface { 19 | Collection 20 | } 21 | ``` 22 | 23 | In the example below, `Book` is a struct that satisfies the `db.Record` 24 | interface: 25 | 26 | ```go 27 | package main 28 | 29 | import ( 30 | "github.com/upper/db/v4" 31 | ) 32 | 33 | type Book struct { 34 | ID uint `db:"id,omitempty"` 35 | Title string `db:"title"` 36 | AuthorID uint `db:"author_id,omitempty"` 37 | SubjectID uint `db:"subject_id,omitempty"` 38 | } 39 | 40 | func (book *Book) Store(sess db.Session) db.Store { 41 | return sess.Collection("books") 42 | } 43 | 44 | // Compile-time check. 45 | var _ = db.Record(&Book{}) 46 | ``` 47 | 48 | You can use the `db.Record` interface with special `db.Session` methods such as 49 | `Get`, `Save` or `Delete`: 50 | 51 | ```go 52 | var book Book 53 | 54 | // Get "The Shining" from the catalog 55 | err = sess.Get(&book, db.Cond{"title": "The Shining"}) 56 | // ... 57 | 58 | // Persist record to database 59 | err = sess.Save(&book) 60 | // ... 61 | 62 | // Create a new record 63 | book := Book{Title: "My new book"} 64 | err = sess.Save(&book) 65 | 66 | // Delete record 67 | err = sess.Delete(&book) 68 | ``` 69 | 70 | ## Hooks 71 | 72 | `db.Record` objects can optionally satisfy hooks, which are special methods 73 | called before or after specific events. For instance, if we'd like the `Book` 74 | record to execute code right before inserting a new entry into the database, 75 | we'd add a `BeforeCreate` hook like this: 76 | 77 | ```go 78 | func (book *Book) BeforeCreate(sess db.Session) error { 79 | // ... 80 | return nil 81 | } 82 | ``` 83 | 84 | `upper/db` records support the following hooks: 85 | 86 | * `BeforeCreate(db.Session) error` 87 | * `AfterCreate(db.Session) error` 88 | * `BeforeUpdate(db.Session) error` 89 | * `AfterUpdate(db.Session) error` 90 | * `BeforeDelete(db.Session) error` 91 | * `AfterDelete(db.Session) error` 92 | 93 | Hooks in `upper/db` run within a database transaction; if any of the hooks 94 | returns an error, the whole operation is canceled and rolled back. 95 | 96 | ## Validation 97 | 98 | Besides hooks, there's another optional interface defined as: 99 | 100 | ``` 101 | type Validator interface { 102 | Validate() error 103 | } 104 | ``` 105 | 106 | You can use the `Validator` interface to run validations against the record's 107 | data. 108 | -------------------------------------------------------------------------------- /tour/tutorials/records/01/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/upper/db/v4" 8 | "github.com/upper/db/v4/adapter/postgresql" 9 | ) 10 | 11 | var settings = postgresql.ConnectionURL{ 12 | Database: "booktown", 13 | Host: "postgres", 14 | User: "demo", 15 | Password: "b4dp4ss", 16 | Options: map[string]string{ 17 | "sslmode": "disable", // Disable TLS 18 | }, 19 | } 20 | 21 | // Book represents a record from the "books" table. 22 | type Book struct { 23 | ID uint `db:"id,omitempty"` 24 | Title string `db:"title"` 25 | AuthorID uint `db:"author_id,omitempty"` 26 | SubjectID uint `db:"subject_id,omitempty"` 27 | } 28 | 29 | func (book *Book) Store(sess db.Session) db.Store { 30 | return sess.Collection("books") 31 | } 32 | 33 | func (book *Book) BeforeUpdate(sess db.Session) error { 34 | fmt.Println("**** BeforeUpdate was called ****") 35 | return nil 36 | } 37 | 38 | func (book *Book) AfterUpdate(sess db.Session) error { 39 | fmt.Println("**** AfterUpdate was called ****") 40 | return nil 41 | } 42 | 43 | // Interface checks 44 | var _ = interface { 45 | db.Record 46 | db.BeforeUpdateHook 47 | db.AfterUpdateHook 48 | }(&Book{}) 49 | 50 | func main() { 51 | sess, err := postgresql.Open(settings) 52 | if err != nil { 53 | log.Fatal("Open: ", err) 54 | } 55 | defer sess.Close() 56 | 57 | var book Book 58 | 59 | // Get a book 60 | err = sess.Get(&book, db.Cond{"title": "The Shining"}) 61 | if err != nil { 62 | log.Fatal("Get: ", err) 63 | } 64 | 65 | fmt.Printf("book: %#v\n", book) 66 | 67 | // Change the title 68 | book.Title = "The Shining (novel)" 69 | 70 | // Persist changes 71 | err = sess.Save(&book) 72 | if err != nil { 73 | // Allow this to fail in the sandbox 74 | log.Print("Save: ", err) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tour/tutorials/records/02/README.md: -------------------------------------------------------------------------------- 1 | # The `db.Store` interface 2 | 3 | The `db.Store` provides a foundation for creating data stores and custom 4 | methods around regular collections. 5 | 6 | The following example: 7 | ```go 8 | book, err := Books(sess).GetBookByTitle("The Shining") 9 | if err != nil { 10 | // ... 11 | } 12 | ``` 13 | 14 | could be implemented with a struct, a method, and a function: 15 | 16 | ```go 17 | package main 18 | 19 | import ( 20 | "github.com/upper/db/v4" 21 | ) 22 | 23 | // BooksStore represents a store for books 24 | type BooksStore struct { 25 | db.Collection 26 | } 27 | 28 | func (books *BooksStore) GetBookByTitle(title string) (*Book, error) { 29 | var book Book 30 | if err := books.Find(db.Cond{"title": title}).One(&book); err != nil { 31 | return nil, err 32 | } 33 | return &book, nil 34 | } 35 | 36 | // Books initializes a BookStore 37 | func Books(sess db.Session) *BooksStore { 38 | return &BooksStore{sess.Collection("books")} 39 | } 40 | 41 | // Interface check 42 | var _ = interface{db.Store}(&BooksStore{}) 43 | ``` 44 | 45 | You can use the `Books` function depicted above to create new instances of 46 | `BooksStore`; a common use case for this is the `Store()` method of a 47 | `db.Record`: 48 | 49 | ```go 50 | // Book represents a record from the "books" table. 51 | type Book struct { 52 | // ... 53 | } 54 | 55 | func (book *Book) Store(sess db.Session) db.Store { 56 | return Books(sess) 57 | } 58 | ``` 59 | 60 | Keep in mind that using `db.Record` and/or `db.Store` interfaces is completely 61 | optional, the ultimate decision should be based on the needs of your project. 62 | 63 | The `db.Store` interface is only available for databases that support 64 | transactions, such as CockroachDB, PostgreSQL, MySQL, MSSQL, SQLite and ql. 65 | -------------------------------------------------------------------------------- /tour/tutorials/records/02/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/upper/db/v4" 8 | "github.com/upper/db/v4/adapter/postgresql" 9 | ) 10 | 11 | var settings = postgresql.ConnectionURL{ 12 | Database: "booktown", 13 | Host: "postgres", 14 | User: "demo", 15 | Password: "b4dp4ss", 16 | Options: map[string]string{ 17 | "sslmode": "disable", // Disable TLS 18 | }, 19 | } 20 | 21 | type BooksStore struct { 22 | db.Collection 23 | } 24 | 25 | func (books *BooksStore) GetBookByTitle(title string) (*Book, error) { 26 | var book Book 27 | if err := books.Find(db.Cond{"title": title}).One(&book); err != nil { 28 | return nil, err 29 | } 30 | return &book, nil 31 | } 32 | 33 | func Books(sess db.Session) *BooksStore { 34 | return &BooksStore{sess.Collection("books")} 35 | } 36 | 37 | // Book represents a record from the "books" table. 38 | type Book struct { 39 | ID uint `db:"id,omitempty"` 40 | Title string `db:"title"` 41 | AuthorID uint `db:"author_id,omitempty"` 42 | SubjectID uint `db:"subject_id,omitempty"` 43 | } 44 | 45 | func (book *Book) Store(sess db.Session) db.Store { 46 | return Books(sess) 47 | } 48 | 49 | func (book *Book) BeforeUpdate(sess db.Session) error { 50 | fmt.Println("**** BeforeUpdate was called ****") 51 | return nil 52 | } 53 | 54 | func (book *Book) AfterUpdate(sess db.Session) error { 55 | fmt.Println("**** AfterUpdate was called ****") 56 | return nil 57 | } 58 | 59 | // Interface checks 60 | var _ = interface { 61 | db.Record 62 | db.BeforeUpdateHook 63 | db.AfterUpdateHook 64 | }(&Book{}) 65 | 66 | var _ = interface { 67 | db.Store 68 | }(&BooksStore{}) 69 | 70 | func main() { 71 | sess, err := postgresql.Open(settings) 72 | if err != nil { 73 | log.Fatal("Open: ", err) 74 | } 75 | defer sess.Close() 76 | 77 | // Get a book 78 | book, err := Books(sess).GetBookByTitle("The Shining") 79 | if err != nil { 80 | log.Fatal("Get: ", err) 81 | } 82 | 83 | fmt.Printf("book: %#v\n", book) 84 | 85 | // Change the title 86 | book.Title = "The Shining (novel)" 87 | 88 | // Persist changes 89 | err = sess.Save(book) 90 | if err != nil { 91 | // Allow this to fail in the sandbox 92 | log.Print("Save: ", err) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tour/tutorials/sql-builder/01/README.md: -------------------------------------------------------------------------------- 1 | ## a) SQL Builder: Select 2 | 3 | All the adapters for SQL databases come with a handy SQL builder that you can 4 | use to compose queries. 5 | 6 | You can access the SQL builder on SQL adapters using the `SQL()` method: 7 | 8 | ```go 9 | builder := sess.SQL() 10 | ``` 11 | 12 | There are different methods you can use to begin your query, for instance 13 | `SelectFrom`: 14 | 15 | ```go 16 | q := sess.SQL(). 17 | SelectFrom("books") 18 | ``` 19 | 20 | `SelectFrom` returns a [Selector][1] and some `Selector` methods return 21 | `Selector` too, so you can chain method calls like this: 22 | 23 | ```go 24 | q := sess.SQL(). 25 | SelectFrom("books"). 26 | Where("title LIKE ?", "P%") 27 | ``` 28 | 29 | or 30 | 31 | ```go 32 | q := sess.SQL(). 33 | SelectFrom("books") 34 | 35 | q = q.Where("title LIKE ?", "P%") 36 | ``` 37 | 38 | Note that we're reassigning `q` in the last example. This is because queries 39 | are immutable, and methods do not affect the caller, like in the next case 40 | (where `q` is not affected by `Where`): 41 | 42 | ```go 43 | q := sess.SelectFrom("books") 44 | 45 | p := q.Where("title LIKE ?", "P%"). 46 | OrderBy("title") 47 | ``` 48 | 49 | You can use `All` or `One` to compile, execute, and map results into a Go type: 50 | 51 | ```go 52 | var books []Book 53 | err := q.All(&books) 54 | ``` 55 | 56 | or 57 | 58 | ```go 59 | var book Book 60 | err := q.One(&book) 61 | ``` 62 | 63 | The `Selector` interface also features a special `Iterator` method to create an 64 | iterator and go through the results one by one: 65 | 66 | ```go 67 | iter := q.Iterator() 68 | defer iter.Close() 69 | 70 | for iter.Next(&book) { 71 | // ... 72 | } 73 | 74 | if err := iter.Err(); err != nil { 75 | // ... 76 | } 77 | ``` 78 | 79 | [1]: https://pkg.go.dev/github.com/upper/db/v4#Selector 80 | -------------------------------------------------------------------------------- /tour/tutorials/sql-builder/01/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/upper/db/v4/adapter/postgresql" 8 | ) 9 | 10 | var settings = postgresql.ConnectionURL{ 11 | Database: `booktown`, 12 | Host: `postgres`, 13 | User: `demo`, 14 | Password: `b4dp4ss`, 15 | } 16 | 17 | type Book struct { 18 | ID uint `db:"id,omitempty"` 19 | Title string `db:"title"` 20 | AuthorID uint `db:"author_id,omitempty"` 21 | SubjectID uint `db:"subject_id,omitempty"` 22 | } 23 | 24 | func main() { 25 | sess, err := postgresql.Open(settings) 26 | if err != nil { 27 | log.Fatal("Open: ", err) 28 | } 29 | defer sess.Close() 30 | 31 | // The Collection / Find / Result syntax was created with compatibility 32 | // across all supported databases in mind. However, sometimes it might not be 33 | // enough for all your needs, especially when working with complex queries. 34 | 35 | // In such a case, you can also use the SQL builder. 36 | q := sess.SQL().SelectFrom("books") 37 | 38 | // `q` is a `sqlbuilder.Selector`, you can chain any of its other methods 39 | // that also return `Selector`. 40 | q = q.OrderBy("title") 41 | 42 | // Note that queries are immutable, here `p` is a completely independent 43 | // query. 44 | p := q.Where("title LIKE ?", "P%") 45 | 46 | // Queries are not compiled nor executed until you use methods like `One` or 47 | // `All`. 48 | var booksQ, booksP []Book 49 | if err := q.All(&booksQ); err != nil { 50 | log.Fatal("q.All: ", err) 51 | } 52 | 53 | // The `Iterator` method is a way to go through large result sets from top to 54 | // bottom. 55 | booksP = make([]Book, 0, len(booksQ)) 56 | iter := p.Iterator() 57 | var book Book 58 | for iter.Next(&book) { 59 | booksP = append(booksP, book) 60 | } 61 | 62 | // Remember to check for error values at the end of the loop. 63 | if err := iter.Err(); err != nil { 64 | log.Fatal("iter.Err: ", err) 65 | } 66 | // ... and to free up any locked resources. 67 | if err := iter.Close(); err != nil { 68 | log.Fatal("iter.Close: ", err) 69 | } 70 | 71 | // Listing all books 72 | fmt.Printf("All books:\n") 73 | for _, book := range booksQ { 74 | fmt.Printf("Book %d:\t%q\n", book.ID, book.Title) 75 | } 76 | fmt.Println("") 77 | 78 | // Listing books that begin with P 79 | fmt.Printf("Books that begin with \"P\":\n") 80 | for _, book := range booksP { 81 | fmt.Printf("Book %d:\t%q\n", book.ID, book.Title) 82 | } 83 | fmt.Println("") 84 | } 85 | -------------------------------------------------------------------------------- /tour/tutorials/sql-builder/02/README.md: -------------------------------------------------------------------------------- 1 | ## a) SQL Builder: JOIN Queries and Struct Composition 2 | 3 | Now, let's suppose you have independent Go structs (each one mapped to a 4 | different table) and a JOIN query that returns a result combining columns from 5 | all the mapped tables. 6 | 7 | In this scenario, you can create a new struct and embed all the structs using 8 | the `inline` option (`db:",inline"`), like in the following example (where 9 | `Book`, `Author`, and `Subject` are independent structs): 10 | 11 | ```go 12 | type BookAuthorSubject struct { 13 | Book `db:",inline"` 14 | Author `db:",inline"` 15 | Subject `db:",inline"` 16 | } 17 | ``` 18 | 19 | ... and then create the JOIN query using the builder: 20 | 21 | 22 | ```go 23 | q := sess.Select("b.id AS book_id", "*"). 24 | From("books AS b"). 25 | Join("subjects AS s").On("b.subject_id = s.id"). 26 | Join("authors AS a").On("b.author_id = a.id"). 27 | OrderBy("a.last_name", "b.title") 28 | ``` 29 | 30 | Note that an alias for `book_id` is created with `Select("b.id AS book_id", 31 | "*")`. This is because all three embedded structs have a field with the same 32 | name (`id`), which is ambiguous. The final struct (`BookAuthorSubject`) should 33 | look like this: 34 | 35 | ```go 36 | type BookAuthorSubject struct { 37 | BookID uint `db:"book_id"` 38 | 39 | Book `db:",inline"` 40 | Author `db:",inline"` 41 | Subject `db:",inline"` 42 | } 43 | ``` 44 | 45 | Finally, use `All` to copy every single result of the query into the `books` 46 | slice: 47 | 48 | ```go 49 | var books []BookAuthorSubject 50 | err := q.All(&books) 51 | ``` 52 | -------------------------------------------------------------------------------- /tour/tutorials/sql-builder/02/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/upper/db/v4/adapter/postgresql" 8 | ) 9 | 10 | var settings = postgresql.ConnectionURL{ 11 | Database: "booktown", 12 | Host: "postgres", 13 | User: "demo", 14 | Password: "b4dp4ss", 15 | Options: map[string]string{ 16 | "sslmode": "disable", // Disable TLS 17 | }, 18 | } 19 | 20 | // Book represents a record from the "books" table. 21 | // booktown=> \d books 22 | // 23 | // Table "public.books" 24 | // Column | Type | Modifiers 25 | // 26 | // ------------+---------+----------- 27 | // 28 | // id | integer | not null 29 | // title | varchar | not null 30 | // author_id | integer | 31 | // subject_id | integer | 32 | // 33 | // Indexes: 34 | // 35 | // "books_id_pkey" PRIMARY KEY, btree (id) 36 | // "books_title_idx" btree (title) 37 | type Book struct { 38 | ID uint `db:"id,omitempty"` 39 | Title string `db:"title"` 40 | AuthorID uint `db:"author_id,omitempty"` 41 | SubjectID uint `db:"subject_id,omitempty"` 42 | } 43 | 44 | // Author represents a record from the "authors" table. 45 | // booktown=> \d authors 46 | // 47 | // Table "public.authors" 48 | // Column | Type | Modifiers 49 | // 50 | // ------------+---------+----------- 51 | // 52 | // id | integer | not null 53 | // last_name | text | 54 | // first_name | text | 55 | // 56 | // Indexes: 57 | // 58 | // "authors_pkey" PRIMARY KEY, btree (id) 59 | type Author struct { 60 | ID uint `db:"id,omitempty"` 61 | LastName string `db:"last_name"` 62 | FirstName string `db:"first_name"` 63 | } 64 | 65 | // Subject represents a record from the "subjects" table. 66 | // booktown=> \d subjects 67 | // 68 | // Table "public.subjects" 69 | // Column | Type | Modifiers 70 | // 71 | // ----------+---------+----------- 72 | // 73 | // id | integer | not null 74 | // subject | text | 75 | // location | text | 76 | // 77 | // Indexes: 78 | // 79 | // "subjects_pkey" PRIMARY KEY, btree (id) 80 | type Subject struct { 81 | ID uint `db:"id,omitempty"` 82 | Subject string `db:"subject"` 83 | Location string `db:"location"` 84 | } 85 | 86 | func main() { 87 | sess, err := postgresql.Open(settings) 88 | if err != nil { 89 | log.Fatal("Open: ", err) 90 | } 91 | defer sess.Close() 92 | 93 | // The BookAuthorSubject type represents an element that has columns from 94 | // different tables. 95 | type BookAuthorSubject struct { 96 | // The book_id column was added to prevent collisions with the other "id" 97 | // columns from Author and Subject. 98 | BookID uint `db:"book_id"` 99 | 100 | Book `db:",inline"` 101 | Author `db:",inline"` 102 | Subject `db:",inline"` 103 | } 104 | 105 | // This is a query with a JOIN clause that was built using the SQL builder. 106 | q := sess.SQL(). 107 | Select("b.id AS book_id", "*"). // Note the alias set for book.id. 108 | From("books AS b"). 109 | Join("subjects AS s").On("b.subject_id = s.id"). 110 | Join("authors AS a").On("b.author_id = a.id"). 111 | OrderBy("a.last_name", "b.title") 112 | 113 | // The JOIN query above returns data from three different tables. 114 | var books []BookAuthorSubject 115 | if err := q.All(&books); err != nil { 116 | log.Fatal("q.All: ", err) 117 | } 118 | 119 | for _, book := range books { 120 | fmt.Printf("Book %d:\t%s. %q on %s\n", book.BookID, book.Author.LastName, book.Book.Title, book.Subject.Subject) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /tour/tutorials/sql-builder/03/README.md: -------------------------------------------------------------------------------- 1 | ## a) SQL Builder: Update, Insert and Delete 2 | 3 | The `Update` method creates and returns an [Updater][1] that you can use to 4 | build an UPDATE statement: 5 | 6 | ```go 7 | q := sess.SQL(). 8 | Update("authors"). 9 | Set("first_name = ?", "Edgar Allan"). 10 | Where("id = ?", eaPoe.ID) 11 | 12 | res, err := q.Exec() 13 | ``` 14 | 15 | The `InsertInto` method creates and returns an [Inserter][2] that you can use 16 | to build an INSERT statement: 17 | 18 | ```go 19 | res, err = sess.SQL(). 20 | InsertInto("books"). 21 | Columns( 22 | "title", 23 | "author_id", 24 | "subject_id", 25 | ). 26 | Values( 27 | "Brave New World", 28 | 45, 29 | 11, 30 | ). 31 | Exec() 32 | ``` 33 | 34 | In this case, using `Columns` is not mandatory. You can pass a struct to the 35 | `Values` method so it is mapped to columns and values, as shown below: 36 | 37 | ```go 38 | book := Book{ 39 | Title: "The Crow", 40 | AuthorID: eaPoe.ID, 41 | } 42 | 43 | res, err = sess.SQL(). 44 | InsertInto("books"). 45 | Values(book). 46 | Exec() 47 | ``` 48 | 49 | The `DeleteFrom` method creates and returns a [Deleter][3] that you can use to 50 | build a DELETE query: 51 | 52 | ```go 53 | q := sess.SQL(). 54 | DeleteFrom("books"). 55 | Where("title", "The Crow") 56 | 57 | res, err := q.Exec() 58 | ``` 59 | 60 | ## b) Raw SQL 61 | 62 | If none of the previous methods described are enough to express your query, you 63 | can use raw SQL. Look at the [db.SQL][4] interface to learn about all available 64 | methods for building and executing raw SQL statements. 65 | 66 | ```go 67 | res, err := sess.SQL().Exec(`UPDATE authors SET first_name = ? WHERE id = ?`, "Edgar 68 | Allan", eaPoe.ID) 69 | ... 70 | 71 | res, err := sess.SQL().Exec(`INSERT INTO authors VALUES`) 72 | ... 73 | 74 | res, err := sess.SQL().Exec(`DELETE authors WHERE id = ?`, "Edgar Allan", eaPoe.ID) 75 | ``` 76 | 77 | [1]: https://pkg.go.dev/github.com/upper/db/v4#Updater 78 | [2]: https://pkg.go.dev/github.com/upper/db/v4#Inserter 79 | [3]: https://pkg.go.dev/github.com/upper/db/v4#Deleter 80 | [4]: https://pkg.go.dev/github.com/upper/db/v4#SQL 81 | -------------------------------------------------------------------------------- /tour/tutorials/sql-builder/03/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/upper/db/v4" 8 | "github.com/upper/db/v4/adapter/postgresql" 9 | ) 10 | 11 | var settings = postgresql.ConnectionURL{ 12 | Database: "booktown", 13 | Host: "postgres", 14 | User: "demo", 15 | Password: "b4dp4ss", 16 | Options: map[string]string{ 17 | "sslmode": "disable", // Disable TLS 18 | }, 19 | } 20 | 21 | // Book represents a record from the "books" table. 22 | type Book struct { 23 | ID uint `db:"id,omitempty"` 24 | Title string `db:"title"` 25 | AuthorID uint `db:"author_id,omitempty"` 26 | SubjectID uint `db:"subject_id,omitempty"` 27 | } 28 | 29 | // Author represents a record from the "authors" table. 30 | type Author struct { 31 | ID uint `db:"id,omitempty"` 32 | LastName string `db:"last_name"` 33 | FirstName string `db:"first_name"` 34 | } 35 | 36 | // Subject represents a record from the "subjects" table. 37 | type Subject struct { 38 | ID uint `db:"id,omitempty"` 39 | Subject string `db:"subject"` 40 | Location string `db:"location"` 41 | } 42 | 43 | func main() { 44 | sess, err := postgresql.Open(settings) 45 | if err != nil { 46 | log.Fatal("Open: ", err) 47 | } 48 | defer sess.Close() 49 | 50 | db.LC().SetLevel(db.LogLevelDebug) 51 | 52 | var eaPoe Author 53 | 54 | // We use sqlbuilder.Selector to retrieve the last name "Poe" from the 55 | // "authors" table. 56 | err = sess.SQL(). 57 | SelectFrom("authors"). 58 | Where("last_name", "Poe"). // Or Where("last_name = ?", "Poe") 59 | One(&eaPoe) 60 | if err != nil { 61 | log.Fatal("Query: ", err) 62 | } 63 | fmt.Printf("eaPoe: %#v\n", eaPoe) 64 | 65 | // We use sqlbuilder.Updater to correct the typo in the name "Edgar Allen". 66 | res, err := sess.SQL(). 67 | Update("authors"). 68 | Set("first_name = ?", "Edgar Allan"). // Or Set("first_name", "Edgar Allan"). 69 | Where("id = ?", eaPoe.ID). // Or Where("id", eaPoe.ID) 70 | Exec() 71 | if err != nil { 72 | fmt.Printf("Query: %v. This is expected on the read-only sandbox.\n", err) 73 | } 74 | 75 | // We use sqlbuilder.Inserter to add a new book under "Edgar Allan Poe". 76 | book := Book{ 77 | Title: "The Crow", 78 | AuthorID: eaPoe.ID, 79 | } 80 | res, err = sess.SQL(). 81 | InsertInto("books"). 82 | Values(book). // Or Columns(c1, c2, c2, ...).Values(v1, v2, v2, ...). 83 | Exec() 84 | if err != nil { 85 | fmt.Printf("Query: %v. This is expected on the read-only sandbox.\n", err) 86 | } 87 | if res != nil { 88 | id, _ := res.LastInsertId() 89 | fmt.Printf("New book id: %d\n", id) 90 | } 91 | 92 | // We use sqlbuilder.Deleter to erase the book we've just created (and any 93 | // other book with the same name). 94 | q := sess.SQL(). 95 | DeleteFrom("books"). 96 | Where("title", "The Crow") 97 | fmt.Printf("Compiled query: %v\n", q) 98 | 99 | _, err = q.Exec() 100 | if err != nil { 101 | fmt.Printf("Query: %v. This is expected on the read-only sandbox\n", err) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tour/tutorials/sql-builder/04/README.md: -------------------------------------------------------------------------------- 1 | ## b) Raw SQL 2 | 3 | The raw SQL API provides all the power you need for custom queries, along with 4 | object mapping, and other useful features of `upper/db`. 5 | 6 | ```go 7 | rows, err := sess.SQL().Query(` 8 | SELECT id, first_name, last_name FROM authors WHERE last_name = ? 9 | `, "Poe") 10 | ... 11 | 12 | row, err := sess.SQL().QueryRow(`SELECT * FROM authors WHERE id = ?`, 23) 13 | ... 14 | ``` 15 | 16 | Use the `NewIterator` method to make mapping easier: 17 | 18 | ```go 19 | iter := sess.SQL().NewIterator(rows) 20 | 21 | var books []Book 22 | err := iter.All(&books) 23 | ``` 24 | 25 | This iterator provides well-known `upper/db` methods like `One`, `All`, and 26 | `Next`. 27 | 28 | [1]: https://pkg.go.dev/github.com/upper/db/v4#SQL 29 | -------------------------------------------------------------------------------- /tour/tutorials/sql-builder/04/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/upper/db/v4/adapter/postgresql" 8 | ) 9 | 10 | var settings = postgresql.ConnectionURL{ 11 | Database: "booktown", 12 | Host: "postgres", 13 | User: "demo", 14 | Password: "b4dp4ss", 15 | Options: map[string]string{ 16 | "sslmode": "disable", // Disable TLS 17 | }, 18 | } 19 | 20 | // Book represents a record from the "books" table. 21 | type Book struct { 22 | ID uint `db:"id,omitempty"` 23 | Title string `db:"title"` 24 | AuthorID uint `db:"author_id,omitempty"` 25 | SubjectID uint `db:"subject_id,omitempty"` 26 | } 27 | 28 | // Author represents a record from the "authors" table. 29 | type Author struct { 30 | ID uint `db:"id,omitempty"` 31 | LastName string `db:"last_name"` 32 | FirstName string `db:"first_name"` 33 | } 34 | 35 | // Subject represents a record from the "subjects" table. 36 | type Subject struct { 37 | ID uint `db:"id,omitempty"` 38 | Subject string `db:"subject"` 39 | Location string `db:"location"` 40 | } 41 | 42 | func main() { 43 | sess, err := postgresql.Open(settings) 44 | if err != nil { 45 | log.Fatal("Open: ", err) 46 | } 47 | defer sess.Close() 48 | 49 | var eaPoe Author 50 | 51 | // Query, QueryRow, and Exec are raw SQL methods you can use when db.SQL is 52 | // not enough for the complexity of your query. 53 | rows, err := sess.SQL(). 54 | Query(`SELECT id, first_name, last_name FROM authors WHERE last_name = ?`, "Poe") 55 | if err != nil { 56 | log.Fatal("Query: ", err) 57 | } 58 | 59 | // This is a standard query that mimics the API from database/sql. 60 | if !rows.Next() { 61 | log.Fatal("Expecting one row") 62 | } 63 | if err := rows.Scan(&eaPoe.ID, &eaPoe.FirstName, &eaPoe.LastName); err != nil { 64 | log.Fatal("Scan: ", err) 65 | } 66 | if err := rows.Close(); err != nil { 67 | log.Fatal("Close: ", err) 68 | } 69 | 70 | fmt.Printf("%#v", eaPoe) 71 | 72 | // Make sure to use Exec or Query, as the case may be. 73 | _, err = sess.SQL(). 74 | Exec(`UPDATE authors SET first_name = ? WHERE id = ?`, "Edgar Allan", eaPoe.ID) 75 | if err != nil { 76 | fmt.Printf("Query: %v. This is expected on the read-only sandbox\n", err) 77 | } 78 | 79 | // The sqlbuilder package provides tools for working with raw sql.Rows, such 80 | // as the NewIterator function. 81 | rows, err = sess.SQL(). 82 | Query(`SELECT * FROM books LIMIT 5`) 83 | if err != nil { 84 | log.Fatal("Query: ", err) 85 | } 86 | 87 | // The NewIterator function takes a *sql.Rows value and returns an Iterator. 88 | iter := sess.SQL().NewIterator(rows) 89 | 90 | // This iterator provides methods for going through data, such as All, One, 91 | // Next, and the like. If you use Next, remember to use Err and Close too. 92 | var books []Book 93 | if err := iter.All(&books); err != nil { 94 | log.Fatal("Query: ", err) 95 | } 96 | 97 | fmt.Printf("Books: %#v", books) 98 | } 99 | -------------------------------------------------------------------------------- /tour/tutorials/transactions/01/README.md: -------------------------------------------------------------------------------- 1 | # Transactions 2 | 3 | To create a transaction block, use the `Tx` method provided by `Session`. 4 | 5 | ```go 6 | import ( 7 | db "github.com/upper/db/v4" 8 | ) 9 | 10 | err := sess.Tx(func(tx db.Session) error { 11 | ... 12 | }) 13 | ``` 14 | 15 | The passed function defines what you want to do within the transaction; it 16 | receives a ready-to-be-used transactional session. This `tx` value can be used 17 | like a regular `db.Session`, except that any write operation that happens on it 18 | will be either fully committed or discarded (rolled back). 19 | 20 | If the passed function returns an error, the transaction gets rolled back: 21 | 22 | ```go 23 | err := sess.Tx(func(sess db.Session) error { 24 | ... 25 | return errors.New("Transaction failed") 26 | }) 27 | ``` 28 | 29 | If the passed function returns `nil`, the transaction will be commited: 30 | 31 | ```go 32 | err := sess.Tx(func(tx db.Session) error { 33 | ... 34 | return nil 35 | }) 36 | ``` 37 | -------------------------------------------------------------------------------- /tour/tutorials/transactions/01/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/upper/db/v4" 8 | "github.com/upper/db/v4/adapter/postgresql" 9 | ) 10 | 11 | var settings = postgresql.ConnectionURL{ 12 | Database: "booktown", 13 | Host: "postgres", 14 | User: "demo", 15 | Password: "b4dp4ss", 16 | Options: map[string]string{ 17 | "sslmode": "disable", // Disable TLS 18 | }, 19 | } 20 | 21 | // Book represents an record from the "books" table. 22 | type Book struct { 23 | ID uint `db:"id,omrecordpty"` 24 | Title string `db:"title"` 25 | AuthorID uint `db:"author_id,omrecordpty"` 26 | SubjectID uint `db:"subject_id,omrecordpty"` 27 | } 28 | 29 | // Author represents an record from the "authors" table. 30 | type Author struct { 31 | ID uint `db:"id,omrecordpty"` 32 | LastName string `db:"last_name"` 33 | FirstName string `db:"first_name"` 34 | } 35 | 36 | // Subject represents an record from the "subjects" table. 37 | type Subject struct { 38 | ID uint `db:"id,omrecordpty"` 39 | Subject string `db:"subject"` 40 | Location string `db:"location"` 41 | } 42 | 43 | func main() { 44 | sess, err := postgresql.Open(settings) 45 | if err != nil { 46 | log.Fatal("Open: ", err) 47 | } 48 | defer sess.Close() 49 | 50 | db.LC().SetLevel(db.LogLevelDebug) 51 | 52 | // The `tx` value in the function required by `sess.Tx` is just like `sess`, except 53 | // it lives within a transaction. This means that if the function returns an 54 | // error, the transaction will be rolled back. 55 | err = sess.Tx(func(tx db.Session) error { 56 | // Anything you set the `tx` variable to execute will be part of the 57 | // transaction. 58 | cols, err := tx.Collections() 59 | if err != nil { 60 | return err 61 | } 62 | fmt.Printf("Cols: %#v\n", cols) 63 | 64 | // The booksTable value is valid only within the transaction. 65 | booksTable := tx.Collection("books") 66 | total, err := booksTable.Find().Count() 67 | if err != nil { 68 | return err 69 | } 70 | fmt.Printf("There are %d records in %s\n", total, booksTable.Name()) 71 | 72 | var books []Book 73 | err = tx.SQL(). 74 | SelectFrom("books").Limit(3).OrderBy(db.Raw("RANDOM()")).All(&books) 75 | if err != nil { 76 | return err 77 | } 78 | fmt.Printf("Books: %#v\n", books) 79 | 80 | res, err := tx.SQL(). 81 | Query("SELECT * FROM books ORDER BY RANDOM() LIMIT 1") 82 | if err != nil { 83 | return err 84 | } 85 | 86 | var book Book 87 | err = tx.SQL().NewIterator(res).One(&book) 88 | if err != nil { 89 | return err 90 | } 91 | fmt.Printf("Random book: %#v\n", book) 92 | 93 | // If the function returns no error the transaction is committed. 94 | return nil 95 | }) 96 | 97 | if err != nil { 98 | fmt.Printf("sess.Tx: ", err) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tour/tutorials/welcome/01/README.md: -------------------------------------------------------------------------------- 1 | # Hello! 2 | 3 | Welcome to the [upper/db][1] tour. 4 | 5 | Here, you’ll learn how to use [upper/db][1] in your project. The current 6 | version of `upper/db` is v4. 7 | 8 | The example code displayed on the right is interactive. You can click on Run to 9 | see the output. Feel free to edit the code and run it as often as needed. 10 | 11 | Please remember that this tour uses read-only databases in a sandboxed 12 | environment, so any operations that would alter the environment (such as write 13 | operations) won’t work. 14 | 15 | If you want to improve anything, please do and submit your pull request. The 16 | tour’s code is [here](https://github.com/upper/tour). 17 | 18 | Have fun ✌️! 19 | 20 | [1]: https://upper.io/ 21 | -------------------------------------------------------------------------------- /tour/tutorials/welcome/01/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func main() { 8 | fmt.Println("Hello world!") 9 | } 10 | -------------------------------------------------------------------------------- /vanity/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM xiam/vanity:latest 2 | 3 | EXPOSE 9001 4 | 5 | ENTRYPOINT [ \ 6 | "/bin/vanity", \ 7 | "-addr", ":9001", \ 8 | "-repo-root", "https://github.com/upper", \ 9 | "-vanity-root", "https://upper.io" \ 10 | ] 11 | -------------------------------------------------------------------------------- /vanity/ansible/tasks.yml: -------------------------------------------------------------------------------- 1 | - set_fact: 2 | image_name: "upper/vanity" 3 | image_tag: "{{ docker_image_tag }}" 4 | container_name: "{{ container_prefix }}vanity" 5 | app_bind_port: 9001 6 | container_bind_port: 9001 7 | - set_fact: 8 | image_full_name: "{{ public_docker_registry }}/{{ image_name }}:{{ image_tag }}" 9 | - name: "pull docker image: {{ image_full_name }}" 10 | docker_image: 11 | name: "{{ image_full_name }}" 12 | source: "pull" 13 | state: "present" 14 | force_source: "yes" 15 | - name: "stop container: {{ container_name }}" 16 | docker_container: 17 | name: "{{ container_name }}" 18 | state: "stopped" 19 | ignore_errors: "yes" 20 | - name: "(re)start container: {{ container_name }}" 21 | docker_container: 22 | name: "{{ container_name }}" 23 | image: "{{ image_full_name }}" 24 | state: "started" 25 | restart_policy: "always" 26 | networks: 27 | - name: "{{ network_name }}" 28 | ports: 29 | - "{{ container_bind_ip }}:{{ container_bind_port }}:{{ app_bind_port }}" 30 | env: 31 | ENV: "{{ env }}" 32 | - name: "legacy health check (db.v3)" 33 | uri: 34 | url: "http://{{ container_bind_ip }}:{{ container_bind_port }}/db.v3?go-get=1" 35 | method: GET 36 | headers: 37 | Host: upper.io 38 | return_content: yes 39 | register: response 40 | until: response.status == 200 41 | retries: "{{ healthcheck_retries }}" 42 | delay: "{{ healthcheck_interval }}" 43 | failed_when: "'tree/3' not in response.content" 44 | - name: "legacy health check (db.v2)" 45 | uri: 46 | url: "http://{{ container_bind_ip }}:{{ container_bind_port }}/db.v2?go-get=1" 47 | method: GET 48 | status_code: 49 | - 200 50 | headers: 51 | Host: upper.io 52 | return_content: yes 53 | register: response 54 | failed_when: "'tree/2' not in response.content" 55 | - name: "legacy health check (db.v1)" 56 | uri: 57 | url: "http://{{ container_bind_ip }}:{{ container_bind_port }}/db.v1?go-get=1" 58 | method: GET 59 | status_code: 60 | - 200 61 | headers: 62 | Host: upper.io 63 | return_content: yes 64 | register: response 65 | failed_when: "'tree/1' not in response.content" 66 | - name: "legacy health check (db.v0)" 67 | uri: 68 | url: "http://{{ container_bind_ip }}:{{ container_bind_port }}/db?go-get=1" 69 | method: GET 70 | status_code: 71 | - 200 72 | headers: 73 | Host: upper.io 74 | return_content: yes 75 | register: response 76 | failed_when: "'tree/master' not in response.content" 77 | --------------------------------------------------------------------------------